@devassure/tpp-engine 0.0.0-stage → 1.8.1

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.
Files changed (69) hide show
  1. package/DEV_README.md +19 -0
  2. package/README.md +6 -3
  3. package/index.js +121 -0
  4. package/package.json +30 -4
  5. package/src/accessibility/combined-a11y-report.js +208 -0
  6. package/src/accessibility/tpp-a11y.js +81 -0
  7. package/src/actions.js +3201 -0
  8. package/src/api/api-auth.js +18 -0
  9. package/src/api/api-service.js +289 -0
  10. package/src/app-config.json +7 -0
  11. package/src/autoheal-actions.js +309 -0
  12. package/src/console-log/console-log-manager.js +39 -0
  13. package/src/console-log/console-log-utils.js +201 -0
  14. package/src/database/mysql/mysql-service.js +23 -0
  15. package/src/database/neo4j/neo4j-service.js +32 -0
  16. package/src/database/oracle/oracle-service.js +48 -0
  17. package/src/database/pg/pg-service.js +29 -0
  18. package/src/downloads/downloads-manager.js +86 -0
  19. package/src/downloads/downloads-utils.js +113 -0
  20. package/src/globalsetup.js +68 -0
  21. package/src/globalteardown.js +57 -0
  22. package/src/hooks/each-hook.js +45 -0
  23. package/src/hooks/hook-utils.js +21 -0
  24. package/src/library/authenticator/authenticator-utils.js +43 -0
  25. package/src/library/csv/csv-utils.js +297 -0
  26. package/src/library/date/date-utils.js +168 -0
  27. package/src/library/emailTesting/TokenConfig.js +46 -0
  28. package/src/library/emailTesting/emailTesting-utils.js +527 -0
  29. package/src/library/excel/excel-utils.js +922 -0
  30. package/src/library/file/file-action-utils.js +129 -0
  31. package/src/library/file/file-utils.js +127 -0
  32. package/src/library/json/json-utils.js +104 -0
  33. package/src/library/math/math-utils.js +283 -0
  34. package/src/library/softAssert/softAssert-utils.js +154 -0
  35. package/src/library/table/table-utils.js +92 -0
  36. package/src/library/text/text-utils.js +45 -0
  37. package/src/library/tpp-library.js +81 -0
  38. package/src/library/web/slider-utils.js +66 -0
  39. package/src/mobile/appium/appium-manager.js +158 -0
  40. package/src/mobile/device-cloud/browserstack-service.js +44 -0
  41. package/src/mobile/device-cloud/commands/browserstack-commands.js +20 -0
  42. package/src/mobile/device-cloud/commands/device-cloud-commands.js +17 -0
  43. package/src/mobile/device-cloud/device-cloud-service.js +29 -0
  44. package/src/mobile/device-cloud/pcloudy-service.js +65 -0
  45. package/src/mobile/mobile-actions.js +1568 -0
  46. package/src/mobile/recorder-utils/css-selector-generator.js +2 -0
  47. package/src/mobile/recorder-utils/finder.js +752 -0
  48. package/src/mobile/recorder-utils/mobile-recorder.js +266 -0
  49. package/src/mobile/web-driver-utils.js +110 -0
  50. package/src/models/tpp-element.js +58 -0
  51. package/src/models/tpp-mobile-element.js +50 -0
  52. package/src/network-log/network-log-manager.js +96 -0
  53. package/src/network-log/network-log-utils.js +361 -0
  54. package/src/types/elements.types.js +24 -0
  55. package/src/utils/ai-utils.js +48 -0
  56. package/src/utils/assert-utils.js +50 -0
  57. package/src/utils/autoheal-utils.js +9 -0
  58. package/src/utils/common-utils.js +120 -0
  59. package/src/utils/cookie-utils.js +70 -0
  60. package/src/utils/json-utils.js +10 -0
  61. package/src/utils/log-utils.js +10 -0
  62. package/src/utils/logger.js +76 -0
  63. package/src/utils/screenshot-utils.js +42 -0
  64. package/src/utils/token-utils.js +18 -0
  65. package/src/utils/vectordb-utils.js +125 -0
  66. package/src/visual-test/tpp-visual-test.js +122 -0
  67. package/src/web/web-executor-agent.js +149 -0
  68. package/src/web/web-executor-data-fetch.js +353 -0
  69. package/test-excel-utils.js +570 -0
