@globisnv/cypress-core 1.1.0 → 2.0.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/README.md
CHANGED
|
@@ -34,6 +34,7 @@ See [Cutting a release](#cutting-a-release) for how a bump actually gets publish
|
|
|
34
34
|
|
|
35
35
|
| Core version | Minimum globis-online version |
|
|
36
36
|
| --- | --- |
|
|
37
|
+
| `2.0.0` | `11.0.3` |
|
|
37
38
|
| `0.0.1` | `11.0.2` |
|
|
38
39
|
|
|
39
40
|
The floor is recorded in `package.json` under `globisOnline.minVersion` (machine-readable, in case tooling wants to check it later) and here in this table (human-readable, updated on every release alongside `CHANGELOG.md`). `globis-cypress` stays pinned to the latest core version — since it's Globis's own suite, it's the first thing to catch a globis-online release breaking something, before any customer repo would.
|
|
@@ -183,6 +183,8 @@ declare global {
|
|
|
183
183
|
/**
|
|
184
184
|
* Command to select a tab using its data-cy attribute, which is composed of formName.elementName
|
|
185
185
|
*
|
|
186
|
+
* Fails if the tab has to be opened or closed but is disabled.
|
|
187
|
+
*
|
|
186
188
|
* @param {string[]} selector - One element: `[name=]` lookup. Two elements: `[data-cy$="form.element"]` lookup.
|
|
187
189
|
* @param {string} tabName - The name of the tab to be selected.
|
|
188
190
|
* @param {OpenClose} openOrClose - Indicates whether to open (true) or close (false) the tab.
|
|
@@ -647,6 +649,14 @@ declare global {
|
|
|
647
649
|
* @param {disabledFlag} [shouldBeEnabled=true] - Whether the field should be enabled (true) or disabled (false).
|
|
648
650
|
*/
|
|
649
651
|
GLO_checkFieldEnabled(selector: string[], shouldBeEnabled?: disabledFlag): void;
|
|
652
|
+
/**
|
|
653
|
+
* Checks if a tab is enabled or disabled, without clicking it.
|
|
654
|
+
*
|
|
655
|
+
* @param {string[]} selector - One element: `[name=]` lookup. Two elements: `[data-cy$="form.element"]` lookup.
|
|
656
|
+
* @param {string} tabName - The name of the tab to check.
|
|
657
|
+
* @param {disabledFlag} [shouldBeEnabled=true] - Whether the tab should be enabled (true) or disabled (false).
|
|
658
|
+
*/
|
|
659
|
+
GLO_checkTabEnabled(selector: string[], tabName: string, shouldBeEnabled?: disabledFlag): void;
|
|
650
660
|
/**
|
|
651
661
|
* Checks if a field has the given background color class applied to its input element.
|
|
652
662
|
*
|
|
@@ -33,6 +33,9 @@ const isGridRowSelector = (baseSelector, index) => {
|
|
|
33
33
|
// rendered-but-scrolled-past rows as hidden (measured: 14 of 21 rendered rows drop out of `:visible`
|
|
34
34
|
// after scrolling a grid to the bottom). Filter on the container, which is what both were after.
|
|
35
35
|
const isInOpenContainer = (_, el) => !el.closest('[hidden]');
|
|
36
|
+
// Data rows of an AG Grid 36 body. Group rows (full-width) share the scrolling container with the data rows,
|
|
37
|
+
// so they are excluded to keep row indexes and counts about data rows only.
|
|
38
|
+
const gridDataRows = '.ag-grid-scrolling-container [row-index]:not(.ag-full-width-row)';
|
|
36
39
|
const pickByRowIndex = (chain, index, log = false, timeout = defaultTimeout) => {
|
|
37
40
|
if (log)
|
|
38
41
|
Cypress.log({ name: 'idx', message: String(index) });
|
|
@@ -713,6 +716,12 @@ Cypress.Commands.add('GLO_selectTab', (selector, tabName, openOrClose) => {
|
|
|
713
716
|
const isOpenAttr = $tab[0].getAttribute('open'); // Attribute is a string 'true' or missing -> null
|
|
714
717
|
const isOpen = isOpenAttr === 'true'; // Convert to boolean
|
|
715
718
|
if (openOrClose !== isOpen) {
|
|
719
|
+
// The tab header is a div, so Cypress's `:disabled` actionability check doesn't apply: the click goes through
|
|
720
|
+
// and the server silently ignores it. Closing a disabled tab that is open fails too, because the close request
|
|
721
|
+
// goes through the same click on the same disabled tab, which the server ignores the same way.
|
|
722
|
+
if ($tab[0].hasAttribute('disabled')) {
|
|
723
|
+
throw new Error(`Tab "${tabName}" in ${selector.join('.')} is disabled and cannot be ${openOrClose ? 'opened' : 'closed'}`);
|
|
724
|
+
}
|
|
716
725
|
const action = openOrClose ? 'Opening' : 'Closing';
|
|
717
726
|
cy.log(`${action} tab ${tabName}`);
|
|
718
727
|
cy.wrap($tab).click();
|
|
@@ -750,7 +759,7 @@ const navItem = (topBottom, programName, tabNumber, mode) => {
|
|
|
750
759
|
throw new Error(`Incorrect type for GLO_${mode}Program. Available types: top, bottom`);
|
|
751
760
|
const location = topBottom === top ? 0 : 1;
|
|
752
761
|
const regex = new RegExp(`^${Cypress._.escapeRegExp(programName)}$`);
|
|
753
|
-
cy.get('
|
|
762
|
+
cy.get('[navigation] > [nodelist]')
|
|
754
763
|
.eq(location)
|
|
755
764
|
.within(() => {
|
|
756
765
|
const items = cy
|
|
@@ -1066,13 +1075,13 @@ Cypress.Commands.add('GLO_selectProgram', (topBottom, programName, tabnumber) =>
|
|
|
1066
1075
|
navItem(topBottom, programName, tabnumber, 'select');
|
|
1067
1076
|
});
|
|
1068
1077
|
Cypress.Commands.add('GLO_getRowInGrid', (rowFilter) => {
|
|
1069
|
-
const rows = cy.get(
|
|
1078
|
+
const rows = cy.get(gridDataRows).filter(isInOpenContainer);
|
|
1070
1079
|
return typeof rowFilter === 'number' ? rows.eq(rowFilter) : rows.filter(`:contains("${rowFilter}")`).first();
|
|
1071
1080
|
});
|
|
1072
1081
|
Cypress.Commands.add('GLO_selectRowInGrid', (rowFilter) => {
|
|
1073
1082
|
let found = false;
|
|
1074
1083
|
if (typeof rowFilter === 'number') {
|
|
1075
|
-
cy.get(`[row-id="root_${rowFilter}"]`).filter(isInOpenContainer).find('[data-ref="eInput"]').click();
|
|
1084
|
+
cy.get(`[row-id="root_${rowFilter}"]`).filter(isInOpenContainer).find('.ag-grid-pinned-left-cells [data-ref="eInput"]').click();
|
|
1076
1085
|
return;
|
|
1077
1086
|
}
|
|
1078
1087
|
// Convert to array if it's a string
|
|
@@ -1086,15 +1095,8 @@ Cypress.Commands.add('GLO_selectRowInGrid', (rowFilter) => {
|
|
|
1086
1095
|
const matches = filters.every((filterValue) => cellTexts.some((val) => val.includes(filterValue)));
|
|
1087
1096
|
if (matches) {
|
|
1088
1097
|
found = true;
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
// filter above, so everything inside shares its ancestors. The re-query is only needed
|
|
1092
|
-
// because the checkbox lives in the pinned-left twin carrying the same row-id.
|
|
1093
|
-
cy.wrap($row)
|
|
1094
|
-
.closest('.ag-root')
|
|
1095
|
-
.within(() => {
|
|
1096
|
-
cy.get(`[row-id="${rowId}"]`).find('[data-ref="eInput"]').click();
|
|
1097
|
-
});
|
|
1098
|
+
// AG Grid 36 renders the pinned-left selection checkbox inside the row itself.
|
|
1099
|
+
cy.wrap($row).find('.ag-grid-pinned-left-cells [data-ref="eInput"]').click();
|
|
1098
1100
|
return false; // Stop after the first match
|
|
1099
1101
|
}
|
|
1100
1102
|
})
|
|
@@ -1141,16 +1143,16 @@ const dblClickRowInGrid = (type, value, columnId, equals) => {
|
|
|
1141
1143
|
};
|
|
1142
1144
|
const helper = (filterSelector) => {
|
|
1143
1145
|
if (columnId) {
|
|
1144
|
-
cy.get('[row-index]')
|
|
1146
|
+
cy.get('[row-index]:visible')
|
|
1145
1147
|
.find(`[col-id="${columnId}"]`)
|
|
1146
1148
|
.filter(filterSelector, { timeout: 10000 })
|
|
1147
|
-
.parentsUntil('.ag-
|
|
1149
|
+
.parentsUntil('.ag-grid-scrolling-container')
|
|
1148
1150
|
.filter('[row-index]')
|
|
1149
1151
|
.first()
|
|
1150
1152
|
.then(saveValues);
|
|
1151
1153
|
}
|
|
1152
1154
|
else {
|
|
1153
|
-
cy.get('[row-index]').filter(filterSelector, { timeout: 10000 }).first().then(saveValues);
|
|
1155
|
+
cy.get('[row-index]:visible').filter(filterSelector, { timeout: 10000 }).first().then(saveValues);
|
|
1154
1156
|
}
|
|
1155
1157
|
};
|
|
1156
1158
|
cy.GLO_checkAGLoader();
|
|
@@ -1167,7 +1169,7 @@ const dblClickRowInGrid = (type, value, columnId, equals) => {
|
|
|
1167
1169
|
cy.get('[row-index]:visible')
|
|
1168
1170
|
.find(columnId ? `[col-id="${columnId}"] ${span}` : span)
|
|
1169
1171
|
.contains(regex, { timeout: 10000 })
|
|
1170
|
-
.parentsUntil('.ag-
|
|
1172
|
+
.parentsUntil('.ag-grid-scrolling-container')
|
|
1171
1173
|
.filter('[row-index]')
|
|
1172
1174
|
.first()
|
|
1173
1175
|
.then(saveValues);
|
|
@@ -1192,13 +1194,13 @@ const dblClickRowInGrid = (type, value, columnId, equals) => {
|
|
|
1192
1194
|
}
|
|
1193
1195
|
// The actual dblClick on the row
|
|
1194
1196
|
cy.get('@COM_selectedRowSelector').then((selector) => {
|
|
1195
|
-
columnId ? cy.get(selector).find(`[col-id="${columnId}"]`).dblclick() : cy.get(selector).find('[aria-colindex]').eq(1).dblclick();
|
|
1197
|
+
columnId ? cy.get(selector).find(`[col-id="${columnId}"]`).dblclick() : cy.get(selector).find('.ag-grid-scrolling-cells [aria-colindex]').eq(1).dblclick();
|
|
1196
1198
|
});
|
|
1197
1199
|
};
|
|
1198
1200
|
Cypress.Commands.add('GLO_clickRowInGrid', (value, columnId) => {
|
|
1199
1201
|
if (value === undefined)
|
|
1200
1202
|
throw new Error('Value missing for GLO_clickRowInGrid');
|
|
1201
|
-
columnId ? cy.GLO_getRowInGrid(value).find(`[col-id="${columnId}"]`).click() : cy.GLO_getRowInGrid(value).children().first().click();
|
|
1203
|
+
columnId ? cy.GLO_getRowInGrid(value).find(`[col-id="${columnId}"]`).click() : cy.GLO_getRowInGrid(value).find('.ag-grid-scrolling-cells').children().first().click();
|
|
1202
1204
|
cy.GLO_checkLoadingIndicator();
|
|
1203
1205
|
});
|
|
1204
1206
|
// Shared popup-window search + grid selection logic used by GLO_popupWindow.
|
|
@@ -1211,7 +1213,9 @@ const selectFromPopupWindow = (value, columnId, equals = noEqualBool) => {
|
|
|
1211
1213
|
const loadingColumnId = () => columnId
|
|
1212
1214
|
? cy.wrap(columnId)
|
|
1213
1215
|
: cy
|
|
1214
|
-
.get('.svy-dialog.active .window-body .ag-
|
|
1216
|
+
.get('.svy-dialog.active .window-body .ag-grid-scrolling-container [row-index="0"]:not(.ag-full-width-row) .ag-grid-scrolling-cells [role="gridcell"]', {
|
|
1217
|
+
timeout: 30000
|
|
1218
|
+
})
|
|
1215
1219
|
.filter((_, el) => Cypress.$(el).text().trim().length > 0)
|
|
1216
1220
|
.first()
|
|
1217
1221
|
.invoke('attr', 'col-id');
|
|
@@ -1475,13 +1479,15 @@ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate,
|
|
|
1475
1479
|
const pollInterval = 5000;
|
|
1476
1480
|
const HEADER_ROWS = 2;
|
|
1477
1481
|
const getAriaRowcount = ($grid) => parseInt($grid.find('[role="grid"]').attr('aria-rowcount') || '0', 10);
|
|
1478
|
-
// Column header row + filter row
|
|
1479
|
-
|
|
1480
|
-
|
|
1482
|
+
// Column header row + filter row (AG Grid 36 renders each header row once, pinned cells included).
|
|
1483
|
+
const headerRowCount = ($grid) => $grid.find('.ag-header .ag-header-row').length || HEADER_ROWS;
|
|
1484
|
+
// Rows aria-rowcount counts that are not data rows: header rows plus pinned top/bottom rows (e.g. a totals
|
|
1485
|
+
// row), which AG Grid 36 includes in aria-rowcount. Pinned rows are never virtualised, so the DOM count is exact.
|
|
1486
|
+
const nonDataRowCount = ($grid) => headerRowCount($grid) + $grid.find('.ag-row-pinned').length;
|
|
1481
1487
|
const isLazyLoaded = ($grid) => {
|
|
1482
1488
|
const ariaRowcount = getAriaRowcount($grid);
|
|
1483
|
-
const renderedRows = $grid.find(
|
|
1484
|
-
return renderedRows > 0 && (ariaRowcount === -1 || ariaRowcount > renderedRows +
|
|
1489
|
+
const renderedRows = $grid.find(gridDataRows).length;
|
|
1490
|
+
return renderedRows > 0 && (ariaRowcount === -1 || ariaRowcount > renderedRows + nonDataRowCount($grid));
|
|
1485
1491
|
};
|
|
1486
1492
|
// When a client-side column filter is active, the tab-/datacontrol-counter still shows the
|
|
1487
1493
|
// unfiltered foundset total, so we must count the rendered (filtered) rows instead.
|
|
@@ -1503,12 +1509,12 @@ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate,
|
|
|
1503
1509
|
const total = parseInt(parts[1].trim(), 10);
|
|
1504
1510
|
return Number.isNaN(total) ? null : total;
|
|
1505
1511
|
};
|
|
1506
|
-
// AG Grid publishes the displayed row total (header rows included
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1512
|
+
// AG Grid publishes the displayed row total (header and pinned rows included) in aria-rowcount,
|
|
1513
|
+
// so a virtualised grid still reports rows the viewport never rendered. Verified on
|
|
1514
|
+
// ng$consignment_dtl_general.grid: 25 goods -> aria-rowcount 27 with 2 header rows, while only
|
|
1515
|
+
// 12 rows exist in the DOM.
|
|
1510
1516
|
const getCountFromAriaRowcount = ($grid) => {
|
|
1511
|
-
const total = getAriaRowcount($grid) -
|
|
1517
|
+
const total = getAriaRowcount($grid) - nonDataRowCount($grid);
|
|
1512
1518
|
return total >= 0 ? total : null; // An aria-rowcount of -1 means the total is unknown (infinite row model)
|
|
1513
1519
|
};
|
|
1514
1520
|
// Form-level counters (#txtCount navigator, tab "(n)" label) only match a grid on list
|
|
@@ -1552,7 +1558,7 @@ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate,
|
|
|
1552
1558
|
runChainAssertion(tabCount);
|
|
1553
1559
|
}
|
|
1554
1560
|
else {
|
|
1555
|
-
runChainAssertion($grid.find(
|
|
1561
|
+
runChainAssertion($grid.find(gridDataRows));
|
|
1556
1562
|
}
|
|
1557
1563
|
});
|
|
1558
1564
|
}
|
|
@@ -1563,7 +1569,7 @@ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate,
|
|
|
1563
1569
|
const log = Cypress.log({ name: 'checkNumberOfRowsInGrid', message: `Polling rows (${lengthCondition} ${numberToValidate})…`, autoEnd: false, timeout: 0 });
|
|
1564
1570
|
const checkStatus = (resolve, reject) => {
|
|
1565
1571
|
cy.GLO_getElement(selector, { timeout: timeOut, log: false }).then(($grid) => {
|
|
1566
|
-
const $rows = $grid.find(
|
|
1572
|
+
const $rows = $grid.find(gridDataRows);
|
|
1567
1573
|
let actual = $rows.length;
|
|
1568
1574
|
let passed = false;
|
|
1569
1575
|
try {
|
|
@@ -1762,7 +1768,7 @@ Cypress.Commands.add('GLO_selectRows', (selector, rowNumber) => {
|
|
|
1762
1768
|
}
|
|
1763
1769
|
};
|
|
1764
1770
|
cy.GLO_getElement(selector)
|
|
1765
|
-
.find('.ag-
|
|
1771
|
+
.find('.ag-grid-scrolling-rows')
|
|
1766
1772
|
.within(() => {
|
|
1767
1773
|
cy.GLO_clickRowInGrid(0); // Always click the first row
|
|
1768
1774
|
if (rowNumber !== undefined) {
|
|
@@ -1777,7 +1783,7 @@ Cypress.Commands.add('GLO_selectRows', (selector, rowNumber) => {
|
|
|
1777
1783
|
});
|
|
1778
1784
|
Cypress.Commands.add('GLO_sortGrid', (selector, headerText) => {
|
|
1779
1785
|
cy.GLO_getElement(selector)
|
|
1780
|
-
.find('.ag-header
|
|
1786
|
+
.find('.ag-header .ag-header-row.ag-header-row-column')
|
|
1781
1787
|
.within(() => {
|
|
1782
1788
|
cy.get('.ag-header-cell').contains(headerText).click();
|
|
1783
1789
|
});
|
|
@@ -1960,14 +1966,15 @@ Cypress.Commands.add('GLO_getAllRowsInGrid', (selector, timeOut = defaultTimeout
|
|
|
1960
1966
|
const deadline = Date.now() + timeOut;
|
|
1961
1967
|
return cy
|
|
1962
1968
|
.GLO_getElement(selector, { timeout: timeOut })
|
|
1963
|
-
.find(
|
|
1969
|
+
.find(gridDataRows, { timeout: timeOut + 500 })
|
|
1964
1970
|
.should(($rows) => {
|
|
1965
1971
|
if ($rows.length === 0 && Date.now() < deadline)
|
|
1966
1972
|
throw new Error('GLO_getAllRowsInGrid: waiting for rows');
|
|
1967
1973
|
});
|
|
1968
1974
|
}
|
|
1969
1975
|
else {
|
|
1970
|
-
|
|
1976
|
+
// AG Grid 36 nests cells in pinned/scrolling wrappers, so a cell's parent is no longer the row.
|
|
1977
|
+
return cy.get(`${gridDataRows}:has([role="gridcell"])`, { timeout: timeOut }).filter(isInOpenContainer);
|
|
1971
1978
|
}
|
|
1972
1979
|
});
|
|
1973
1980
|
Cypress.Commands.add('GLO_validateGridOrFail', (waitValue, failValue, rowNumber, columnId, timeOut = 600000, interval = 15000) => {
|
|
@@ -2143,6 +2150,11 @@ Cypress.Commands.add('GLO_checkFieldEnabled', (selector, shouldBeEnabled = enabl
|
|
|
2143
2150
|
}
|
|
2144
2151
|
});
|
|
2145
2152
|
});
|
|
2153
|
+
Cypress.Commands.add('GLO_checkTabEnabled', (selector, tabName, shouldBeEnabled = enabled) => {
|
|
2154
|
+
cy.GLO_getElement(selector)
|
|
2155
|
+
.contains('[tabcontainer] .tab', tabName)
|
|
2156
|
+
.should(shouldBeEnabled ? 'not.have.attr' : 'have.attr', 'disabled');
|
|
2157
|
+
});
|
|
2146
2158
|
Cypress.Commands.add('GLO_checkFieldBgColor', (selector, color = 'red') => {
|
|
2147
2159
|
const [formName, fieldOrFields] = selector;
|
|
2148
2160
|
const fields = Array.isArray(fieldOrFields) ? fieldOrFields : [fieldOrFields];
|
|
@@ -2290,7 +2302,12 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2290
2302
|
cy.wrap($field).find('button.fa-search').click();
|
|
2291
2303
|
cy.GLO_getElement(['svy_nav_selection', 'search_criteria']).type(String(v));
|
|
2292
2304
|
cy.GLO_clickButton(['svy_nav_selection', 'btn_search']);
|
|
2293
|
-
dblClickRowInGrid
|
|
2305
|
+
// dblClickRowInGrid is document-scoped unless wrapped in .within() - without this,
|
|
2306
|
+
// its unscoped [row-index] lookup can match a row in whatever grid sits behind this
|
|
2307
|
+
// dialog instead of the dialog's own grid. Same scoping GLO_popupWindow uses.
|
|
2308
|
+
cy.GLO_getVisible('.svy-dialog.active .window-body').within(() => {
|
|
2309
|
+
dblClickRowInGrid(span, String(v));
|
|
2310
|
+
});
|
|
2294
2311
|
cy.GLO_checkLoadingIndicator();
|
|
2295
2312
|
}
|
|
2296
2313
|
else {
|
|
@@ -2312,8 +2329,6 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2312
2329
|
// Fall through
|
|
2313
2330
|
case 'before':
|
|
2314
2331
|
case 'after':
|
|
2315
|
-
case '<= today +/- days':
|
|
2316
|
-
case '>= today +/- days':
|
|
2317
2332
|
case 'contains':
|
|
2318
2333
|
case 'starts with':
|
|
2319
2334
|
case 'ends with':
|
|
@@ -2323,6 +2338,32 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2323
2338
|
case '<=':
|
|
2324
2339
|
filterComponent().find(getFilterFieldSelector(path, 'value')).type(`{selectall}{del}${v}{enter}`);
|
|
2325
2340
|
return;
|
|
2341
|
+
case '<= today +/- days':
|
|
2342
|
+
case '>= today +/- days': {
|
|
2343
|
+
// This operator renders two separate controls (a sign select + a days number box),
|
|
2344
|
+
// not the single 'value' input the other operators share - getFilterFieldSelector's
|
|
2345
|
+
// generic 'value' selector doesn't match either one.
|
|
2346
|
+
const num = Number(v);
|
|
2347
|
+
const signInput = () => filterComponent().find(`[data-cy="filter-builder-xdays-sign-${path}"] .dx-texteditor-input`);
|
|
2348
|
+
signInput().then(($el) => {
|
|
2349
|
+
const id = $el[0].getAttribute('aria-controls');
|
|
2350
|
+
if (!id)
|
|
2351
|
+
cy.wrap($el).click();
|
|
2352
|
+
});
|
|
2353
|
+
signInput().then(($el) => {
|
|
2354
|
+
const id = $el[0].getAttribute('aria-controls');
|
|
2355
|
+
if (!id)
|
|
2356
|
+
throw new Error('Missing aria-controls');
|
|
2357
|
+
cy.get(`[id="${id}"] .dx-scrollview-content`)
|
|
2358
|
+
.children()
|
|
2359
|
+
.contains(num < 0 ? '-' : '+')
|
|
2360
|
+
.click();
|
|
2361
|
+
});
|
|
2362
|
+
filterComponent()
|
|
2363
|
+
.find(`[data-cy="filter-builder-xdays-value-${path}"] .dx-texteditor-input`)
|
|
2364
|
+
.type(`{selectall}{del}${Math.abs(num)}{enter}`);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2326
2367
|
default: {
|
|
2327
2368
|
const _exhaustive = operator;
|
|
2328
2369
|
throw new Error(`Unhandled operator: ${String(_exhaustive)}`);
|
|
@@ -19,15 +19,15 @@ Cypress.Commands.add('GLS_mainNavigation', (value) => {
|
|
|
19
19
|
throw new Error(`Could not find menu item: ${value}`);
|
|
20
20
|
}
|
|
21
21
|
return cy
|
|
22
|
-
.get('.ag-
|
|
22
|
+
.get('.ag-grid-scrolling-rows')
|
|
23
23
|
.first()
|
|
24
24
|
.then(($viewport) => {
|
|
25
25
|
const $label = $viewport.find('label').filter((_i, el) => el.innerText.trim() === value);
|
|
26
26
|
if ($label.length > 0 && $label.is(':visible')) {
|
|
27
|
-
return cy.contains('.ag-
|
|
27
|
+
return cy.contains('.ag-grid-scrolling-rows label', value).click();
|
|
28
28
|
}
|
|
29
29
|
return cy
|
|
30
|
-
.get('.ag-
|
|
30
|
+
.get('.ag-grid-scrolling-container [role="row"][tabindex="0"]')
|
|
31
31
|
.first()
|
|
32
32
|
.focus()
|
|
33
33
|
.realPress('ArrowDown')
|
|
@@ -65,7 +65,7 @@ Cypress.Commands.add('POR_clickTopDropdownMenu', (linkText) => {
|
|
|
65
65
|
});
|
|
66
66
|
Cypress.Commands.add('POR_checkTableHasData', () => {
|
|
67
67
|
cy.GLO_getElement(['ep$consignment_lst', 'shipments_grid']).within(() => {
|
|
68
|
-
cy.get('.ag-
|
|
68
|
+
cy.get('.ag-grid-scrolling-container').POR_isNotEmpty();
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
Cypress.Commands.add('POR_isNotEmpty', { prevSubject: true }, (subject, className) => {
|
|
@@ -35,6 +35,10 @@ let lastCypressLocator = '';
|
|
|
35
35
|
let lastGLOCommand = '';
|
|
36
36
|
let lastActionStartTime = null;
|
|
37
37
|
let wsInteractionCounter = 0;
|
|
38
|
+
// Module-scoped (not per-WS_loadingCheck-invocation): a componentApi call's reply can arrive after
|
|
39
|
+
// the invocation that recorded its smsgid has already resolved, so tracking has to survive across
|
|
40
|
+
// invocations within a test — cleared per-test in beforeEach below, same as the other cross-invocation state.
|
|
41
|
+
const componentApiSmsgids = new Set();
|
|
38
42
|
Cypress.on('command:start', (cmd) => {
|
|
39
43
|
const rawName = cmd.get('name');
|
|
40
44
|
const name = typeof rawName === 'string' ? rawName : '';
|
|
@@ -57,6 +61,7 @@ beforeEach(() => {
|
|
|
57
61
|
lastGLOCommand = '';
|
|
58
62
|
lastActionStartTime = null;
|
|
59
63
|
wsInteractionCounter = 0;
|
|
64
|
+
componentApiSmsgids.clear();
|
|
60
65
|
});
|
|
61
66
|
after(() => {
|
|
62
67
|
cy.task('flushWsMetrics', null, { log: false }).then((result) => {
|
|
@@ -828,17 +833,31 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
828
833
|
debugLog('🔁 Ignored columnState-only dataPush');
|
|
829
834
|
return;
|
|
830
835
|
}
|
|
836
|
+
// Server-initiated componentApi calls (e.g. internalGetColumnState) carry their own smsgid the
|
|
837
|
+
// client will echo back in its reply - record it so checkForDialogInteraction can recognize that
|
|
838
|
+
// reply as unrelated to a dialog, rather than mistaking it for one (both share the {smsgid, ret}
|
|
839
|
+
// wire shape).
|
|
840
|
+
const componentApisField = data.componentApis;
|
|
841
|
+
if (Array.isArray(componentApisField) && 'smsgid' in data && typeof data.smsgid === 'number') {
|
|
842
|
+
componentApiSmsgids.add(data.smsgid);
|
|
843
|
+
}
|
|
831
844
|
const hasDialog = 'serviceApis' in data && Array.isArray(data.serviceApis) && checkForDialogInResponse(data.serviceApis);
|
|
845
|
+
// Response triggers a dialog — hold the triggering request until the dialog is dismissed.
|
|
846
|
+
// The held id is threaded through to updateDialogState so the dialog record is attributed to
|
|
847
|
+
// the exact request that was held, instead of updateDialogState independently re-deriving it
|
|
848
|
+
// from state.lastOutgoingRequestId (which can point at a different, possibly already-resolved
|
|
849
|
+
// request when several requests are in flight - leaking the hold forever, since dismissal only
|
|
850
|
+
// releases a held request whose id matches the dialog's recorded triggeringRequestId exactly).
|
|
851
|
+
let heldRequestId = null;
|
|
832
852
|
if (hasDialog) {
|
|
833
|
-
|
|
834
|
-
handleDialogResponse(data);
|
|
853
|
+
heldRequestId = handleDialogResponse(data);
|
|
835
854
|
}
|
|
836
855
|
else {
|
|
837
856
|
handleNormalResponse(data);
|
|
838
857
|
}
|
|
839
858
|
if ('serviceApis' in data && Array.isArray(data.serviceApis)) {
|
|
840
859
|
updateLoadingState(data.serviceApis);
|
|
841
|
-
updateDialogState(data.serviceApis);
|
|
860
|
+
updateDialogState(data.serviceApis, heldRequestId);
|
|
842
861
|
updateExpectFormShowState(data.serviceApis);
|
|
843
862
|
}
|
|
844
863
|
// Check if this response echoes back a pending svyPush data change
|
|
@@ -889,6 +908,10 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
889
908
|
function checkForDialogInteraction(data) {
|
|
890
909
|
if (!('smsgid' in data && 'ret' in data && typeof data.ret === 'string'))
|
|
891
910
|
return;
|
|
911
|
+
// This reply answers a componentApi call (e.g. internalGetColumnState), not a dialog button -
|
|
912
|
+
// it just happens to share the same {smsgid, ret} wire shape. Ignore it here.
|
|
913
|
+
if (typeof data.smsgid === 'number' && componentApiSmsgids.delete(data.smsgid))
|
|
914
|
+
return;
|
|
892
915
|
state.extendedQuietPeriod = true;
|
|
893
916
|
// Release the oldest held non-Window pending dialog (FIFO order)
|
|
894
917
|
for (const [key, dialogData] of pendingDialogs) {
|
|
@@ -955,6 +978,8 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
955
978
|
/**
|
|
956
979
|
* When a response arrives that also opens a dialog, the triggering request is held
|
|
957
980
|
* rather than resolved. The actual result will arrive after the dialog is dismissed.
|
|
981
|
+
* Returns the held request id so the caller can attribute the dialog record to this exact
|
|
982
|
+
* request (see the comment in handleIncomingMessage for why that matters).
|
|
958
983
|
*/
|
|
959
984
|
function handleDialogResponse(data) {
|
|
960
985
|
let targetId = null;
|
|
@@ -971,6 +996,7 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
971
996
|
state.heldRequests.add(targetId);
|
|
972
997
|
debugLog(`🔲 Dialog detected - holding request: ${targetId}`);
|
|
973
998
|
}
|
|
999
|
+
return targetId;
|
|
974
1000
|
}
|
|
975
1001
|
function wsRequestPhase(sendTs) {
|
|
976
1002
|
// No loading detected yet at response time → pre_loading
|
|
@@ -1180,10 +1206,10 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
1180
1206
|
});
|
|
1181
1207
|
}
|
|
1182
1208
|
/** Dispatches serviceApi calls to the appropriate dialog/window handler. */
|
|
1183
|
-
function updateDialogState(apis) {
|
|
1209
|
+
function updateDialogState(apis, heldRequestId = null) {
|
|
1184
1210
|
apis.forEach((api) => {
|
|
1185
1211
|
if (api.name === 'dialogs')
|
|
1186
|
-
handleMessageDialog(api);
|
|
1212
|
+
handleMessageDialog(api, heldRequestId);
|
|
1187
1213
|
if (api.name === '$windowService')
|
|
1188
1214
|
handleWindowDialog(api);
|
|
1189
1215
|
if (api.name === 'window' && api.call === 'cancelFormPopupInternal')
|
|
@@ -1212,8 +1238,14 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
1212
1238
|
* Only Info dialogs with the literal title "Info" are auto-handled and block the loader.
|
|
1213
1239
|
* All others (Warning, Error, Input, Select, Info with a different title) use a "Question:"
|
|
1214
1240
|
* key which unblocks the loader so the test can interact with the dialog.
|
|
1241
|
+
*
|
|
1242
|
+
* triggeringRequestId must be the exact request id handleDialogResponse held for this same
|
|
1243
|
+
* incoming message (passed in as heldRequestId), not independently re-derived from
|
|
1244
|
+
* state.lastOutgoingRequestId - that can point at a different (or already-resolved) request
|
|
1245
|
+
* when several requests are in flight, which would leave the actually-held request unreleasable
|
|
1246
|
+
* once the dialog is dismissed and permanently hang the loader.
|
|
1215
1247
|
*/
|
|
1216
|
-
function handleMessageDialog(api) {
|
|
1248
|
+
function handleMessageDialog(api, heldRequestId = null) {
|
|
1217
1249
|
if (!api.call || !DIALOG_METHODS.includes(api.call))
|
|
1218
1250
|
return;
|
|
1219
1251
|
const dialogType = api.call.replace('show', '').replace('Dialog', '');
|
|
@@ -1221,7 +1253,7 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
1221
1253
|
// 'Info' key → loader blocks until auto-handled; 'Question' key → loader unblocks immediately
|
|
1222
1254
|
const keyType = dialogType === 'Info' && wsTitle === 'Info' ? 'Info' : 'Question';
|
|
1223
1255
|
const dialogKey = `${keyType}:${++dialogIdCounter}`;
|
|
1224
|
-
pendingDialogs.set(dialogKey, { dialogType, triggeringRequestId:
|
|
1256
|
+
pendingDialogs.set(dialogKey, { dialogType, triggeringRequestId: heldRequestId, timestamp: Date.now() });
|
|
1225
1257
|
debugLog(`🔲 ${dialogType} dialog opened: ${api.call}`, { ...createDialogDebugInfo(api), dialogKey, wsTitle });
|
|
1226
1258
|
}
|
|
1227
1259
|
/** Routes $windowService API calls to create/show/destroy handlers. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@globisnv/cypress-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Core building blocks for Globis Cypress E2E tests",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"globisOnline": {
|
|
18
|
-
"minVersion": "11.0.
|
|
18
|
+
"minVersion": "11.0.3"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./dist/index.js",
|