@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 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 scripts. 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.
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
- scripts/ # standalone lint rules (shipped to consumers)
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. `src/types.ts` has `noUnusedVariables` disabled since it is declaration-only.
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 scripts
246
+ ### Shipped lint plugins
247
247
 
248
- `scripts/` is published with the package so consumer repos can run the same rule over their specs. It's a standalone Node script (no ESLint) that reads the invoking repo's `tsconfig.json` from `process.cwd()`, using the TypeScript 7 `typescript/unstable/*` preview API — the classic in-process Compiler API that `typescript-eslint` and `ts-morph` depend on was removed in TS7, which is why those tools cap their peer range below it.
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
- | Script | What it does |
250
+ | Plugin | What it does |
251
251
  | --- | --- |
252
- | [check-unnecessary-waiting.mjs](scripts/check-unnecessary-waiting.mjs) | Fails on `cy.wait(<number>)`, including numeric constants/default params aliased in the enclosing scope. Exempt a call with a `// biome-ignore plugin: <reason>` comment on the line directly above — Biome's GritQL plugins have no suppression mechanism, hence the standalone script. |
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
- "scripts": {
259
- "lint:waiting": "node node_modules/@globisnv/cypress-core/scripts/check-unnecessary-waiting.mjs"
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.7/schema.json",
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, 'btn_select_relation'], obj.customerRelation);
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 || obj.deliveryAddress || obj.loadStartBooked || obj.loadEndBooked || obj.loadStartBookedTime || obj.loadEndBookedTime) {
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.GLO_addInput([formNameSea, 'from_logistic_location'], obj.POL);
87
- cy.GLO_addInput([formNameSea, 'to_logistic_location'], obj.POD);
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
- cy.wait(250); // Special dropdown for filter, doesn't work without small wait..
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
- cy.GLO_checkField(selector, value, equals ? contains : undefined);
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); // Wait because AGLoader is not instant
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
- cy.wait(500); // Else both click and dblclick fire too fast
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
- cy.wait(500); // Else both click and dblclick fire too fast
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); // Wait for Okta to process logout
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.add('GLO_getCurrentRecordCount', () => {
2106
- return cy
2107
- .GLO_getElement(['ng$datacontrol', 'datacontrol'])
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(currentStr.trim(), 10),
2117
- totalRecords: parseInt(totalStr.trim(), 10)
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
- const scrollAndSearch = (attempts = 0) => {
18
- if (attempts > 20) {
19
- throw new Error(`Could not find menu item: ${value}`);
20
- }
21
- return cy
22
- .get('.ag-body-viewport')
23
- .first()
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.2",
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
- "scripts",
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.7",
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",
@@ -1,6 +1,7 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
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
  }
@@ -9,13 +9,13 @@
9
9
  "typescript": "^7.0.2"
10
10
  },
11
11
  "devDependencies": {
12
- "@biomejs/biome": "^2.5.7",
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 . && node node_modules/@globisnv/cypress-core/scripts/check-unnecessary-waiting.mjs",
18
- "lint:fix": "biome check . --write && node node_modules/@globisnv/cypress-core/scripts/check-unnecessary-waiting.mjs",
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();