@globisnv/cypress-core 1.1.0 → 1.1.2
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.
|
@@ -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
|
};
|
|
@@ -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
|
*
|
|
@@ -713,6 +713,12 @@ Cypress.Commands.add('GLO_selectTab', (selector, tabName, openOrClose) => {
|
|
|
713
713
|
const isOpenAttr = $tab[0].getAttribute('open'); // Attribute is a string 'true' or missing -> null
|
|
714
714
|
const isOpen = isOpenAttr === 'true'; // Convert to boolean
|
|
715
715
|
if (openOrClose !== isOpen) {
|
|
716
|
+
// The tab header is a div, so Cypress's `:disabled` actionability check doesn't apply: the click goes through
|
|
717
|
+
// and the server silently ignores it. Closing a disabled tab that is open fails too, because the close request
|
|
718
|
+
// goes through the same click on the same disabled tab, which the server ignores the same way.
|
|
719
|
+
if ($tab[0].hasAttribute('disabled')) {
|
|
720
|
+
throw new Error(`Tab "${tabName}" in ${selector.join('.')} is disabled and cannot be ${openOrClose ? 'opened' : 'closed'}`);
|
|
721
|
+
}
|
|
716
722
|
const action = openOrClose ? 'Opening' : 'Closing';
|
|
717
723
|
cy.log(`${action} tab ${tabName}`);
|
|
718
724
|
cy.wrap($tab).click();
|
|
@@ -960,21 +966,43 @@ Cypress.Commands.add('GLO_filterGrid', (colId, selector, text, exactMatch = noEq
|
|
|
960
966
|
}
|
|
961
967
|
});
|
|
962
968
|
if (filterType) {
|
|
969
|
+
// The typed value needs to sync into the grid's filter model before the operator
|
|
970
|
+
// menu opens, or the menu's own value field still reads empty and the operator
|
|
971
|
+
// picker starts from a stale/default state (reproduced manually: opening the menu
|
|
972
|
+
// too soon after typing shows the filter value field blank).
|
|
973
|
+
cy.GLO_checkLoadingIndicator();
|
|
963
974
|
cy.get('@filterHeaderCell').find('button').click();
|
|
964
|
-
|
|
965
|
-
//
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
975
|
+
// Selecting the operator in this popup is occasionally flaky in Cypress (the click
|
|
976
|
+
// registers but the picker display doesn't update to the new operator - reproduced
|
|
977
|
+
// manually in the live app and ruled out as a wrong-popup/stale-aria-controls issue;
|
|
978
|
+
// the open+select sequence itself is correct). Self-heal by reopening and reselecting
|
|
979
|
+
// instead of guessing at the exact cause of the dropped click.
|
|
980
|
+
// A compound AND/OR filter always has a second condition row scaffolded in (even
|
|
981
|
+
// when unused), each with its own operator picker - scope to .first() throughout so
|
|
982
|
+
// this always targets the primary condition's picker instead of matching 2 elements.
|
|
983
|
+
const selectFilterType = (attemptsLeft) => {
|
|
984
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').first().click();
|
|
985
|
+
cy.wait(250); // Special dropdown for filter, doesn't work without small wait..
|
|
986
|
+
cy.get('.ag-filter-menu .ag-picker-expanded')
|
|
987
|
+
.invoke('attr', 'aria-controls')
|
|
988
|
+
.then((dropDownId) => {
|
|
989
|
+
cy.get(`[id="${dropDownId}"]`)
|
|
990
|
+
.should('be.visible')
|
|
991
|
+
.within(() => {
|
|
992
|
+
cy.get('.ag-list-item').contains(filterType).should('be.visible').parent().click();
|
|
993
|
+
});
|
|
974
994
|
});
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
995
|
+
cy.GLO_checkAGLoader();
|
|
996
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display')
|
|
997
|
+
.first()
|
|
998
|
+
.then(($el) => {
|
|
999
|
+
if ($el.text().trim() !== filterType && attemptsLeft > 0) {
|
|
1000
|
+
selectFilterType(attemptsLeft - 1);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
selectFilterType(2);
|
|
1005
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').first().should('contain.text', filterType);
|
|
978
1006
|
cy.press(Cypress.Keyboard.Keys.ESC); // Close the filter menu
|
|
979
1007
|
}
|
|
980
1008
|
cy.GLO_checkLoadingIndicator();
|
|
@@ -1141,7 +1169,7 @@ const dblClickRowInGrid = (type, value, columnId, equals) => {
|
|
|
1141
1169
|
};
|
|
1142
1170
|
const helper = (filterSelector) => {
|
|
1143
1171
|
if (columnId) {
|
|
1144
|
-
cy.get('[row-index]')
|
|
1172
|
+
cy.get('[row-index]:visible')
|
|
1145
1173
|
.find(`[col-id="${columnId}"]`)
|
|
1146
1174
|
.filter(filterSelector, { timeout: 10000 })
|
|
1147
1175
|
.parentsUntil('.ag-center-cols-container')
|
|
@@ -1150,7 +1178,7 @@ const dblClickRowInGrid = (type, value, columnId, equals) => {
|
|
|
1150
1178
|
.then(saveValues);
|
|
1151
1179
|
}
|
|
1152
1180
|
else {
|
|
1153
|
-
cy.get('[row-index]').filter(filterSelector, { timeout: 10000 }).first().then(saveValues);
|
|
1181
|
+
cy.get('[row-index]:visible').filter(filterSelector, { timeout: 10000 }).first().then(saveValues);
|
|
1154
1182
|
}
|
|
1155
1183
|
};
|
|
1156
1184
|
cy.GLO_checkAGLoader();
|
|
@@ -1807,6 +1835,9 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
|
|
|
1807
1835
|
cy.GLO_getElement(selector).within(() => {
|
|
1808
1836
|
cy.GLO_clickRowInGrid(rowFilter, columnId);
|
|
1809
1837
|
});
|
|
1838
|
+
// Re-check outside the .within(): loading-indicator renders at page level, so the check
|
|
1839
|
+
// inside GLO_clickRowInGrid (scoped to the grid element above) can't see it and resolves early.
|
|
1840
|
+
cy.GLO_checkLoadingIndicator();
|
|
1810
1841
|
cy.wait(500); // Else both click and dblclick fire too fast
|
|
1811
1842
|
cy.GLO_dblClickRowInGrid(row, selector, rowFilter, columnId, skipCheck);
|
|
1812
1843
|
}
|
|
@@ -2143,6 +2174,11 @@ Cypress.Commands.add('GLO_checkFieldEnabled', (selector, shouldBeEnabled = enabl
|
|
|
2143
2174
|
}
|
|
2144
2175
|
});
|
|
2145
2176
|
});
|
|
2177
|
+
Cypress.Commands.add('GLO_checkTabEnabled', (selector, tabName, shouldBeEnabled = enabled) => {
|
|
2178
|
+
cy.GLO_getElement(selector)
|
|
2179
|
+
.contains('[tabcontainer] .tab', tabName)
|
|
2180
|
+
.should(shouldBeEnabled ? 'not.have.attr' : 'have.attr', 'disabled');
|
|
2181
|
+
});
|
|
2146
2182
|
Cypress.Commands.add('GLO_checkFieldBgColor', (selector, color = 'red') => {
|
|
2147
2183
|
const [formName, fieldOrFields] = selector;
|
|
2148
2184
|
const fields = Array.isArray(fieldOrFields) ? fieldOrFields : [fieldOrFields];
|
|
@@ -2290,7 +2326,12 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2290
2326
|
cy.wrap($field).find('button.fa-search').click();
|
|
2291
2327
|
cy.GLO_getElement(['svy_nav_selection', 'search_criteria']).type(String(v));
|
|
2292
2328
|
cy.GLO_clickButton(['svy_nav_selection', 'btn_search']);
|
|
2293
|
-
dblClickRowInGrid
|
|
2329
|
+
// dblClickRowInGrid is document-scoped unless wrapped in .within() - without this,
|
|
2330
|
+
// its unscoped [row-index] lookup can match a row in whatever grid sits behind this
|
|
2331
|
+
// dialog instead of the dialog's own grid. Same scoping GLO_popupWindow uses.
|
|
2332
|
+
cy.GLO_getVisible('.svy-dialog.active .window-body').within(() => {
|
|
2333
|
+
dblClickRowInGrid(span, String(v));
|
|
2334
|
+
});
|
|
2294
2335
|
cy.GLO_checkLoadingIndicator();
|
|
2295
2336
|
}
|
|
2296
2337
|
else {
|
|
@@ -2312,8 +2353,6 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2312
2353
|
// Fall through
|
|
2313
2354
|
case 'before':
|
|
2314
2355
|
case 'after':
|
|
2315
|
-
case '<= today +/- days':
|
|
2316
|
-
case '>= today +/- days':
|
|
2317
2356
|
case 'contains':
|
|
2318
2357
|
case 'starts with':
|
|
2319
2358
|
case 'ends with':
|
|
@@ -2323,6 +2362,32 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2323
2362
|
case '<=':
|
|
2324
2363
|
filterComponent().find(getFilterFieldSelector(path, 'value')).type(`{selectall}{del}${v}{enter}`);
|
|
2325
2364
|
return;
|
|
2365
|
+
case '<= today +/- days':
|
|
2366
|
+
case '>= today +/- days': {
|
|
2367
|
+
// This operator renders two separate controls (a sign select + a days number box),
|
|
2368
|
+
// not the single 'value' input the other operators share - getFilterFieldSelector's
|
|
2369
|
+
// generic 'value' selector doesn't match either one.
|
|
2370
|
+
const num = Number(v);
|
|
2371
|
+
const signInput = () => filterComponent().find(`[data-cy="filter-builder-xdays-sign-${path}"] .dx-texteditor-input`);
|
|
2372
|
+
signInput().then(($el) => {
|
|
2373
|
+
const id = $el[0].getAttribute('aria-controls');
|
|
2374
|
+
if (!id)
|
|
2375
|
+
cy.wrap($el).click();
|
|
2376
|
+
});
|
|
2377
|
+
signInput().then(($el) => {
|
|
2378
|
+
const id = $el[0].getAttribute('aria-controls');
|
|
2379
|
+
if (!id)
|
|
2380
|
+
throw new Error('Missing aria-controls');
|
|
2381
|
+
cy.get(`[id="${id}"] .dx-scrollview-content`)
|
|
2382
|
+
.children()
|
|
2383
|
+
.contains(num < 0 ? '-' : '+')
|
|
2384
|
+
.click();
|
|
2385
|
+
});
|
|
2386
|
+
filterComponent()
|
|
2387
|
+
.find(`[data-cy="filter-builder-xdays-value-${path}"] .dx-texteditor-input`)
|
|
2388
|
+
.type(`{selectall}{del}${Math.abs(num)}{enter}`);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2326
2391
|
default: {
|
|
2327
2392
|
const _exhaustive = operator;
|
|
2328
2393
|
throw new Error(`Unhandled operator: ${String(_exhaustive)}`);
|
|
@@ -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": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Core building blocks for Globis Cypress E2E tests",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"url": "git+https://github.com/globisnv/globis-cypress-core.git"
|
|
13
13
|
},
|
|
14
14
|
"publishConfig": {
|
|
15
|
-
"access": "public"
|
|
15
|
+
"access": "public",
|
|
16
|
+
"tag": "release-1.x"
|
|
16
17
|
},
|
|
17
18
|
"globisOnline": {
|
|
18
19
|
"minVersion": "11.0.2"
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@biomejs/biome": "^2.5.7",
|
|
61
62
|
"@types/node": "^26.1.2",
|
|
62
|
-
"cypress": "^16.1.
|
|
63
|
+
"cypress": "^16.1.1",
|
|
63
64
|
"cypress-real-events": "^1.15.1",
|
|
64
65
|
"typescript": "^7.0.2"
|
|
65
66
|
},
|