@globisnv/cypress-core 2.1.1 → 2.1.3

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