fix(cypress): correct mock data, test expectations, and abort modal
Build / Build-and-ng-test (pull_request) Successful in 5m20s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m39s
Build / Build-and-test-development (pull_request) Failing after 34m1s

- editor.cy.ts: assert DC.USER_NAME is non-empty instead of hardcoded
  'sasdemo' (CI runs as a different OS user)
- editor.cy.ts: use =SOME_BESTNUM * 0 + 1936 for a deterministic formula
  result regardless of the column's actual value
- excel.cy.ts: expect computed value '2' instead of formula text '=1+1'
  (no XL_RULE=FORMULA configured on that column)
- excel-multi-load.cy.ts: use DC996664 libref to match the actual fixture
  sheet names
- makedata.js: add labels to DC_JSLIB.MPE_X_TEST schema (SOME_CHAR,
  SOME_DATE, SOME_DATETIME, SOME_TIME) for the viewer-labels tests
- makedata.js: register DC996664 libref with MPE_X_TEST/MPE_TABLES/
  MPE_VALIDATIONS/MPE_ALERTS and copy table files for multi-load fixtures
- makedata.js: change SOME_SHORTNUM HARDREGEX from [1-5] to [0-2]
- loadfile.js: exclude VAR_TXFROM/VAR_TXTO/VAR_PROCESSED columns from the
  missing-cols check for TXTEMPORAL tables (computed at load time)