@@ -0,0 +1,922 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const Excel = require('exceljs');
4
+ const xlsx = require('xlsx');
5
+
6
+ const { Logger } = require('../../utils/logger');
7
+ const { FileUtils } = require('../file/file-utils');
8
+ const { TableUtils } = require('../table/table-utils');
9
+ const { processExcel } = require('../../utils/vectordb-utils');
10
+
11
+ const {
12
+ getLastElements,
13
+ getElementsInRange,
14
+ getFirstElements,
15
+ getElementsAtIndices,
16
+ getAnyElements,
17
+ sleep,
18
+ getBoolean,
19
+ isNumber
20
+ } = require('../../utils/common-utils');
21
+
22
+ class ExcelUtils {
23
+ constructor(caller, blacklistedLogs, customConfig) {
24
+ this.caller = caller;
25
+ this.blacklistedLogs = blacklistedLogs;
26
+ this.customConfig = customConfig;
27
+ this.fileUtils = new FileUtils();
28
+ this.dependenciesPath = customConfig?.dependenciesPath;
29
+ }
30
+
31
+ setCaller(caller) {
32
+ this.caller = caller;
33
+ }
34
+
35
+ setBlacklistedLogs(blacklistedLogs) {
36
+ this.blacklistedLogs = blacklistedLogs;
37
+ }
38
+
39
+ // ---------- Small internal helpers ----------
40
+
41
+ _getLogger(logLabel) {
42
+ return new Logger(this.caller, this.blacklistedLogs);
43
+ }
44
+
45
+ _ensureFileExists(rawPath, logger, logLabel) {
46
+ const filePath = this.fileUtils.getFilePath(rawPath);
47
+ if (!fs.existsSync(filePath)) {
48
+ logger.failureLog(logLabel);
49
+ throw { message: `Excel file ${filePath} does not exists!` };
50
+ }
51
+ return filePath;
52
+ }
53
+
54
+ _cleanupTempFile(filePath) {
55
+ if (!filePath) return;
56
+ try {
57
+ if (fs.existsSync(filePath)) {
58
+ fs.unlinkSync(filePath);
59
+ }
60
+ } catch {
61
+ // ignore cleanup errors
62
+ }
63
+ }
64
+
65
+ _isSheetsReadError(err) {
66
+ return err?.message?.includes("reading 'sheets'");
67
+ }
68
+
69
+ async _fixFileWithXlsxAndSleep(filePath, logLabel, logger, sleepMs = 1000) {
70
+ const fixed = await this.fixExcelFileWithXlsx(filePath, logLabel, logger);
71
+ await sleep(sleepMs);
72
+ return fixed;
73
+ }
74
+
75
+ _compareAndLogRowDifference(row1, row2, rowNumber, sheetName, allSheets, logLabel, logger, failures) {
76
+ if (JSON.stringify(row1) !== JSON.stringify(row2)) {
77
+ let message =
78
+ !row1?.length
79
+ ? `Row ${rowNumber} is empty in first file`
80
+ : !row2?.length
81
+ ? `Row ${rowNumber} is empty in second file`
82
+ : `❌ Row ${rowNumber} is different!`;
83
+
84
+ if (allSheets && sheetName) {
85
+ message = `${sheetName}: ${message}`;
86
+ }
87
+ failures.push(message);
88
+ logger.log(logLabel, message);
89
+
90
+ if (row1?.length && row2?.length) {
91
+ logger.log(logLabel, `row in 1st File: ${JSON.stringify(row1)}`);
92
+ logger.log(logLabel, `row in 2nd File: ${JSON.stringify(row2)}`);
93
+ }
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ // ---------- Public wrappers (unchanged behavior) ----------
100
+
101
+ async compare_all_sheets(filePath1, filePath2) {
102
+ const logLabel = `Excel.Compare data of all sheets of ${filePath1} and ${filePath2}`;
103
+ const logger = this._getLogger(logLabel);
104
+ logger.startLog(logLabel);
105
+ await this.compareExcelFiles(filePath1, filePath2, undefined, undefined, true, logLabel);
106
+ logger.endLog(logLabel);
107
+ }
108
+
109
+ async compare_first_sheet(filePath1, filePath2) {
110
+ const logLabel = `Excel.Compare data of first sheet of ${filePath1} and ${filePath2}`;
111
+ const logger = this._getLogger(logLabel);
112
+ logger.startLog(logLabel);
113
+ await this.compareExcelFiles(filePath1, filePath2, undefined, undefined, false, logLabel);
114
+ logger.endLog(logLabel);
115
+ }
116
+
117
+ async compare_given_sheet(filePath1, sheetName1, filePath2, sheetName2) {
118
+ const logLabel = `Excel.Compare data of file ${filePath1} sheet ${sheetName1} and file ${filePath2} sheet ${sheetName2}`;
119
+ const logger = this._getLogger(logLabel);
120
+ logger.startLog(logLabel);
121
+ await this.compareExcelFiles(filePath1, filePath2, sheetName1, sheetName2, false, logLabel);
122
+ logger.endLog(logLabel);
123
+ }
124
+
125
+ async read_first_sheet_rows_as_array(filePath) {
126
+ return await this.read_rows_as_array(filePath);
127
+ }
128
+
129
+ async read_rows_as_array(filePath, sheetName) {
130
+ const logLabel = sheetName
131
+ ? `Excel.Read sheet ${sheetName} rows as array ${filePath}`
132
+ : `Excel.Read first sheet rows as array ${filePath}`;
133
+
134
+ const logger = this._getLogger(logLabel);
135
+ logger.startLog(logLabel);
136
+ const data = await this.readSheetAsArray(filePath, sheetName, logLabel);
137
+ logger.endLog(logLabel);
138
+ return data;
139
+ }
140
+
141
+ async read_first_sheet_rows_as_object(filePath) {
142
+ return await this.read_rows_as_object(filePath);
143
+ }
144
+
145
+ async read_rows_as_object(filePath, sheetName) {
146
+ const logLabel = sheetName
147
+ ? `Excel.Read sheet ${sheetName} rows as objects ${filePath}`
148
+ : `Excel.Read first sheet rows as objects ${filePath}`;
149
+
150
+ const logger = this._getLogger(logLabel);
151
+ logger.startLog(logLabel);
152
+
153
+ const dataArray = await this.readSheetAsArray(filePath, sheetName, logLabel);
154
+ const fields = dataArray[0] || [];
155
+ const data = dataArray.slice(1).map(cells => {
156
+ const row = {};
157
+ for (let i = 0; i < fields.length; i++) {
158
+ if (!fields[i]) continue;
159
+ row[`${fields[i]}`] = i < cells.length ? cells[i] || '' : '';
160
+ }
161
+ return row;
162
+ });
163
+
164
+ logger.endLog(logLabel);
165
+ return data;
166
+ }
167
+
168
+ // ---------- Core reading helpers ----------
169
+
170
+ async readFixedFileAsArray(filePath, sheetName, logLabel, rowCount, ignoreFirstRow) {
171
+ const workbook = new Excel.Workbook();
172
+ await workbook.xlsx.readFile(filePath);
173
+
174
+ let sheet = sheetName
175
+ ? workbook.getWorksheet(sheetName)
176
+ : workbook.worksheets[0];
177
+
178
+ if (!sheet) {
179
+ const msg = sheetName
180
+ ? `Sheet with name ${sheetName} not found!`
181
+ : 'No sheet found in the file!';
182
+ throw { message: msg };
183
+ }
184
+
185
+ const result = [];
186
+ const maxRow = rowCount ? Math.min(rowCount, sheet.rowCount) : sheet.rowCount;
187
+
188
+ for (let i = 1; i <= maxRow; i++) {
189
+ const row = sheet.getRow(i);
190
+ if (!row || row.values.length <= 1) continue;
191
+ const cleanedRow = row.values.slice(1); // first index unused
192
+ result.push(cleanedRow);
193
+ }
194
+
195
+ if (ignoreFirstRow && result.length > 0) {
196
+ return result.slice(1);
197
+ }
198
+
199
+ return result;
200
+ }
201
+
202
+ async fixExcelFileWithXlsx(filePath, logLabel, loggerFromCaller) {
203
+ const logger = loggerFromCaller || this._getLogger(logLabel);
204
+ logger.log(
205
+ logLabel,
206
+ 'Excel file appears corrupted or incompatible with ExcelJS stream reader. Attempting to fix using xlsx library...'
207
+ );
208
+
209
+ try {
210
+ const workbook = xlsx.readFile(filePath);
211
+
212
+ if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
213
+ throw { message: 'No sheets found in the Excel file' };
214
+ }
215
+
216
+ const newWorkbook = new Excel.Workbook();
217
+
218
+ workbook.SheetNames.forEach(sheetName => {
219
+ const worksheet = workbook.Sheets[sheetName];
220
+ const data = xlsx.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
221
+ const newWorksheet = newWorkbook.addWorksheet(sheetName);
222
+ data.forEach(row => newWorksheet.addRow(row));
223
+ });
224
+
225
+ const tempDir = path.join(path.dirname(filePath), '.tpp-excel-temp');
226
+ if (!fs.existsSync(tempDir)) {
227
+ fs.mkdirSync(tempDir, { recursive: true });
228
+ }
229
+
230
+ const fixedFilePath = path.join(
231
+ tempDir,
232
+ `fixed_${Date.now()}_${path.basename(filePath)}`
233
+ );
234
+ await newWorkbook.xlsx.writeFile(fixedFilePath);
235
+
236
+ logger.log(logLabel, `Fixed Excel file created at: ${fixedFilePath}`);
237
+ return fixedFilePath;
238
+ } catch (error) {
239
+ const msg = error?.message || String(error);
240
+ logger.log(logLabel, `Failed to fix Excel file: ${msg}`);
241
+ throw { message: `Failed to fix Excel file: ${msg}` };
242
+ }
243
+ }
244
+
245
+ async readSheetAsArray(
246
+ filePath,
247
+ sheetName,
248
+ logLabel,
249
+ rowCount,
250
+ ignoreFirstRow,
251
+ retryCount,
252
+ fixedFilePath
253
+ ) {
254
+ const logger = this._getLogger(logLabel);
255
+ filePath = this.fileUtils.getFilePath(filePath);
256
+
257
+ if (!fs.existsSync(filePath)) {
258
+ logger.failureLog(logLabel);
259
+ throw { message: `Excel file ${filePath} does not exists!` };
260
+ }
261
+
262
+ try {
263
+ const workbookReader = new Excel.stream.xlsx.WorkbookReader(filePath);
264
+ let sheetFound = false;
265
+ const result = [];
266
+
267
+ for await (const sheet of workbookReader) {
268
+ if (sheetName && sheet.name !== sheetName) continue;
269
+
270
+ sheetFound = true;
271
+
272
+ for await (const row of sheet) {
273
+ if (!row || !row.values) continue;
274
+ const cleanedRow = row.values.slice(1);
275
+ result.push(cleanedRow);
276
+
277
+ if (rowCount && row.number > rowCount) {
278
+ break;
279
+ }
280
+ }
281
+ break;
282
+ }
283
+
284
+ if (!sheetFound) {
285
+ const message = sheetName
286
+ ? `Sheet with name ${sheetName} not found!`
287
+ : 'No sheet found in the file!';
288
+ logger.failureLog(logLabel);
289
+ throw { message };
290
+ }
291
+
292
+ // Sheet found but nothing read -> try auto-fix
293
+ if (result.length === 0 && !retryCount && !fixedFilePath) {
294
+ logger.log(logLabel, 'Sheet found but no rows were read. Attempting to fix file...');
295
+ try {
296
+ const newFixedPath = await this._fixFileWithXlsxAndSleep(filePath, logLabel, logger);
297
+ const fixedResult = await this.readFixedFileAsArray(
298
+ newFixedPath,
299
+ sheetName,
300
+ logLabel,
301
+ rowCount,
302
+ ignoreFirstRow
303
+ );
304
+ this._cleanupTempFile(newFixedPath);
305
+ return fixedResult;
306
+ } catch (fixError) {
307
+ logger.log(logLabel, `File fix attempt failed: ${fixError.message}`);
308
+ }
309
+ }
310
+
311
+ if (ignoreFirstRow && result.length > 0) {
312
+ return result.slice(1);
313
+ }
314
+
315
+ return result;
316
+ } catch (err) {
317
+ const message = err?.message || '';
318
+
319
+ // Special invalid signature case
320
+ if (!retryCount && message.includes('invalid signature: 0x4a4')) {
321
+ const newPath = await this.extractAndRewriteCleanXlsx(filePath, logLabel);
322
+ await sleep(1000);
323
+ return await this.readSheetAsArray(
324
+ newPath,
325
+ sheetName,
326
+ logLabel,
327
+ rowCount,
328
+ ignoreFirstRow,
329
+ 1,
330
+ fixedFilePath
331
+ );
332
+ }
333
+
334
+ // Stream "reading 'sheets'" error handling
335
+ if (this._isSheetsReadError(err)) {
336
+ // try auto-fix first
337
+ if (!retryCount && !fixedFilePath) {
338
+ logger.log(logLabel, 'Error reading sheets. Attempting to fix file...');
339
+ try {
340
+ const newFixedPath = await this._fixFileWithXlsxAndSleep(filePath, logLabel, logger);
341
+ const fixedResult = await this.readFixedFileAsArray(
342
+ newFixedPath,
343
+ sheetName,
344
+ logLabel,
345
+ rowCount,
346
+ ignoreFirstRow
347
+ );
348
+ this._cleanupTempFile(newFixedPath);
349
+ return fixedResult;
350
+ } catch (fixError) {
351
+ if (fixError.message?.includes('not found')) {
352
+ throw fixError;
353
+ }
354
+ logger.log(
355
+ logLabel,
356
+ `File fix attempt failed: ${fixError.message}. Retrying with original file...`
357
+ );
358
+ }
359
+ }
360
+
361
+ // Retry with original file
362
+ if (!fixedFilePath && (!retryCount || retryCount < 4)) {
363
+ console.log('Error in iterating sheets. Retrying...', message);
364
+ await sleep(5000);
365
+ return await this.readSheetAsArray(
366
+ filePath,
367
+ sheetName,
368
+ logLabel,
369
+ rowCount,
370
+ ignoreFirstRow,
371
+ (retryCount || 0) + 1,
372
+ fixedFilePath
373
+ );
374
+ }
375
+
376
+ throw err;
377
+ }
378
+
379
+ throw err;
380
+ }
381
+ }
382
+
383
+ // ---------- Compare files ----------
384
+
385
+ async compareExcelFiles(
386
+ filePath1,
387
+ filePath2,
388
+ sheetName1,
389
+ sheetName2,
390
+ allSheets,
391
+ logLabel,
392
+ retryCount,
393
+ fixedFilePath1,
394
+ fixedFilePath2
395
+ ) {
396
+ const logger = this._getLogger(logLabel);
397
+
398
+ filePath1 = this._ensureFileExists(filePath1, logger, logLabel);
399
+ filePath2 = this._ensureFileExists(filePath2, logger, logLabel);
400
+
401
+ if (filePath1.toLowerCase() === filePath2.toLowerCase()) {
402
+ logger.failureLog(logLabel);
403
+ throw { message: 'Given file paths are same!' };
404
+ }
405
+
406
+ // Check file sizes - if both are < 2MB, use xlsx library for comparison
407
+ const fileSize1 = fs.statSync(filePath1).size;
408
+ const fileSize2 = fs.statSync(filePath2).size;
409
+ const twoMB = 2 * 1024 * 1024; // 2MB in bytes
410
+
411
+ if (fileSize1 < twoMB && fileSize2 < twoMB) {
412
+ const workbook1 = xlsx.readFile(filePath1);
413
+ const workbook2 = xlsx.readFile(filePath2);
414
+
415
+ const failures = [];
416
+ let sheetFound = false;
417
+
418
+ for (const sheetName of workbook1.SheetNames) {
419
+ if (sheetName1 && sheetName !== sheetName1) {
420
+ continue;
421
+ }
422
+
423
+ sheetFound = true;
424
+
425
+ const targetSheetName = sheetName2 || sheetName;
426
+ if (!workbook2.SheetNames.includes(targetSheetName)) {
427
+ logger.failureLog(logLabel);
428
+ throw { message: `Sheet ${sheetName} not found in file2` };
429
+ }
430
+
431
+ const worksheet1 = workbook1.Sheets[sheetName];
432
+ const worksheet2 = workbook2.Sheets[targetSheetName];
433
+ const data1 = xlsx.utils.sheet_to_json(worksheet1, { header: 1, defval: '' });
434
+ const data2 = xlsx.utils.sheet_to_json(worksheet2, { header: 1, defval: '' });
435
+
436
+ const maxRows = Math.max(data1.length, data2.length);
437
+ for (let i = 0; i < maxRows; i++) {
438
+ const row1 = data1[i] || [];
439
+ const row2 = data2[i] || [];
440
+
441
+ this._compareAndLogRowDifference(row1, row2, i + 1, sheetName, allSheets, logLabel, logger, failures);
442
+ }
443
+
444
+ if (!sheetName1 && !allSheets) {
445
+ break;
446
+ }
447
+ }
448
+
449
+ if (!sheetFound) {
450
+ const message = sheetName1
451
+ ? `Sheet with name ${sheetName1} not found!`
452
+ : 'No sheet found in the first file!';
453
+ logger.failureLog(logLabel);
454
+ throw { message };
455
+ }
456
+
457
+ if (failures.length) {
458
+ logger.failureLog(logLabel);
459
+ throw { message: `There are ${failures.length} rows that do not match in the 2 files!` };
460
+ }
461
+
462
+ return;
463
+ }
464
+
465
+ try {
466
+ const workbookReader1 = new Excel.stream.xlsx.WorkbookReader(filePath1);
467
+ let sheetFound = false;
468
+ const failures = [];
469
+ let rowsRead = 0;
470
+
471
+ for await (const sheet1 of workbookReader1) {
472
+ console.log(`Reading sheet1: ${sheet1.name}`);
473
+
474
+ if (sheetName1 && sheet1.name !== sheetName1) {
475
+ continue;
476
+ }
477
+
478
+ sheetFound = true;
479
+
480
+ const sheet2 = await this.getMatchingSheet(
481
+ filePath2,
482
+ sheetName2 || sheet1.name,
483
+ undefined,
484
+ fixedFilePath2
485
+ );
486
+
487
+ if (!sheet2) {
488
+ logger.failureLog(logLabel);
489
+ throw { message: `Sheet ${sheet1.name} not found in file2` };
490
+ }
491
+
492
+ const iter1 = sheet1[Symbol.asyncIterator]();
493
+ const iter2 = sheet2[Symbol.asyncIterator]();
494
+
495
+ while (true) {
496
+ const [res1, res2] = await Promise.all([iter1.next(), iter2.next()]);
497
+
498
+ if (res1.done && res2.done) break;
499
+
500
+ const row1 = res1.value?.values?.slice(1) || [];
501
+ const row2 = res2.value?.values?.slice(1) || [];
502
+
503
+ if (res1.value || res2.value) {
504
+ rowsRead++;
505
+ }
506
+
507
+ const rowNumber = res1.value?.number || res2.value?.number || '';
508
+ this._compareAndLogRowDifference(row1, row2, rowNumber, sheet1.name, allSheets, logLabel, logger, failures);
509
+ }
510
+
511
+ if (!sheetName1 && !allSheets) {
512
+ break;
513
+ }
514
+ }
515
+
516
+ // If sheet found but nothing read -> try fix
517
+ if (sheetFound && rowsRead === 0 && !retryCount && !fixedFilePath1 && !fixedFilePath2) {
518
+ logger.log(logLabel, 'Sheets found but no rows were read. Attempting to fix files...');
519
+ try {
520
+ const newFixedPath1 = await this._fixFileWithXlsxAndSleep(filePath1, logLabel, logger);
521
+ const newFixedPath2 = await this._fixFileWithXlsxAndSleep(filePath2, logLabel, logger);
522
+ const result = await this.compareExcelFiles(
523
+ newFixedPath1,
524
+ newFixedPath2,
525
+ sheetName1,
526
+ sheetName2,
527
+ allSheets,
528
+ logLabel,
529
+ 1,
530
+ newFixedPath1,
531
+ newFixedPath2
532
+ );
533
+ this._cleanupTempFile(newFixedPath1);
534
+ this._cleanupTempFile(newFixedPath2);
535
+ return result;
536
+ } catch (fixError) {
537
+ logger.log(logLabel, `File fix attempt failed: ${fixError.message}`);
538
+ }
539
+ }
540
+
541
+ if (!sheetFound) {
542
+ const message = sheetName1
543
+ ? `Sheet with name ${sheetName1} not found!`
544
+ : 'No sheet found in the first file!';
545
+ logger.failureLog(logLabel);
546
+ throw { message };
547
+ }
548
+
549
+ if (failures.length) {
550
+ logger.failureLog(logLabel);
551
+ throw { message: `There are ${failures.length} rows that do not match in the 2 files!` };
552
+ }
553
+ } catch (err) {
554
+ const message = err?.message || '';
555
+
556
+ if (!retryCount && message.includes('invalid signature: 0x4a4')) {
557
+ const newPath1 = await this.extractAndRewriteCleanXlsx(filePath1, logLabel);
558
+ await sleep(1000);
559
+ const newPath2 = await this.extractAndRewriteCleanXlsx(filePath2, logLabel);
560
+ await sleep(1000);
561
+ return await this.compareExcelFiles(
562
+ newPath1,
563
+ newPath2,
564
+ sheetName1,
565
+ sheetName2,
566
+ allSheets,
567
+ logLabel,
568
+ 1,
569
+ fixedFilePath1,
570
+ fixedFilePath2
571
+ );
572
+ }
573
+
574
+ if (this._isSheetsReadError(err)) {
575
+ if (!retryCount && !fixedFilePath1 && !fixedFilePath2) {
576
+ logger.log(logLabel, 'Error reading sheets. Attempting to fix files...');
577
+ try {
578
+ const newFixedPath1 = await this._fixFileWithXlsxAndSleep(filePath1, logLabel, logger);
579
+ const newFixedPath2 = await this._fixFileWithXlsxAndSleep(filePath2, logLabel, logger);
580
+ const result = await this.compareExcelFiles(
581
+ newFixedPath1,
582
+ newFixedPath2,
583
+ sheetName1,
584
+ sheetName2,
585
+ allSheets,
586
+ logLabel,
587
+ 1,
588
+ newFixedPath1,
589
+ newFixedPath2
590
+ );
591
+ this._cleanupTempFile(newFixedPath1);
592
+ this._cleanupTempFile(newFixedPath2);
593
+ return result;
594
+ } catch (fixError) {
595
+ logger.log(
596
+ logLabel,
597
+ `File fix attempt failed: ${fixError.message}. Retrying with original files...`
598
+ );
599
+ }
600
+ }
601
+
602
+ if (!retryCount || retryCount < 4) {
603
+ console.log('Error in iterating sheets. Retrying...', message);
604
+ await sleep(5000);
605
+ return await this.compareExcelFiles(
606
+ filePath1,
607
+ filePath2,
608
+ sheetName1,
609
+ sheetName2,
610
+ allSheets,
611
+ logLabel,
612
+ (retryCount || 0) + 1,
613
+ fixedFilePath1,
614
+ fixedFilePath2
615
+ );
616
+ }
617
+
618
+ throw err;
619
+ }
620
+
621
+ throw err;
622
+ }
623
+ }
624
+
625
+ async getMatchingSheet(filePath, targetSheetName, retry, fixedFilePath) {
626
+ try {
627
+ const workbookReader = new Excel.stream.xlsx.WorkbookReader(filePath);
628
+ let sheetFound = false;
629
+
630
+ for await (const sheet of workbookReader) {
631
+ if (sheet.name === targetSheetName) {
632
+ sheetFound = true;
633
+ let columnCount = sheet.dimensions?.model?.right || 0;
634
+
635
+ if (columnCount === 0 && !retry && !fixedFilePath) {
636
+ try {
637
+ const newFixedPath = await this._fixFileWithXlsxAndSleep(
638
+ filePath,
639
+ `Finding sheet ${targetSheetName}`,
640
+ this._getLogger(`Finding sheet ${targetSheetName}`)
641
+ );
642
+ return await this.getMatchingSheet(newFixedPath, targetSheetName, 1, newFixedPath);
643
+ } catch {
644
+ // fallback to original
645
+ }
646
+ }
647
+
648
+ return sheet;
649
+ }
650
+ }
651
+
652
+ if (!sheetFound && !retry && !fixedFilePath) {
653
+ try {
654
+ const newFixedPath = await this._fixFileWithXlsxAndSleep(
655
+ filePath,
656
+ `Finding sheet ${targetSheetName}`,
657
+ this._getLogger(`Finding sheet ${targetSheetName}`)
658
+ );
659
+ return await this.getMatchingSheet(newFixedPath, targetSheetName, 1, newFixedPath);
660
+ } catch {
661
+ // return null below
662
+ }
663
+ }
664
+ } catch (er) {
665
+ console.log('Error reading matching sheet!', er.message);
666
+ if ((!retry || retry < 2) && this._isSheetsReadError(er)) {
667
+ console.log('Retrying find matching sheet on iterator error...', er.message);
668
+ await sleep(5000);
669
+ return await this.getMatchingSheet(filePath, targetSheetName, (retry || 0) + 1, fixedFilePath);
670
+ } else if (!retry && !fixedFilePath && !this._isSheetsReadError(er)) {
671
+ try {
672
+ const newFixedPath = await this._fixFileWithXlsxAndSleep(
673
+ filePath,
674
+ `Finding sheet ${targetSheetName}`,
675
+ this._getLogger(`Finding sheet ${targetSheetName}`)
676
+ );
677
+ return await this.getMatchingSheet(newFixedPath, targetSheetName, 1, newFixedPath);
678
+ } catch (fixError) {
679
+ console.log(`Not able to find sheet ${targetSheetName}`, er);
680
+ throw er;
681
+ }
682
+ } else {
683
+ console.log(`Not able to find sheet ${targetSheetName}`, er);
684
+ throw er;
685
+ }
686
+ }
687
+
688
+ return null;
689
+ }
690
+
691
+ // ---------- Advanced read (modularized) ----------
692
+
693
+ async advanced_excel_read(params) {
694
+ const logLabel = 'Excel.Advanced Read';
695
+ const logger = this._getLogger(logLabel);
696
+ logger.startLog(logLabel);
697
+
698
+ if (!params.filePath?.trim()) {
699
+ logger.failureLog(logLabel);
700
+ throw { message: 'No file path provided!' };
701
+ }
702
+
703
+ const filePath = this._ensureFileExists(params.filePath, logger, logLabel);
704
+ const sheetName = params.sheetName?.trim() || undefined;
705
+ const ignore = getBoolean(params.ignoreMissingData);
706
+ const isFirstRowHeader = getBoolean(params.isFirstRowHeader);
707
+ const getRowsAsObjects = getBoolean(params.getRowsAsObjects);
708
+
709
+ // Read header row once
710
+ const sheetData = await this.readSheetAsArray(filePath, sheetName, logLabel, 1);
711
+ const header = sheetData?.at(0) || [];
712
+ const firstRowHeader = isFirstRowHeader && sheetData?.length > 0;
713
+
714
+ let fields = firstRowHeader
715
+ ? header.map(field => field.trim())
716
+ : header.map((_h, index) => index);
717
+
718
+ const allFields = fields.slice();
719
+
720
+ // Filter rows
721
+ const rowsFilter = params.rows || 'all';
722
+ const dataRows = await this._readRowsWithFilter({
723
+ filePath,
724
+ sheetName,
725
+ logLabel,
726
+ rowsFilter,
727
+ ignore,
728
+ isFirstRowHeader
729
+ });
730
+
731
+ // Filter columns
732
+ fields = new TableUtils(this.caller, this.blacklistedLogs, logLabel).filterColumns(
733
+ params,
734
+ fields,
735
+ ignore,
736
+ firstRowHeader
737
+ );
738
+
739
+ const dataObject = this._buildDataObject({
740
+ dataRows,
741
+ fields,
742
+ allFields,
743
+ getRowsAsObjects,
744
+ firstRowHeader
745
+ });
746
+
747
+ logger.endLog(logLabel);
748
+ return dataObject;
749
+ }
750
+
751
+ async _readRowsWithFilter({
752
+ filePath,
753
+ sheetName,
754
+ logLabel,
755
+ rowsFilter,
756
+ ignore,
757
+ isFirstRowHeader
758
+ }) {
759
+ const filter = rowsFilter?.toString().trim().toLowerCase() || 'all';
760
+
761
+ const readAll = () =>
762
+ this.readSheetAsArray(filePath, sheetName, logLabel, undefined, isFirstRowHeader);
763
+
764
+ if (filter === 'all') {
765
+ return await readAll();
766
+ }
767
+
768
+ if (filter.startsWith('last')) {
769
+ const countStr = filter.replace('last', '').trim();
770
+ if (!isNumber(countStr)) {
771
+ throw { message: `${rowsFilter} is not a valid rows filter!` };
772
+ }
773
+ const count = parseInt(countStr, 10);
774
+ let dataRows = await this.readSheetAsArray(
775
+ filePath,
776
+ sheetName,
777
+ logLabel,
778
+ undefined,
779
+ isFirstRowHeader
780
+ );
781
+ const data = getLastElements(dataRows, count, ignore);
782
+ if (!data.success) throw { message: data.error };
783
+ return data.data;
784
+ }
785
+
786
+ if (filter.startsWith('first')) {
787
+ const countStr = filter.replace('first', '').trim();
788
+ if (!isNumber(countStr)) {
789
+ throw { message: `${rowsFilter} is not a valid rows filter!` };
790
+ }
791
+ const count = parseInt(countStr, 10);
792
+ let dataRows = await this.readSheetAsArray(
793
+ filePath,
794
+ sheetName,
795
+ logLabel,
796
+ count,
797
+ isFirstRowHeader
798
+ );
799
+ const data = getFirstElements(dataRows, count, ignore);
800
+ if (!data.success) throw { message: data.error };
801
+ return data.data;
802
+ }
803
+
804
+ if (filter.startsWith('any')) {
805
+ const countStr = filter.replace('any', '').trim();
806
+ if (!isNumber(countStr)) {
807
+ throw { message: `${rowsFilter} is not a valid rows filter!` };
808
+ }
809
+ const count = parseInt(countStr, 10);
810
+ let dataRows = await this.readSheetAsArray(
811
+ filePath,
812
+ sheetName,
813
+ logLabel,
814
+ count + 10, // now correctly numeric
815
+ isFirstRowHeader
816
+ );
817
+ const data = getAnyElements(dataRows, count, ignore);
818
+ if (!data.success) throw { message: data.error };
819
+ return data.data;
820
+ }
821
+
822
+ if (filter.includes('-')) {
823
+ const [, y] = filter.split('-').map(Number);
824
+ let dataRows = await this.readSheetAsArray(
825
+ filePath,
826
+ sheetName,
827
+ logLabel,
828
+ y + 10,
829
+ isFirstRowHeader
830
+ );
831
+ const data = getElementsInRange(dataRows, filter, ignore);
832
+ if (!data.success) throw { message: data.error };
833
+ return data.data;
834
+ }
835
+
836
+ if (filter.includes(',')) {
837
+ const indexArray = filter.split(',').map(Number);
838
+ const max = Math.max(...indexArray);
839
+ let dataRows = await this.readSheetAsArray(
840
+ filePath,
841
+ sheetName,
842
+ logLabel,
843
+ max + 10,
844
+ isFirstRowHeader
845
+ );
846
+ const data = getElementsAtIndices(dataRows, filter, ignore);
847
+ if (!data.success) throw { message: data.error };
848
+ return data.data;
849
+ }
850
+
851
+ // single index
852
+ if (!isNumber(filter)) {
853
+ throw { message: `${rowsFilter} is not a valid rows filter!` };
854
+ }
855
+
856
+ const index = parseInt(filter.trim(), 10);
857
+ let dataRows = await this.readSheetAsArray(
858
+ filePath,
859
+ sheetName,
860
+ logLabel,
861
+ index + 2,
862
+ isFirstRowHeader
863
+ );
864
+
865
+ if (index < 0 || (index >= dataRows.length && !ignore)) {
866
+ throw { message: `${rowsFilter} - index is invalid. Data size - ${dataRows.length}` };
867
+ }
868
+
869
+ if (ignore && index >= dataRows.length) {
870
+ return [];
871
+ }
872
+
873
+ return [dataRows[index]];
874
+ }
875
+
876
+ _buildDataObject({ dataRows, fields, allFields, getRowsAsObjects, firstRowHeader }) {
877
+ return dataRows.map(cells => {
878
+ const row = getRowsAsObjects ? {} : [];
879
+ for (let i = 0; i < allFields.length; i++) {
880
+ const skip = firstRowHeader ? !fields.includes(allFields[i]) : !fields.includes(i);
881
+ if (skip) continue;
882
+ const val = i < cells.length ? cells[i] || '' : '';
883
+ if (getRowsAsObjects) {
884
+ const fieldName = firstRowHeader ? allFields[i] : `column_${i}`;
885
+ row[fieldName] = val;
886
+ } else {
887
+ row.push(val);
888
+ }
889
+ }
890
+ return row;
891
+ });
892
+ }
893
+
894
+ // ---------- Repair via external tool ----------
895
+
896
+ async extractAndRewriteCleanXlsx(filePath, parentLogLabel) {
897
+ const logLabel = `${parentLogLabel} :: Excel file ${filePath} is not properly formatted, fixing the file!`;
898
+ const logger = this._getLogger(logLabel);
899
+ logger.startLog(logLabel);
900
+
901
+ const inputPath = this.fileUtils.getFilePath(filePath);
902
+
903
+ if (!this.dependenciesPath) {
904
+ logger.failureLog(logLabel);
905
+ throw {
906
+ message: 'Dependencies path is not defined! Please set it in the config file.'
907
+ };
908
+ }
909
+
910
+ const result = await processExcel(this.dependenciesPath, inputPath);
911
+
912
+ if (result.status !== 0) {
913
+ logger.failureLog(logLabel);
914
+ throw { message: result.message };
915
+ }
916
+
917
+ logger.endLog(logLabel);
918
+ return result.message;
919
+ }
920
+ }
921
+
922
+ exports.ExcelUtils = ExcelUtils;