@globisnv/cypress-core 2.3.0 → 2.4.0

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/biome.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.6.3/schema.json",
3
3
  "vcs": {
4
4
  "enabled": true,
5
5
  "clientKind": "git",
6
6
  "useIgnoreFile": true
7
7
  },
8
8
  "files": {
9
- "includes": ["**", "!dist", "!template"]
9
+ "includes": ["**", "!dist", "!template", "!**/*.md", "!**/*.yml"]
10
10
  },
11
11
  "formatter": {
12
12
  "enabled": true,
@@ -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';
@@ -29,7 +29,14 @@ export const GLO_enterConsignment = (obj, formName = defaultFormName, formNameGe
29
29
  cy.GLO_addInput([formName, 'equipment_type_id'], obj.equipmentType);
30
30
  cy.GLO_checkLoadingIndicator();
31
31
  }
32
- if (obj.collectionAddress || obj.deliveryAddress || obj.loadStartBooked || obj.loadEndBooked || obj.loadStartBookedTime || obj.loadEndBookedTime) {
32
+ if (obj.collectionAddress ||
33
+ obj.deliveryAddress ||
34
+ obj.loadStartBooked ||
35
+ obj.loadEndBooked ||
36
+ obj.loadStartBookedTime ||
37
+ obj.loadEndBookedTime ||
38
+ obj.loadStartRequested ||
39
+ obj.unloadStartRequested) {
33
40
  cy.GLO_selectTab([formName, 'tabs_70'], 'General', open);
34
41
  if (obj.collectionAddress) {
35
42
  cy.GLO_clickButton([formNameGeneral, 'btn_select_from_address'], 'fa-search-plus');
@@ -43,6 +50,8 @@ export const GLO_enterConsignment = (obj, formName = defaultFormName, formNameGe
43
50
  obj.deliveryAddress.type ??= 'search';
44
51
  GLO_enterAddressPopup(obj.deliveryAddress);
45
52
  }
53
+ cy.GLO_addDate(input, [formNameGeneral, 'load_start_requested'], obj.loadStartRequested);
54
+ cy.GLO_addDate(input, [formNameGeneral, 'unload_start_requested'], obj.unloadStartRequested);
46
55
  cy.GLO_addDate(input, [formNameGeneral, 'load_start_booked'], obj.loadStartBooked);
47
56
  cy.GLO_addDate(input, [formNameGeneral, 'load_start_booked_time'], obj.loadStartBookedTime);
48
57
  cy.GLO_addDate(input, [formNameGeneral, 'load_end_booked'], obj.loadEndBooked);
@@ -87,8 +96,8 @@ const seaTab = (obj, formName, formNameSea) => {
87
96
  cy.GLO_addInput([formNameSea, 'txt_shipper_reference'], obj.shipperRef);
88
97
  cy.GLO_addInput([formNameSea, 'supplier_incoterm'], obj.shipperIncoTerm);
89
98
  cy.GLO_addInput([formNameSea, 'quay_code'], obj.quayCode);
90
- cy.GLO_addInput([formNameSea, 'from_logistic_location'], obj.POL);
91
- cy.GLO_addInput([formNameSea, 'to_logistic_location'], obj.POD);
99
+ cy.GLO_popupWindow([formNameSea, 'from_logistic_location'], obj.POL, undefined, equalBool, typeSearch);
100
+ cy.GLO_popupWindow([formNameSea, 'to_logistic_location'], obj.POD, undefined, equalBool, typeSearch);
92
101
  cy.GLO_addInput([formNameSea, 'transport_method'], obj.transportMethod);
93
102
  cy.GLO_addDate(input, [formNameSea, 'atd'], obj.atd);
94
103
  cy.GLO_addDate(input, [formNameSea, 'goods_closing'], obj.goodsClosing);
@@ -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
  };
@@ -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) => {
@@ -167,7 +167,7 @@ Cypress.Commands.add('FOB_showGridColumn', (column, shouldShow) => {
167
167
  else {
168
168
  $checkbox = $sidebar
169
169
  .find('.ag-column-select-column-label')
170
- .filter((_, el) => new RegExp(`^${column}$`, 'i').test(el.innerText))
170
+ .filter((_, el) => new RegExp(`^${column}$`, 'i').test(el.textContent ?? ''))
171
171
  .closest('.ag-column-select-column')
172
172
  .find('input.ag-checkbox-input');
173
173
  }
@@ -610,6 +610,36 @@ declare global {
610
610
  * @param {number} [interval] - use this TOGETHER with timeOut for slow-running updates (e.g. batch process) (default: 15s)
611
611
  */
612
612
  GLO_validateGridOrFail(waitValue: string, failValue: string, rowNumber?: number, columnId?: string, timeOut?: number, interval?: number): void;
613
+ /**
614
+ * Command to wait until a field shows a value, and to fail instantly when it shows a fail value.
615
+ * The field counterpart of GLO_validateGridOrFail, e.g. for a status that is updated by a running process.
616
+ * The displayed value is checked, as in GLO_checkField (the text of a valuelist, not its stored code).
617
+ *
618
+ * @param {selectorType} selector - [formName, elementName] of the field.
619
+ * @param {string} waitValue - The value to wait for.
620
+ * @param {string | string[]} failValue - The value(s) that stop the waiting and fail instantly.
621
+ * @param {number} [timeOut] - how long it should wait for the field to update, default: 10m
622
+ * @param {number} [interval] - how often the field is checked, default: 15s
623
+ * @example - cy.GLO_validateFieldOrFail(['ng$billing_task_dtl', 'status'], 'Finished', ['Error', 'Finished with error'], 300000, 2000);
624
+ */
625
+ GLO_validateFieldOrFail(selector: selectorType, waitValue: string, failValue: string | string[], timeOut?: number, interval?: number): void;
626
+ /**
627
+ * Executes a batch processing type and waits until the run it started has finished.
628
+ * The last run of the type is remembered before Execute, so a run that finished earlier is never taken for
629
+ * the new one (row 0 of the 'Batch processing' tab is still the previous run right after Execute).
630
+ * The command opens 'Batch processing type' itself and closes it again when the run has finished.
631
+ *
632
+ * @param {string} batchType - The value to filter the batch processing type list on.
633
+ * @param {string} [columnId='description'] - The column of the batch processing type list to filter on (e.g. 'description', 'transaction_type_text', 'batch_module').
634
+ * @param {string | null} [waitValue='Completed'] - The status the new run must end with. Use null to only wait until the run has finished, whatever its status,
635
+ * e.g. for a batch that processes the records of the whole environment, where one record of a parallel test can turn the run into Error.
636
+ * @param {string} [failValue='Error'] - The status that stops the waiting and fails instantly (not used when waitValue is null).
637
+ * @param {number} [timeOut] - how long it should wait for the run to finish, default: 20m
638
+ * @param {number} [interval] - how often the 'Batch processing' tab is checked, default: 15s
639
+ * @returns {Chainable<batchRunType>} - Yields the new run (submitted, completed, status, error message).
640
+ * @example - cy.GLO_executeBatchProcess('Batch processor accountancy SALES', 'batch_module', null);
641
+ */
642
+ GLO_executeBatchProcess(batchType: string, columnId?: string, waitValue?: string | null, failValue?: string, timeOut?: number, interval?: number): Chainable<batchRunType>;
613
643
  /**
614
644
  * Store the value(s) of one or more fields into the nested '@saveData' alias:
615
645
  * saveData[formName][elementName] = value
@@ -1,4 +1,4 @@
1
- import { bottom, buttonSearch, check, contains, cypressWebsocketLoader, defaultTimeout, enabled, equal, greaterEqual, gridDropDown, gridInput, gridInputWithoutEnter, gridSelect, hideInCommandLog, input, isInvalid, noEqualBool, noSkipCheck, notEqual, parseLocalizedNumber, row, select, skipCheck, span, top, unCheck } from '../tsHelper.js';
1
+ import { bottom, buttonSearch, check, contains, cypressWebsocketLoader, defaultTimeout, enabled, equal, greaterEqual, gridDropDown, gridInput, gridInputWithoutEnter, gridSelect, hideInCommandLog, input, isInvalid, noEqualBool, noSkipCheck, notEqual, open, parseLocalizedNumber, row, select, skipCheck, span, top, unCheck } from '../tsHelper.js';
2
2
  import { attachWebSocketSpy } from '../websocketDetector.js';
3
3
  //! File-specific helpers
4
4
  const monthNames = ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'];
@@ -15,6 +15,17 @@ const extractSelectorParts = (selector) => {
15
15
  return { baseSelector: selector.slice(0, -1), index: last };
16
16
  return { baseSelector: selector, index: 0 };
17
17
  };
18
+ // Row index of an element repeated per row, undefined when it is not in a row. AG Grid rows carry it in `row-index`.
19
+ // A list form component in paging mode (ListFormComponent.pagingMode) draws its rows as plain `.svy-listformcomponent-row`
20
+ // siblings without that attribute, so there the position among them is the index. AG Grid rows of a list form component
21
+ // have that class too, inside the `row-index` cell, which is why `row-index` is checked first.
22
+ const getRowIndex = (el) => {
23
+ const rowIndexEl = el.closest('[row-index]');
24
+ if (rowIndexEl)
25
+ return Number(rowIndexEl.getAttribute('row-index'));
26
+ const pagingRow = el.closest('.svy-listformcomponent-row');
27
+ return pagingRow?.parentElement ? Array.from(pagingRow.parentElement.querySelectorAll(':scope > .svy-listformcomponent-row')).indexOf(pagingRow) : undefined;
28
+ };
18
29
  // When index refers to a grid row (element repeated per row), GLO_getElement handles row filtering via the full selector.
19
30
  // When index refers to a sub-input within a single element (e.g. a field with two inputs), the caller uses eq(index).
20
31
  // This helper distinguishes the two cases synchronously.
@@ -23,7 +34,7 @@ const isGridRowSelector = (baseSelector, index) => {
23
34
  return false;
24
35
  const selectorStr = baseSelector.length === 2 ? `[data-cy$="${baseSelector[0]}.${baseSelector[1]}"]` : `[name="${baseSelector[0]}"]`;
25
36
  const el = Cypress.$(`div[formslist]:not([hidden]) ${selectorStr}`)[0];
26
- return !!el?.closest('[row-index]');
37
+ return !!el && getRowIndex(el) !== undefined;
27
38
  };
28
39
  // Row queries must ignore grids that are not on screen, but neither obvious filter does that job.
29
40
  // Servoy marks the *containers* hidden - `div[formslist][hidden]` for background programs,
@@ -40,8 +51,8 @@ const pickByRowIndex = (chain, index, log = false, timeout = defaultTimeout) =>
40
51
  if (log)
41
52
  Cypress.log({ name: 'idx', message: String(index) });
42
53
  return chain.filter((i, el) => {
43
- const rowIndexEl = el.closest('[row-index]');
44
- return rowIndexEl ? rowIndexEl.getAttribute('row-index') === String(index) : i === index;
54
+ const rowIndex = getRowIndex(el);
55
+ return rowIndex !== undefined ? rowIndex === index : i === index;
45
56
  }, { timeout });
46
57
  };
47
58
  Cypress.Commands.add('GLO_getElement', (selectorArg, options = {}) => {
@@ -683,8 +694,8 @@ const GLO_inputValuelist = (selector, rawValue) => {
683
694
  cy.get('@before').then(($before) => {
684
695
  getDropdown('.dx-scrollview-content .dx-item', 30000)
685
696
  .should(($current) => {
686
- const initialTexts = [...$before].map((el) => el.innerText.trim());
687
- const currentTexts = [...$current].map((el) => el.innerText.trim());
697
+ const initialTexts = [...$before].map((el) => (el.textContent ?? '').trim());
698
+ const currentTexts = [...$current].map((el) => (el.textContent ?? '').trim());
688
699
  const hasChanged = !Cypress._.isEqual(currentTexts, initialTexts);
689
700
  // Also accept when the list is the same size or larger: branch-filtered valuelists
690
701
  // with few options won't change after clearing because the full list IS the filtered list.
@@ -722,8 +733,8 @@ const GLO_inputValuelist = (selector, rawValue) => {
722
733
  cy.get('@before')
723
734
  .then(($before) => {
724
735
  getDropdown('.dx-scrollview-content .dx-item', 30000).should(($current) => {
725
- const initialTexts = [...$before].map((el) => el.innerText.trim());
726
- const currentTexts = [...$current].map((el) => el.innerText.trim());
736
+ const initialTexts = [...$before].map((el) => (el.textContent ?? '').trim());
737
+ const currentTexts = [...$current].map((el) => (el.textContent ?? '').trim());
727
738
  const hasChanged = !Cypress._.isEqual(currentTexts, initialTexts);
728
739
  const anyIncludes = currentTexts.every((text) => text.includes(dropdownValue));
729
740
  // An empty list is the transitional state while the filtered valuelist is still loading, never a valid result.
@@ -1221,7 +1232,7 @@ Cypress.Commands.add('GLO_selectRowInGrid', (rowFilter) => {
1221
1232
  cy.get('[row-index]')
1222
1233
  .filter(isInOpenContainer)
1223
1234
  .each(($row) => {
1224
- const cellTexts = Array.from($row.find('.ag-cell-value')).map((el) => el.innerText.trim());
1235
+ const cellTexts = Array.from($row.find('.ag-cell-value')).map((el) => (el.textContent ?? '').trim());
1225
1236
  // Check to see if all requested values are present in any of the cells.
1226
1237
  const matches = filters.every((filterValue) => cellTexts.some((val) => val.includes(filterValue)));
1227
1238
  if (matches) {
@@ -1442,7 +1453,8 @@ Cypress.Commands.add('GLO_popupWindow', (selector, value, columnId, equals = noE
1442
1453
  selectFromPopupWindow(value, columnId, equals);
1443
1454
  }
1444
1455
  else {
1445
- cy.GLO_checkField(selector, value, equals ? contains : undefined);
1456
+ // Type search with an exact match must stay exact: a contains check would accept LOC1 for LOC10.
1457
+ cy.GLO_checkField(selector, value, equals ? (useMagnifierFlag ? contains : equal) : undefined);
1446
1458
  }
1447
1459
  });
1448
1460
  });
@@ -1945,6 +1957,9 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
1945
1957
  cy.GLO_getElement(selector).within(() => {
1946
1958
  cy.GLO_clickRowInGrid(rowFilter, columnId);
1947
1959
  });
1960
+ // Re-check outside the .within(): loading-indicator renders at page level, so the check
1961
+ // inside GLO_clickRowInGrid (scoped to the grid element above) can't see it and resolves early.
1962
+ cy.GLO_checkLoadingIndicator();
1948
1963
  // biome-ignore lint/plugin/noUnnecessaryWaiting: else both click and dblclick fire too fast
1949
1964
  cy.wait(500);
1950
1965
  cy.GLO_dblClickRowInGrid(row, selector, rowFilter, columnId, skipCheck);
@@ -2113,6 +2128,120 @@ Cypress.Commands.add('GLO_getAllRowsInGrid', (selector, timeOut = defaultTimeout
2113
2128
  Cypress.Commands.add('GLO_validateGridOrFail', (waitValue, failValue, rowNumber, columnId, timeOut = 600000, interval = 15000) => {
2114
2129
  validateGridValue(waitValue, failValue, rowNumber, columnId, timeOut, interval);
2115
2130
  });
2131
+ Cypress.Commands.add('GLO_validateFieldOrFail', (selector, waitValue, failValue, timeOut = 600000, interval = 15000) => {
2132
+ const failValues = Array.isArray(failValue) ? failValue : [failValue];
2133
+ const maxAttempts = Math.max(1, Math.ceil(timeOut / interval));
2134
+ const fieldName = selector.join('.');
2135
+ // A trailing index picks a grid row or, on a field with several inputs, the input: same as GLO_checkField
2136
+ const { baseSelector, index } = extractSelectorParts(selector);
2137
+ const checkField = (attempt = 1) => {
2138
+ // Decided on every attempt and only once the field is in the DOM: a grid that is still rendering would
2139
+ // otherwise be taken for a field with several inputs, and that choice would stick for the whole poll.
2140
+ // A grid row is then looked up with the full selector, so GLO_getElement waits for that row itself.
2141
+ cy.GLO_getElement(baseSelector).then(() => {
2142
+ const inGrid = isGridRowSelector(baseSelector, index);
2143
+ const effectiveIndex = inGrid ? 0 : index;
2144
+ // Same value as GLO_checkField reads: the displayed text, with html and other unicode spaces normalized
2145
+ cy.GLO_getElement(inGrid ? selector : baseSelector).then(($field) => {
2146
+ const fieldValue = String($field.find('.dx-texteditor-input').eq(effectiveIndex).val()).replace(/\s+/g, ' ').trim();
2147
+ if (fieldValue === waitValue) {
2148
+ cy.log(`${fieldName} is "${waitValue}"`);
2149
+ return;
2150
+ }
2151
+ if (failValues.includes(fieldValue)) {
2152
+ throw new Error(`${fieldName} is "${fieldValue}" (waited for "${waitValue}")`);
2153
+ }
2154
+ if (attempt >= maxAttempts) {
2155
+ throw new Error(`${fieldName} is still "${fieldValue}" after ${timeOut / 1000}s (waited for "${waitValue}")`);
2156
+ }
2157
+ // biome-ignore lint/plugin/noUnnecessaryWaiting: wait for next poll interval before checking again
2158
+ cy.wait(interval, { log: false }).then(() => {
2159
+ checkField(attempt + 1);
2160
+ });
2161
+ });
2162
+ });
2163
+ };
2164
+ checkField();
2165
+ });
2166
+ Cypress.Commands.add('GLO_executeBatchProcess', (batchType, columnId = 'description', waitValue = 'Completed', failValue = 'Error', timeOut = 1200000, interval = 15000) => {
2167
+ const formNameTypeList = 'ng$batch_execution_type_lst';
2168
+ const formNameTypeTab = 'ng$batch_execution_type_tab';
2169
+ const formNameRunList = 'ng$batch_execution_lst';
2170
+ const maxAttempts = Math.max(1, Math.ceil(timeOut / interval));
2171
+ // Row 0 of the run list is the most recent run (sorted on process_submitted desc); an empty list has no run yet.
2172
+ // Without these columns (hidden in a custom view) a new run can never be recognised, so that fails at once.
2173
+ const requiredColumns = ['process_submitted', 'process_completed', 'process_status'];
2174
+ const readLastRun = () => {
2175
+ const noRun = { submitted: '', completed: '', status: '', errorMessage: '' };
2176
+ return cy.GLO_getElement([formNameRunList, 'grid']).then(($grid) => {
2177
+ if ($grid.find(gridDataRows).length === 0) {
2178
+ return cy.wrap(noRun, { log: false });
2179
+ }
2180
+ return cy
2181
+ .wrap($grid, { log: false })
2182
+ .within(() => {
2183
+ cy.GLO_getRowInGrid(0).then(($row) => {
2184
+ const missingColumns = requiredColumns.filter((requiredColumnId) => $row.find(`[col-id="${requiredColumnId}"]`).length === 0);
2185
+ if (missingColumns.length) {
2186
+ throw new Error(`The 'Batch processing' tab of ${batchType} does not show the column(s) ${missingColumns.join(', ')}`);
2187
+ }
2188
+ const cellText = (cellColumnId) => $row.find(`[col-id="${cellColumnId}"]`).text().trim();
2189
+ cy.wrap({
2190
+ submitted: cellText('process_submitted'),
2191
+ completed: cellText('process_completed'),
2192
+ status: cellText('process_status'),
2193
+ errorMessage: cellText('last_error_message')
2194
+ }, { log: false }).as('COM_batchRun');
2195
+ });
2196
+ })
2197
+ .then(() => cy.get('@COM_batchRun', { log: false }));
2198
+ });
2199
+ };
2200
+ // The completion time (process_completed) is stamped for every final status, Completed as well as Error
2201
+ const waitForNewRun = (previousSubmitted, attempt = 1) => {
2202
+ cy.GLO_checkAGLoader();
2203
+ return readLastRun().then((run) => {
2204
+ const isNewRun = run.submitted !== '' && run.submitted !== previousSubmitted;
2205
+ const hasFailed = waitValue !== null && run.status === failValue;
2206
+ if (isNewRun && (run.completed !== '' || hasFailed)) {
2207
+ cy.GLO_log(`Batch run of ${run.submitted} (${batchType}) finished with status "${run.status}" ${run.errorMessage}`);
2208
+ if (hasFailed) {
2209
+ throw new Error(`Batch run of ${run.submitted} (${batchType}) ended with status "${run.status}": ${run.errorMessage}`);
2210
+ }
2211
+ if (waitValue !== null) {
2212
+ expect(run.status, `status of the batch run of ${run.submitted} (${batchType})`).to.equal(waitValue);
2213
+ }
2214
+ return cy.wrap(run, { log: false });
2215
+ }
2216
+ if (attempt >= maxAttempts) {
2217
+ throw new Error(`No finished run of ${batchType} after ${timeOut / 1000}s (last run submitted: ${run.submitted})`);
2218
+ }
2219
+ // biome-ignore lint/plugin/noUnnecessaryWaiting: wait for next poll interval before checking again
2220
+ return cy.wait(interval, { log: false }).then(() => waitForNewRun(previousSubmitted, attempt + 1));
2221
+ });
2222
+ };
2223
+ cy.GLO_searchHeader('Batch processes', 'Batch processes', 'Batch processing type');
2224
+ cy.GLO_clickDatacontrolBrowserButton('btnAll');
2225
+ // A filter typed while the list is still loading is lost
2226
+ cy.GLO_checkAGLoader();
2227
+ // Equals, not contains: 'CALCULATE INVOICE' must not select 'CALCULATE INVOICE RETOUR'
2228
+ cy.GLO_filterGrid(columnId, [formNameTypeList, 'grid'], batchType, undefined, 'Equals');
2229
+ cy.GLO_getElement([formNameTypeList, 'grid']).within(() => {
2230
+ cy.GLO_checkValueGrid(batchType, 0, columnId, undefined, undefined, equal);
2231
+ });
2232
+ cy.GLO_selectTab([formNameTypeTab, 'tabs'], 'Batch processing', open);
2233
+ cy.GLO_checkAGLoader();
2234
+ return readLastRun().then((previousRun) => {
2235
+ cy.GLO_selectMenuButton('Execute');
2236
+ cy.GLO_log(`Executed batch processing type ${batchType}`);
2237
+ return waitForNewRun(previousRun.submitted).then((run) => {
2238
+ // Every call opens the program, so it is closed again: a second instance would keep a hidden run list in
2239
+ // the page, and the caller gets back the programs it had open
2240
+ cy.GLO_closeProgram(top, 'Batch processing type');
2241
+ return cy.wrap(run, { log: false });
2242
+ });
2243
+ });
2244
+ });
2116
2245
  const validateGridValue = (value, failValue, rowFilter, columnId, timeOut = 10000, interval, operator = contains) => {
2117
2246
  const assertOrCheckCellValue = ($cells, toCheck, doAssert) => {
2118
2247
  const compare = (cellText) => {
@@ -2211,21 +2340,22 @@ const validateGridValue = (value, failValue, rowFilter, columnId, timeOut = 1000
2211
2340
  cy.then(() => promise);
2212
2341
  }
2213
2342
  };
2214
- Cypress.Commands.add('GLO_getCurrentRecordCount', () => {
2215
- return cy
2216
- .GLO_getElement(['ng$datacontrol', 'datacontrol'])
2217
- .find('#txtCount')
2218
- .invoke('text')
2219
- .then((records) => {
2220
- const [currentStr, totalStr] = records.split('/');
2221
- if (!currentStr || !totalStr) {
2222
- throw new Error(`Could not parse record count from "${records}"`);
2223
- }
2224
- return {
2225
- currentRecord: parseInt(currentStr.trim(), 10),
2226
- totalRecords: parseInt(totalStr.trim(), 10)
2227
- };
2228
- });
2343
+ Cypress.Commands.addQuery('GLO_getCurrentRecordCount', function (timeOut) {
2344
+ // this.set('timeout', ...) is how the Cypress docs give a custom query its timeout, but the typings leave
2345
+ // 'timeout' out of EnqueuedCommandAttributes
2346
+ if (timeOut !== undefined)
2347
+ this.set('timeout', timeOut);
2348
+ return () => {
2349
+ const $count = Cypress.$('div[formslist]:not([hidden]) [data-cy$="ng$datacontrol.datacontrol"] #txtCount');
2350
+ const text = $count.first().text();
2351
+ const [currentRecord, totalRecords] = text.split('/').map((part) => Number.parseInt(part.trim(), 10));
2352
+ // Throwing makes the query retry: while the datacontrol is still rendering there is no count (or no
2353
+ // '<current>/<total>' text yet), and returning NaN would let the caller go on with an invalid count
2354
+ if ($count.length === 0 || !Number.isInteger(currentRecord) || !Number.isInteger(totalRecords)) {
2355
+ throw new Error(`No record count on the active datacontrol yet (#txtCount: "${text}")`);
2356
+ }
2357
+ return { currentRecord, totalRecords };
2358
+ };
2229
2359
  });
2230
2360
  Cypress.Commands.add('GLO_storeObject', (formName, elementNames, index, log = hideInCommandLog) => {
2231
2361
  const keys = (Array.isArray(elementNames) ? elementNames : [elementNames]);
@@ -14,28 +14,43 @@ 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}`);
17
+ // Newer GlobScan versions show the menu as a list form component (VCK-376, 11.0.x), older ones as an AG Grid.
18
+ // Waits for whichever menu is there and uses that one.
19
+ const listMenuItems = '.svy-listformcomponent label.clickable';
20
+ cy.get(`${listMenuItems}, .ag-grid-scrolling-rows`).then(($menu) => {
21
+ if ($menu.is(listMenuItems)) {
22
+ // Exact match, so 'Wegzetten' does not open 'Wegzetten drager'. A menu level that is still mounted but hidden is skipped
23
+ // through its [hidden] container instead of :visible, which also drops items scrolled out of the list (see isInOpenContainer)
24
+ const exactText = new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`);
25
+ cy.get(listMenuItems)
26
+ .filter((_i, el) => !el.closest('[hidden]') && exactText.test(el.textContent ?? ''))
27
+ .click();
28
+ cy.GLO_checkLoadingIndicator();
29
+ return;
20
30
  }
21
- return cy
22
- .get('.ag-grid-scrolling-rows')
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-grid-scrolling-rows label', value).click();
31
+ const scrollAndSearch = (attempts = 0) => {
32
+ if (attempts > 20) {
33
+ throw new Error(`Could not find menu item: ${value}`);
28
34
  }
29
35
  return cy
30
- .get('.ag-grid-scrolling-container [role="row"][tabindex="0"]')
36
+ .get('.ag-grid-scrolling-rows')
31
37
  .first()
32
- .focus()
33
- .realPress('ArrowDown')
34
- .then(() => scrollAndSearch(attempts + 1));
38
+ .then(($viewport) => {
39
+ const $label = $viewport.find('label').filter((_i, el) => (el.textContent ?? '').trim() === value);
40
+ if ($label.length > 0 && $label.is(':visible')) {
41
+ return cy.contains('.ag-grid-scrolling-rows label', value).click();
42
+ }
43
+ return cy
44
+ .get('.ag-grid-scrolling-container [role="row"][tabindex="0"]')
45
+ .first()
46
+ .focus()
47
+ .realPress('ArrowDown')
48
+ .then(() => scrollAndSearch(attempts + 1));
49
+ });
50
+ };
51
+ scrollAndSearch().then(() => {
52
+ cy.GLO_checkLoadingIndicator();
35
53
  });
36
- };
37
- scrollAndSearch().then(() => {
38
- cy.GLO_checkLoadingIndicator();
39
54
  });
40
55
  });
41
56
  Cypress.Commands.add('GLS_selectStock', (formName, elementName, index) => {
@@ -5,7 +5,7 @@ Cypress.Commands.add('POR_checkPortalSideMenuCorrect', (menuItems) => {
5
5
  .find('.main-items .item-main , .main-items .item-header')
6
6
  .should('have.length.gt', 1)
7
7
  .then((elements) => {
8
- return Cypress.$.makeArray(elements).map((el) => el.innerText);
8
+ return Cypress.$.makeArray(elements).map((el) => (el.textContent ?? '').trim());
9
9
  })
10
10
  .should('deep.eq', menuItems);
11
11
  cy.GLO_checkLoadingIndicator();
package/dist/types.d.ts CHANGED
@@ -13,6 +13,12 @@ type viewModeButtonType = 'btnList' | 'btnDetail';
13
13
  type datacontrolbuttonType = baseDatacontrolViewerButtonType | viewModeButtonType | 'btnPrev' | 'btnNext' | 'btnAll' | 'btnPivot' | 'btnAttachment' | 'btnAddMode' | 'btnEditMode' | 'btnDeleteMode' | 'btnDeeplink' | 'btnExtendedSearch' | 'btnCustomViews' | 'btnFilter' | 'btnClearAllFilters';
14
14
  type datacontrolCounterButtonType = 'btnAttachment' | 'btnNotification';
15
15
  type checkValueInGridType = string | string[] | number;
16
+ type batchRunType = {
17
+ submitted: string;
18
+ completed: string;
19
+ status: string;
20
+ errorMessage: string;
21
+ };
16
22
  type selectMenuButtonLeftType = 'home' | 'settings' | 'help' | 'support' | 'excel' | 'info' | 'refresh' | 'query' | 'clients' | 'users' | 'theme';
17
23
  type urlTypeType = 'online' | 'pos' | 'globgate' | 'globjob' | 'globscan' | 'portal' | 'carrier' | 'globtrace' | 'external_portal' | 'globtake' | 'globis_external_portal';
18
24
  type customValueType = JQuery | string | number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globisnv/cypress-core",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Core building blocks for Globis Cypress E2E tests",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -59,7 +59,7 @@
59
59
  "devDependencies": {
60
60
  "@biomejs/biome": "^2.5.15",
61
61
  "@types/node": "^26.1.2",
62
- "cypress": "^16.1.0",
62
+ "cypress": "^16.2.1",
63
63
  "cypress-real-events": "^1.15.1",
64
64
  "typescript": "^7.0.2"
65
65
  },