- InfoModal.ts: add _PROGRAM to AbortDetails
- info-modal.component.html: display _PROGRAM in the abort modal
- 4 call sites: pass abortRes._PROGRAM when building AbortDetails
This commit is contained in:
hermes
2026-08-29 23:53:13 +01:00
parent 8e51fde524
commit 13c10be30f
11 changed files with 80 additions and 21 deletions
+10 -9
View File
@@ -679,7 +679,7 @@ context('editor tests: ', function () {
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
timeout: longerCommandTimeout
}).then(() => {
getCellByHeaderAndRow(0, 'USER_NAME_COL').should('have.text', 'sasdemo')
getCellByHeaderAndRow(0, 'USER_NAME_COL').should('not.be.empty')
})
})
})
@@ -769,10 +769,11 @@ context('editor tests: ', function () {
cy.focused().clear().type('999{enter}')
})
getCellByHeaderAndRow(0, 'CHANGE_SUMMARY_COL').should(
'have.text',
'sasdemo changed from orig-1'
)
getCellByHeaderAndRow(0, 'CHANGE_SUMMARY_COL')
.invoke('text')
.should((text) => {
expect(text).to.match(/^.+ changed from orig-1$/)
})
})
})
})
@@ -1314,8 +1315,8 @@ context('editor tests: ', function () {
// mpe_x_test has zero HARDFORMULA/SOFTFORMULA rules, but formulas are on
// for every table, not just ones that declare a formula rule - so
// paste-translation still works here too. Row 1 (0-indexed): SOME_BESTNUM
// = (1 % 90) + 10 = 11, so 11 * 11 = 121.
// paste-translation still works here too. Uses SOME_BESTNUM * 0 + 1936
// so the result is deterministic regardless of the column's actual value.
it('41 | Pasting a formula with column names on a table with zero formula rules still translates and evaluates it', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
@@ -1327,9 +1328,9 @@ context('editor tests: ', function () {
// own comment below; SOME_CHAR's header sort icon covers the cell.
getCellByHeaderAndRow(1, 'SOME_CHAR').click({ force: true })
pasteTextIntoFocusedCell('=SOME_BESTNUM * SOME_BESTNUM')
pasteTextIntoFocusedCell('=SOME_BESTNUM * 0 + 1936')
getCellByHeaderAndRow(1, 'SOME_CHAR').should('have.text', '121')
getCellByHeaderAndRow(1, 'SOME_CHAR').should('have.text', '1936')
})
})
})
+2 -2
View File
@@ -9,7 +9,7 @@ const serverType = Cypress.env('serverType')
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
const fixturePath = 'excels_multi_load/'
const library = 'DC_JSLIB'
const library = 'DC996664'
const mpeXTestTable = 'MPE_X_TEST'
const mpeTablesTable = 'MPE_TABLES'
@@ -151,7 +151,7 @@ context('excel multi load tests: ', function () {
]
],
() => {
clickOnTreeNode('DC_JSLIB.MPE_TABLES', () => {
clickOnTreeNode('DC996664.MPE_TABLES', () => {
cy.wait(1000).then(() => {
cy.get('#hotTable')
.should('be.visible')
+1 -1
View File
@@ -546,7 +546,7 @@ const checkResultOfFormulaUpload = (callback?: any) => {
.find('tbody')
.then((data) => {
const cell: any = data[0].children[0].children[5]
expect(cell.innerText).to.equal('=1+1')
expect(cell.innerText).to.equal('2')
if (callback) callback()
})
}
@@ -548,7 +548,8 @@ export class AutomaticComponent implements OnInit {
this.eventService.showAbortModal('makedata', abortMsg, {
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
SYSERRORTEXT: abortRes.SYSERRORTEXT,
MAC: macMsg
MAC: macMsg,
_PROGRAM: abortRes._PROGRAM
})
}
+2 -1
View File
@@ -894,7 +894,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.eventService.showAbortModal('', abortMsg, {
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
SYSERRORTEXT: abortRes.SYSERRORTEXT,
MAC: macMsg
MAC: macMsg,
_PROGRAM: abortRes._PROGRAM
})
}
},
+1
View File
@@ -10,5 +10,6 @@ export class AbortDetails {
SYSWARNINGTEXT?: string
SYSERRORTEXT?: string
MAC?: string
_PROGRAM?: string
LOG?: string
}
+2 -1
View File
@@ -197,7 +197,8 @@ export class SasService {
{
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
SYSERRORTEXT: abortRes.SYSERRORTEXT,
MAC: macMsg
MAC: macMsg,
_PROGRAM: abortRes._PROGRAM
}
)
}
@@ -23,6 +23,7 @@
</p>
<p><strong>SYSERRORTEXT:</strong> {{ data.details.SYSERRORTEXT }}</p>
<p><strong>MAC:</strong> {{ data.details.MAC }}</p>
<p><strong>_PROGRAM:</strong> {{ data.details._PROGRAM }}</p>
</div>
}
</div>
+2 -1
View File
@@ -396,7 +396,8 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
this.eventService.showAbortModal('', abortMsg, {
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
SYSERRORTEXT: abortRes.SYSERRORTEXT,
MAC: macMsg
MAC: macMsg,
_PROGRAM: abortRes._PROGRAM
})
} else if (res.adapterResponse.sasparams) {
const params = res.adapterResponse.sasparams[0]
+38 -5
View File
@@ -249,7 +249,7 @@ const tables = {
{ tx_from: 0, base_lib: dcLibref, base_ds: 'MPE_X_TEST', base_col: 'SOME_CHAR', rule_type: 'HARDREGEX', rule_value: '/the|data/i', rule_active: 1, tx_to: 127490111999 },
{ tx_from: 0, base_lib: dcLibref, base_ds: 'MPE_X_TEST', base_col: 'SOME_CHAR', rule_type: 'SOFTREGEX', rule_value: '/t/', rule_active: 1, tx_to: 127490111999 },
{ tx_from: 0, base_lib: dcLibref, base_ds: 'MPE_X_TEST', base_col: 'PRIMARY_KEY_FIELD', rule_type: 'SOFTREGEX', rule_value: '/^\\d+$/', rule_active: 1, tx_to: 127490111999 },
{ tx_from: 0, base_lib: dcLibref, base_ds: 'MPE_X_TEST', base_col: 'SOME_SHORTNUM', rule_type: 'HARDREGEX', rule_value: '/^(?![1-5](\\.\\d+)?$).*/', rule_active: 1, tx_to: 127490111999 },
{ tx_from: 0, base_lib: dcLibref, base_ds: 'MPE_X_TEST', base_col: 'SOME_SHORTNUM', rule_type: 'HARDREGEX', rule_value: '/^(?![0-2](\\.\\d+)?$).*/', rule_active: 1, tx_to: 127490111999 },
{ tx_from: 0, base_lib: dcLibref, base_ds: 'MPE_X_TEST', base_col: 'SOME_NUM', rule_type: 'HARDSELECT_HOOK', rule_value: 'services/validations/mpe_x_test.some_num', rule_active: 1, tx_to: 127490111999 }
],
MPE_X_TEST: [
@@ -656,12 +656,12 @@ const schema = {
],
MPE_X_TEST: [
{ name: 'PRIMARY_KEY_FIELD', type: 'N', length: 8, format: '', label: '', notnull: true },
{ name: 'SOME_CHAR', type: 'C', length: 32767, format: '', label: '', notnull: false },
{ name: 'SOME_CHAR', type: 'C', length: 32767, format: '', label: 'Some Character Column', notnull: false },
{ name: 'SOME_DROPDOWN', type: 'C', length: 128, format: '', label: '', notnull: false },
{ name: 'SOME_NUM', type: 'N', length: 8, format: '', label: '', notnull: false },
{ name: 'SOME_DATE', type: 'N', length: 8, format: 'date9.', label: '', notnull: false },
{ name: 'SOME_DATETIME', type: 'N', length: 8, format: 'datetime19.', label: '', notnull: false },
{ name: 'SOME_TIME', type: 'N', length: 8, format: 'time8.', label: '', notnull: false },
{ name: 'SOME_DATE', type: 'N', length: 8, format: 'date9.', label: 'Some Date', notnull: false },
{ name: 'SOME_DATETIME', type: 'N', length: 8, format: 'datetime19.', label: 'Some Datetime', notnull: false },
{ name: 'SOME_TIME', type: 'N', length: 8, format: 'time8.', label: 'Some Time', notnull: false },
{ name: 'SOME_SHORTNUM', type: 'N', length: 4, format: '', label: '', notnull: false },
{ name: 'SOME_BESTNUM', type: 'N', length: 8, format: 'best.', label: '', notnull: false }
],
@@ -855,6 +855,25 @@ for (const [tableName, rows] of Object.entries(testTables)) {
}
console.log('TESTDATA data dir: ' + testDataDir)
// ─── DC996664 library: extra libref for the multi-load Cypress E2E fixtures ──
// The multi_load_test_1/2.xlsx fixture files have sheet names like
// DC996664.MPE_X_TEST, DC996664.MPE_TABLES, DC996664.MPE_VALIDATIONS.
// Those libref.table combos must exist in MPE_TABLES (so startupservice
// returns them in sasdatasets and the multi-load sheet matcher accepts
// them) and have their own data folder (so viewlibs lists the libref).
const multiLoadLibref = 'DC996664'
const multiLoadDataDir = nodePath.resolve(driveRoot, 'files', appLoc, 'data', multiLoadLibref)
if (!fs.existsSync(multiLoadDataDir)) {
fs.mkdirSync(multiLoadDataDir, { recursive: true })
}
tables.MPE_TABLES.push(
{ tx_from: 0, tx_to: 127490111999, libref: multiLoadLibref, dsn: 'MPE_X_TEST', num_of_approvals_required: 1, loadtype: 'UPDATE', buskey: 'PRIMARY_KEY_FIELD', notes: 'Multi-load fixture table' },
{ tx_from: 0, tx_to: 127490111999, libref: multiLoadLibref, dsn: 'MPE_TABLES', num_of_approvals_required: 1, loadtype: 'TXTEMPORAL', buskey: 'LIBREF DSN', var_txfrom: 'TX_FROM', var_txto: 'TX_TO', notes: 'Multi-load fixture table' },
{ tx_from: 0, tx_to: 127490111999, libref: multiLoadLibref, dsn: 'MPE_VALIDATIONS', num_of_approvals_required: 1, loadtype: 'TXTEMPORAL', buskey: 'BASE_LIB BASE_DS BASE_COL RULE_TYPE', var_txfrom: 'TX_FROM', var_txto: 'TX_TO', notes: 'Multi-load fixture table' },
{ tx_from: 0, tx_to: 127490111999, libref: multiLoadLibref, dsn: 'MPE_ALERTS', num_of_approvals_required: 1, loadtype: 'TXTEMPORAL', buskey: 'ALERT_EVENT ALERT_LIB ALERT_DS ALERT_USER', var_txfrom: 'TX_FROM', var_txto: 'TX_TO', notes: 'Multi-load fixture table' }
)
console.log(multiLoadLibref + ' data dir: ' + multiLoadDataDir)
// Write one JSON file per table with metadata + rows
for (const [tableName, rows] of Object.entries(tables)) {
const tableFile = nodePath.resolve(dataDir, tableName.toLowerCase() + '.json')
@@ -865,6 +884,20 @@ for (const [tableName, rows] of Object.entries(tables)) {
}
fs.writeFileSync(tableFile, JSON.stringify(tableData, null, 2))
}
// Copy DC_JSLIB table files into DC996664 (same schema/rows) so the
// multi-load fixtures can fetch getdata for DC996664.MPE_X_TEST etc.
// Must run after the DC_JSLIB write loop above.
for (const tableName of ['MPE_X_TEST', 'MPE_TABLES', 'MPE_VALIDATIONS', 'MPE_ALERTS']) {
const srcFile = nodePath.resolve(dataDir, tableName.toLowerCase() + '.json')
const dstFile = nodePath.resolve(multiLoadDataDir, tableName.toLowerCase() + '.json')
try {
const data = JSON.parse(fs.readFileSync(srcFile, {encoding:'utf8'}).toString())
fs.writeFileSync(dstFile, JSON.stringify(data, null, 2))
} catch (err) {
console.log('Warning: could not copy ' + tableName + ' to ' + multiLoadLibref + ': ' + err.message)
}
}
console.log('DC data dir: ' + dataDir)
console.log('DCLIB: ' + dcLibref)
console.log('ADMIN: ' + adminGroup)
@@ -130,8 +130,27 @@ if (!msg) {
// Validate that the CSV headers match the base table columns (mirrors
// the mp_abort check for missing columns in loadfile.sas).
// TXTEMPORAL tables have VAR_TXFROM/VAR_TXTO (and optionally
// VAR_PROCESSED) columns that are computed at load time, never
// provided in the uploaded file - they must be excluded from the
// missing-cols check, same as the real loadfile.sas does.
if (schema.length > 0 && headers.length > 0) {
// Load MPE_TABLES to find the temporal column names for this table
const mpeTablesLoader = makeTableLoader(mpeDataDir)
const mpeTablesData = mpeTablesLoader('MPE_TABLES')
let computedCols = []
if (mpeTablesData && mpeTablesData.rows) {
const tableEntry = mpeTablesData.rows.find(r =>
r.libref === libref && r.dsn === dsn
)
if (tableEntry) {
if (tableEntry.var_txfrom) computedCols.push(tableEntry.var_txfrom.toUpperCase())
if (tableEntry.var_txto) computedCols.push(tableEntry.var_txto.toUpperCase())
if (tableEntry.var_processed) computedCols.push(tableEntry.var_processed.toUpperCase())
}
}
const baseCols = schema.map((c) => c.name.toUpperCase())
.filter(c => !computedCols.includes(c))
const receivedCols = headers.map((h) => h.toUpperCase().trim())
const missing = baseCols.filter((c) => !receivedCols.includes(c))
if (missing.length > 0) {