- Paste a formula using column names (e.g. "=A_COL * B_COL") into any
cell and have it translated to that row's cell references
("=B4 * C4") so HyperFormula can evaluate it - covers both a
grid-level paste and pasting directly into an open cell editor
- Only applies on tables that already have formulas enabled (an
existing HARDFORMULA/SOFTFORMULA column); otherwise the pasted text
is left untouched rather than becoming inert translated text
- New substituteColumnReferences() reuses parseFormulaRule's
boundary-matching helpers but deliberately skips DC.* variable
substitution, which only makes sense for admin-defined rule values
1669 lines
61 KiB
TypeScript
1669 lines
61 KiB
TypeScript
const username = Cypress.env('username')
|
|
const password = Cypress.env('password')
|
|
const hostUrl = Cypress.env('hosturl')
|
|
const appLocation = Cypress.env('appLocation')
|
|
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
|
const serverType = Cypress.env('serverType')
|
|
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
|
const fixturePath = 'excels_general/'
|
|
|
|
context('editor tests: ', function () {
|
|
this.beforeAll(() => {
|
|
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
cy.loginAndUpdateValidKey()
|
|
})
|
|
|
|
this.beforeEach(() => {
|
|
cy.visit(hostUrl + appLocation)
|
|
|
|
visitPage('home')
|
|
})
|
|
|
|
it('1 | Submits duplicate primary keys', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
|
|
|
attachExcelFile('MPE_DATADICTIONARY_duplicate_keys.xlsx', () => {
|
|
clickOnUploadPreview(() => {
|
|
confirmEditPreviewFile(() => {
|
|
submitTable(() => {
|
|
cy.get('.modal-body').then((modalBody: any) => {
|
|
if (modalBody[0].innerText.includes(`Duplicates found:`)) {
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('2 | Submits null cells which must not be null', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
cy.get(rows[1].childNodes[2])
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('{enter}')
|
|
.then(() => {
|
|
submitTable(() => {
|
|
cy.get('.modal-body').then((modalBody: any) => {
|
|
if (
|
|
modalBody[0].innerHTML
|
|
.toLowerCase()
|
|
.includes(`invalid values are present`)
|
|
) {
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('3 | Gets basic dynamic cell validation', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
cy.get(rows[1].childNodes[5])
|
|
.click({ force: true })
|
|
.then(($td) => {
|
|
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
|
'exist'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('4 | Gets advanced dynamic cell validation', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_tables')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
cy.get(rows[1].childNodes[3])
|
|
.click({ force: true })
|
|
.then(($td) => {
|
|
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
|
'exist'
|
|
)
|
|
cy.get('.htAutocompleteArrow', {
|
|
withinSubject: rows[1].childNodes[7]
|
|
}).should('exist')
|
|
cy.get('.htAutocompleteArrow', {
|
|
withinSubject: rows[1].childNodes[8]
|
|
}).should('exist')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// An invalid cell shouldn't block submission if its row
|
|
// is marked for delete
|
|
it('5 | Submits a delete-marked row with an invalid cell', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
// childNodes[5] = SOME_NUM (numeric) — feed it a non-numeric value
|
|
// to trigger HOT's native invalid mark.
|
|
cy.get(rows[1].childNodes[5])
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('not a number{enter}')
|
|
.then(() => {
|
|
// childNodes[1] = _____DELETE__THIS__RECORD_____
|
|
setDeleteFlag(rows[1].childNodes[1], 'Yes', () => {
|
|
submitTable(() => {
|
|
// Validation passed despite the invalid SOME_NUM cell:
|
|
// the confirm-submit modal's Submit button is enabled
|
|
// (validationDone === 1), not the "invalid values" abort.
|
|
cy.get('#submitBtn', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
.then(() => done())
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// The delete exemption must not affect the uniqueness (primary key) check.
|
|
it('6 | Still blocks a duplicate primary key even on a delete-marked row', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
cy.get(rows[0].childNodes[2]).then((firstPk) => {
|
|
const duplicateValue = firstPk.text()
|
|
|
|
// childNodes[2] = PRIMARY_KEY_FIELD
|
|
cy.get(rows[1].childNodes[2])
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type(`${duplicateValue}{enter}`)
|
|
.then(() => {
|
|
setDeleteFlag(rows[1].childNodes[1], 'Yes', () => {
|
|
submitTable()
|
|
|
|
cy.get('.duplicate-keys-modal', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.should('exist')
|
|
.then(() => done())
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// REGEX_HARD_COL/REGEX_SOFT_COL only exist un-stripped on MPE_X_NEW (see
|
|
// RULE_DEMO_COLS in the getdata.js mock) — MPE_X_TEST stays clean to
|
|
// match the excel fixtures.
|
|
it('7 | Blocks submission of a value that fails HARDREGEX', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
getCellByHeaderAndRow(1, 'REGEX_HARD_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('not-an-email{enter}')
|
|
.then(() => {
|
|
getCellByHeaderAndRow(1, 'REGEX_HARD_COL').should(
|
|
'have.attr',
|
|
'title',
|
|
'REGEX: /[\\w.]+@[\\w]+\\.[a-z]{2,}/'
|
|
)
|
|
|
|
submitTable(() => {
|
|
cy.get('.modal-body').then((modalBody: any) => {
|
|
if (
|
|
modalBody[0].innerHTML
|
|
.toLowerCase()
|
|
.includes(`invalid values are present`)
|
|
) {
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('8 | Warns (but still submits) a value that fails SOFTREGEX', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('not a postcode{enter}')
|
|
.then(() => {
|
|
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL')
|
|
.should('have.class', 'dc-warning-cell')
|
|
.and(
|
|
'have.attr',
|
|
'title',
|
|
'REGEX: /[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
|
|
)
|
|
|
|
submitTable(() => {
|
|
// Validation passed despite the SOFTREGEX warning: the
|
|
// confirm-submit modal's Submit button is enabled
|
|
// (validationDone === 1), not the "invalid values" abort.
|
|
cy.get('#submitBtn', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
.then(() => done())
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// MPE_X_FORMULA_TEST is a small, dedicated fixture for formula testing
|
|
// (kept separate from MPE_X_NEW, which carries no formula columns at
|
|
// all) - row index 1 (0-indexed, i=1 in the mock's row generator) has
|
|
// A_COL=2, B_COL=10: HARDFORMULA (A_COL * B_COL) computes to 20,
|
|
// SOFTFORMULA (A_COL + B_COL) computes to 12.
|
|
it('9 | HARDFORMULA shows the computed value and blocks direct edits', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(1, 'FORMULA_HARD_COL').should('have.text', '20')
|
|
|
|
getCellByHeaderAndRow(1, 'FORMULA_HARD_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
// readOnly cells don't open an editor - nothing is focused to
|
|
// type into, so this is a no-op if the column is truly readonly.
|
|
cy.focused().type('9999{enter}')
|
|
|
|
getCellByHeaderAndRow(1, 'FORMULA_HARD_COL').should(
|
|
'have.text',
|
|
'20'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('10 | SOFTFORMULA shows the computed default but accepts an override', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL').should('have.text', '12')
|
|
|
|
getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('override{enter}')
|
|
|
|
getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL').should(
|
|
'have.text',
|
|
'override'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('11 | Info dropdown shows the applied formula as √x=<formula>', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
openColumnDropdown('FORMULA_HARD_COL')
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.include('√x=A_COL * B_COL')
|
|
})
|
|
cy.get('body').click(0, 0) // close menu
|
|
|
|
openColumnDropdown('FORMULA_SOFT_COL')
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.include('√x=A_COL + B_COL')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('12 | Info dropdown shows the applied HARDREGEX/SOFTREGEX pattern', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
openColumnDropdown('REGEX_HARD_COL')
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.include(
|
|
'HARDREGEX: /[\\w.]+@[\\w]+\\.[a-z]{2,}/'
|
|
)
|
|
})
|
|
cy.get('body').click(0, 0) // close menu
|
|
|
|
openColumnDropdown('REGEX_SOFT_COL')
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.include(
|
|
'SOFTREGEX: /[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('13 | Info dropdown shows only the applied HARDREGEX pattern when a column has both', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
openColumnDropdown('REGEX_BOTH_COL')
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
const text = $menu.text()
|
|
|
|
expect(text).to.include('HARDREGEX: /^[A-Z0-9_-]+$/')
|
|
expect(text).to.not.include('SOFTREGEX: /^.{5,10}$/')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('14 | REGEX_BOTH_COL: a HARDREGEX failure blocks submission and sets its own tooltip, not yellow', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
// 'bad value' fails HARDREGEX (lowercase + space) but is 9 chars,
|
|
// within SOFTREGEX's 5-10 range - isolates the hard-only failure.
|
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('bad value{enter}')
|
|
.then(() => {
|
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
|
.should('not.have.class', 'dc-warning-cell')
|
|
.and('have.attr', 'title', 'REGEX: /^[A-Z0-9_-]+$/')
|
|
|
|
submitTable(() => {
|
|
cy.get('.modal-body').then((modalBody: any) => {
|
|
if (
|
|
modalBody[0].innerHTML
|
|
.toLowerCase()
|
|
.includes(`invalid values are present`)
|
|
) {
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('15 | REGEX_BOTH_COL: SOFTREGEX is ignored entirely when HARDREGEX is present', (done) => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
scrollGridRight()
|
|
|
|
// 'AB' passes HARDREGEX (uppercase only) but fails SOFTREGEX (too
|
|
// short) - only one regex runs per column, so no warning is shown.
|
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type('AB{enter}')
|
|
.then(() => {
|
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
|
.should('not.have.class', 'dc-warning-cell')
|
|
.and('not.have.attr', 'title')
|
|
|
|
submitTable(() => {
|
|
cy.get('#submitBtn', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
.then(() => done())
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it("16 | Insert Row above keeps existing rows' formulas aligned with their own shifted data", () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
// Row index 2 (0-indexed): PRIMARY_KEY_FIELD=3, A_COL=3, B_COL=10 ->
|
|
// HARDFORMULA (A_COL*B_COL) = 30, SOFTFORMULA (A_COL+B_COL) = 13.
|
|
getCellByHeaderAndRow(2, 'PRIMARY_KEY_FIELD').should('have.text', '3')
|
|
|
|
insertRowViaContextMenu(2, 'Insert Row above')
|
|
|
|
// The new blank row lands at index 2 - its formula columns are
|
|
// seeded with their own row-relative formula (A_COL/B_COL are both
|
|
// still empty, so HyperFormula evaluates the arithmetic as 0).
|
|
getCellByHeaderAndRow(2, 'FORMULA_HARD_COL').should('have.text', '0')
|
|
getCellByHeaderAndRow(2, 'FORMULA_SOFT_COL').should('have.text', '0')
|
|
|
|
// The row that WAS at index 2 (PK=3) is now shifted down to index
|
|
// 3 - its formulas must recompute from ITS OWN data, not read
|
|
// whatever the engine had cached at index 3 before the insert
|
|
// (that would be PK=4's values: 40/14).
|
|
getCellByHeaderAndRow(3, 'PRIMARY_KEY_FIELD').should('have.text', '3')
|
|
getCellByHeaderAndRow(3, 'FORMULA_HARD_COL').should('have.text', '30')
|
|
getCellByHeaderAndRow(3, 'FORMULA_SOFT_COL').should('have.text', '13')
|
|
})
|
|
})
|
|
})
|
|
|
|
it("17 | Insert Row below keeps existing rows' formulas aligned with their own shifted data", () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
// Row index 3 (0-indexed): PRIMARY_KEY_FIELD=4, A_COL=4, B_COL=10 ->
|
|
// HARDFORMULA = 40, SOFTFORMULA = 14.
|
|
getCellByHeaderAndRow(3, 'PRIMARY_KEY_FIELD').should('have.text', '4')
|
|
|
|
// 'Insert Row below' on row index 2 (PK=3) inserts the new blank
|
|
// row at index 3, pushing the old index-3 row (PK=4) to index 4.
|
|
insertRowViaContextMenu(2, 'Insert Row below')
|
|
|
|
// The new blank row lands at index 3 - seeded the same way as
|
|
// above, evaluating to 0 while A_COL/B_COL are still empty.
|
|
getCellByHeaderAndRow(3, 'FORMULA_HARD_COL').should('have.text', '0')
|
|
getCellByHeaderAndRow(3, 'FORMULA_SOFT_COL').should('have.text', '0')
|
|
|
|
getCellByHeaderAndRow(4, 'PRIMARY_KEY_FIELD').should('have.text', '4')
|
|
getCellByHeaderAndRow(4, 'FORMULA_HARD_COL').should('have.text', '40')
|
|
getCellByHeaderAndRow(4, 'FORMULA_SOFT_COL').should('have.text', '14')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('18 | Insert Row seeds formulas that live-recalculate once A_COL/B_COL are filled in', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
insertRowViaContextMenu(0, 'Insert Row below')
|
|
|
|
// New row at index 1 - both formula columns evaluate the seeded
|
|
// formula (=C2*D2 / =C2+D2 in spreadsheet notation) against
|
|
// still-empty A_COL/B_COL, so HyperFormula treats them as 0.
|
|
getCellByHeaderAndRow(1, 'FORMULA_HARD_COL').should('have.text', '0')
|
|
getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL').should('have.text', '0')
|
|
|
|
getCellByHeaderAndRow(1, 'A_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().type('5{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(1, 'B_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().type('3{enter}')
|
|
})
|
|
|
|
// Both formula columns must recompute live from THIS row's own
|
|
// A_COL/B_COL - proving the seeded formula is genuinely wired to
|
|
// HyperFormula, not a one-time static snapshot.
|
|
getCellByHeaderAndRow(1, 'FORMULA_HARD_COL').should('have.text', '15')
|
|
getCellByHeaderAndRow(1, 'FORMULA_SOFT_COL').should('have.text', '8')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('19 | Row header shows blank for unchanged, ± for a modified row', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getRowHeaderSymbol(0).should('have.text', ' ')
|
|
getRowHeaderCell(0)
|
|
.should('not.have.class', 'rowStatusModified')
|
|
.and('not.have.class', 'rowStatusAdded')
|
|
.and('not.have.class', 'rowStatusDeleted')
|
|
|
|
getCellByHeaderAndRow(0, 'A_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('999{enter}')
|
|
})
|
|
|
|
getRowHeaderSymbol(0).should('have.text', '±')
|
|
getRowHeaderCell(0).should('have.class', 'rowStatusModified')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('20 | Row header shows - for a delete-marked row and + for a newly inserted row', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(2, 'Delete?').then(($cell) => {
|
|
setDeleteFlag($cell[0], 'Yes', () => {
|
|
getRowHeaderSymbol(2).should('have.text', '-')
|
|
getRowHeaderCell(2).should('have.class', 'rowStatusDeleted')
|
|
|
|
insertRowViaContextMenu(0, 'Insert Row below')
|
|
|
|
// New row lands at index 1, shifting the delete-marked row
|
|
// (was index 2) down to index 3 - its own status is unaffected
|
|
// by the shift, only its position.
|
|
getRowHeaderSymbol(1).should('have.text', '+')
|
|
getRowHeaderCell(1).should('have.class', 'rowStatusAdded')
|
|
getRowHeaderSymbol(3).should('have.text', '-')
|
|
getRowHeaderCell(3).should('have.class', 'rowStatusDeleted')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('21 | Inserting a row does not falsely mark unrelated rows as modified or revert an existing override', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(0, 'FORMULA_SOFT_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('999{enter}')
|
|
})
|
|
getRowHeaderSymbol(0).should('have.text', '±')
|
|
|
|
// Untouched row, well away from the insert point below.
|
|
getRowHeaderSymbol(5).should('have.text', ' ')
|
|
|
|
insertRowViaContextMenu(2, 'Insert Row below')
|
|
|
|
// The override and its '±' mark must survive the insert...
|
|
getCellByHeaderAndRow(0, 'FORMULA_SOFT_COL').should('have.text', '999')
|
|
getRowHeaderSymbol(0).should('have.text', '±')
|
|
|
|
// ...and a row that was never touched must stay unmarked, even
|
|
// though its position shifted (row 5 is now row 6).
|
|
getRowHeaderSymbol(6).should('have.text', ' ')
|
|
})
|
|
})
|
|
})
|
|
|
|
it("22 | DC.ROW_STATUS resolves to a live cell reference, reacting to this row's own edit status", () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
// dc.row_status itself is a purely client-synthesized column (see
|
|
// editStatusColumnRule.ts) - it must never render as a visible
|
|
// header, only exist as something DC.ROW_STATUS can point a cell
|
|
// reference at.
|
|
cy.get('.ht_clone_top .htCore thead tr th').should(($ths) => {
|
|
const texts = [...$ths].map((th) => th.innerText.trim())
|
|
expect(texts).not.to.include('dc.row_status')
|
|
})
|
|
|
|
getCellByHeaderAndRow(0, 'ROW_STATUS_COL').should('have.text', 'U')
|
|
|
|
getCellByHeaderAndRow(0, 'A_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('999{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(0, 'ROW_STATUS_COL').should('have.text', 'M')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('23 | DC.USER_NAME resolves to the current logged-in user', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(0, 'USER_NAME_COL').should('have.text', 'sasdemo')
|
|
})
|
|
})
|
|
})
|
|
|
|
it("24 | DC.ORIG_VALUE echoes this row's pre-edit value, and is blank for a newly-inserted row", () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(0, 'ORIG_VALUE_COL').should('have.text', 'orig-1')
|
|
|
|
insertRowViaContextMenu(0, 'Insert Row below')
|
|
|
|
// No dataSourceUnchanged match exists for a brand-new row, so
|
|
// DC.ORIG_VALUE falls back to blank rather than echoing anything.
|
|
getCellByHeaderAndRow(1, 'ORIG_VALUE_COL').should('have.text', '')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('25 | Row header symbols stay aligned with their own row after sorting by another column', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
// Mark row 2 (PRIMARY_KEY_FIELD=3) for delete before sorting, so
|
|
// there's a distinctive symbol ('-') to track across the reorder.
|
|
getCellByHeaderAndRow(2, 'Delete?').then(($cell) => {
|
|
setDeleteFlag($cell[0], 'Yes', () => {
|
|
getRowHeaderSymbol(2).should('have.text', '-')
|
|
|
|
sortByColumn('FORMULA_SOFT_COL')
|
|
|
|
// Wherever PRIMARY_KEY_FIELD=3 lands visually after sorting,
|
|
// its OWN row header must show '-' - not whatever symbol
|
|
// previously belonged to that visual position.
|
|
cy.get('.ht_clone_top .htCore thead tr th')
|
|
.should(($ths) => {
|
|
const texts = [...$ths].map((th) => th.innerText.trim())
|
|
expect(texts).to.include('PRIMARY_KEY_FIELD')
|
|
})
|
|
.then(($ths) => {
|
|
const pkColIndex = [...$ths].findIndex(
|
|
(th) => th.innerText.trim() === 'PRIMARY_KEY_FIELD'
|
|
)
|
|
|
|
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
const sortedRowIndex = [...rows].findIndex(
|
|
(row: any) =>
|
|
row.childNodes[pkColIndex].innerText.trim() === '3'
|
|
)
|
|
|
|
getRowHeaderSymbol(sortedRowIndex).should('have.text', '-')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// CHANGE_SUMMARY_COL combines all three DC.* variables in one formula:
|
|
// IF( DC.ROW_STATUS ="U","unedited", DC.USER_NAME &" changed from "& DC.ORIG_VALUE ).
|
|
// DC.USER_NAME/DC.ORIG_VALUE are frozen literals baked in at load time,
|
|
// while DC.ROW_STATUS is a live cell reference - so editing the row
|
|
// doesn't recompute the "changed from" text, it just flips which
|
|
// already-computed branch IF() reveals.
|
|
it("26 | Combining DC.ROW_STATUS/DC.USER_NAME/DC.ORIG_VALUE in one formula reveals the frozen 'changed from' text once the row is edited", () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(0, 'CHANGE_SUMMARY_COL').should(
|
|
'have.text',
|
|
'unedited'
|
|
)
|
|
|
|
getCellByHeaderAndRow(0, 'A_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('999{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(0, 'CHANGE_SUMMARY_COL').should(
|
|
'have.text',
|
|
'sasdemo changed from orig-1'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
// dataSource keeps the raw '=...' formula string for a SOFTFORMULA cell -
|
|
// HyperFormula recalculates the displayed value live without ever
|
|
// mutating that underlying string, so the submit payload must be built
|
|
// from the live computed value, not from dataSource directly. Editing
|
|
// A_COL (not FORMULA_SOFT_COL itself) both marks the row as submittable
|
|
// and proves the live-recalculated value - not a stale one - is what
|
|
// gets sent.
|
|
it('27 | Submits the computed formula value, not the raw formula string', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
cy.intercept('POST', '**/SASjsApi/stp/execute*', (req) => {
|
|
const bodyText = JSON.stringify(req.body || '')
|
|
|
|
if (bodyText.includes('stagedata')) {
|
|
expect(bodyText).to.include('"FORMULA_SOFT_COL":15')
|
|
expect(bodyText).not.to.match(/"FORMULA_SOFT_COL":"?=/)
|
|
}
|
|
|
|
req.continue()
|
|
}).as('stpExecute')
|
|
|
|
// Row 0: PRIMARY_KEY_FIELD=1, B_COL=10. Changing A_COL from 1 to 5
|
|
// marks the row modified (so it's actually submitted) and live-
|
|
// recalculates FORMULA_SOFT_COL (A_COL + B_COL) from 11 to 15,
|
|
// without the user ever directly editing that cell.
|
|
getCellByHeaderAndRow(0, 'A_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('5{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(0, 'FORMULA_SOFT_COL').should('have.text', '15')
|
|
|
|
submitTable()
|
|
|
|
cy.get('#submitBtn', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
|
|
cy.get('#formFields_8').type('formula value submission test')
|
|
|
|
submitTableMessage()
|
|
})
|
|
})
|
|
})
|
|
|
|
// Row 9 (0-indexed, PRIMARY_KEY_FIELD=10): the mock seeds
|
|
// FORMULA_HARD_COL/FORMULA_SOFT_COL with real pre-existing values
|
|
// (1111/2222) that the computed formula (A_COL*B_COL=100,
|
|
// A_COL+B_COL=20 for this row) silently overwrites at load - simulating
|
|
// a formula rule added to a column that already had real data. Every
|
|
// other row's formula columns are seeded blank, so only this row is
|
|
// affected.
|
|
it('28 | A formula overwriting real pre-existing data marks the row modified, adds a comment, and can be reverted', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
// Already true on the very first render, before Edit is ever clicked -
|
|
// dataSourceUnchanged is seeded with the formula overlay at load time
|
|
// too, not just inside editTable().
|
|
getRowHeaderSymbol(9).should('have.text', '±')
|
|
getCellByHeaderAndRow(9, 'ROW_STATUS_COL').should('have.text', 'M')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getRowHeaderSymbol(9).should('have.text', '±')
|
|
|
|
// The silent overwrite is a real, permanent difference from the raw
|
|
// dataset (not a session edit afterChange would ever see), so
|
|
// DC.ROW_STATUS must already read 'M' here too, not just the row
|
|
// header symbol.
|
|
getCellByHeaderAndRow(9, 'ROW_STATUS_COL').should('have.text', 'M')
|
|
|
|
getCellByHeaderAndRow(9, 'FORMULA_HARD_COL')
|
|
.should('have.text', '100')
|
|
.and('have.class', 'htCommentCell')
|
|
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '20')
|
|
.and('have.class', 'htCommentCell')
|
|
.rightclick({ force: true })
|
|
|
|
// Only our own "Revert" is offered - never the Comments plugin's
|
|
// own add/edit/delete items, since comments here are strictly
|
|
// programmatic (see the comments: {readOnly: true} setting and the
|
|
// deliberately curated contextMenu.items list).
|
|
cy.get('.htContextMenu').should(($menu) => {
|
|
const text = $menu.text()
|
|
expect(text).to.include('Revert')
|
|
expect(text).not.to.include('Add comment')
|
|
expect(text).not.to.include('Edit comment')
|
|
expect(text).not.to.include('Delete comment')
|
|
})
|
|
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '2222')
|
|
.and('not.have.class', 'htCommentCell')
|
|
})
|
|
})
|
|
})
|
|
|
|
// dataSourceUnchanged deliberately holds the RAW pre-formula value for
|
|
// FORMULA_HARD_COL/FORMULA_SOFT_COL so classifyRow flags row 9 as
|
|
// modified (see test 28) - but that's not a session edit for Cancel to
|
|
// discard: the formula recomputes on every load regardless of what's
|
|
// sitting in dataSourceUnchanged. Cancelling (without ever touching
|
|
// "Revert") must keep showing the live computed value and the modified
|
|
// marker, both in the cancelled edit session and back in read-only view.
|
|
it('29 | Cancelling an edit session after a formula silently overwrote real data keeps the computed value and modified marker, not the stale raw one', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(9, 'FORMULA_HARD_COL').should('have.text', '100')
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL').should('have.text', '20')
|
|
|
|
cy.contains('button', 'Cancel').click()
|
|
|
|
getRowHeaderSymbol(9).should('have.text', '±')
|
|
getCellByHeaderAndRow(9, 'ROW_STATUS_COL').should('have.text', 'M')
|
|
getCellByHeaderAndRow(9, 'FORMULA_HARD_COL').should('have.text', '100')
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL').should('have.text', '20')
|
|
})
|
|
})
|
|
})
|
|
|
|
// CHANGE_SUMMARY_COL's own raw seed ('orig-1') genuinely differs from its
|
|
// computed result ('unedited') for row 0, purely because its formula
|
|
// references DC.ROW_STATUS/DC.ORIG_VALUE, which are inherently
|
|
// session-dependent - not because anything overwrote real data. See
|
|
// getStableFormulaBaseCols, which excludes any DC.*-referencing formula
|
|
// rule from the "did a formula overwrite real data" comparison entirely.
|
|
it('30 | DC.*-referencing formula columns never get a "changed value" comment, even when their own raw seed differs from the computed result', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(0, 'ORIG_VALUE_COL')
|
|
.should('have.text', 'orig-1')
|
|
.and('not.have.class', 'htCommentCell')
|
|
|
|
getCellByHeaderAndRow(0, 'CHANGE_SUMMARY_COL')
|
|
.should('have.text', 'unedited')
|
|
.and('not.have.class', 'htCommentCell')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('31 | Reverting a formula value then cancelling keeps it plain, and it stays plain on the next edit session', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL').rightclick({
|
|
force: true
|
|
})
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '2222')
|
|
.and('not.have.class', 'htCommentCell')
|
|
|
|
cy.contains('button', 'Cancel').click()
|
|
})
|
|
})
|
|
|
|
// Back in view mode: the revert is a real, permanent change - not a
|
|
// session edit for Cancel to discard (see
|
|
// getFormulaCellsToPreserveOnCancel - a reverted cell has no comment,
|
|
// so it's excluded from the preserve-the-computed-value patch, and
|
|
// dataSourceUnchanged's raw value, which now matches, is left as-is).
|
|
// FORMULA_HARD_COL was never reverted, so it still shows the live
|
|
// computed value and keeps the row modified via that column alone.
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '2222')
|
|
.and('not.have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(9, 'FORMULA_HARD_COL').should('have.text', '100')
|
|
getRowHeaderSymbol(9).should('have.text', '±')
|
|
|
|
// "Forever" per the original requirement - re-entering edit mode must
|
|
// not resurrect the formula for the reverted cell.
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(9, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '2222')
|
|
.and('not.have.class', 'htCommentCell')
|
|
})
|
|
})
|
|
})
|
|
|
|
// Generalized revert: any cell whose value differs from what SAS
|
|
// actually sent, not only formula columns - A_COL is a plain numeric
|
|
// column with no HARDFORMULA/SOFTFORMULA rule of its own.
|
|
it('32 | Editing a plain (non-formula) cell marks it overwritten, and Revert restores it', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(1, 'A_COL')
|
|
.should('have.text', '2')
|
|
.and('not.have.class', 'htCommentCell')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('999{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(1, 'A_COL')
|
|
.should('have.text', '999')
|
|
.and('have.class', 'htCommentCell')
|
|
.rightclick({ force: true })
|
|
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(1, 'A_COL')
|
|
.should('have.text', '2')
|
|
.and('not.have.class', 'htCommentCell')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('33 | Selecting a range containing one overwritten cell shows Revert and only reverts that cell', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(2, 'A_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('999{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(2, 'A_COL').should('have.class', 'htCommentCell')
|
|
|
|
// Select the whole row's cell range (PRIMARY_KEY_FIELD through
|
|
// CHANGE_SUMMARY_COL) via shift-click, covering both the
|
|
// overwritten A_COL and several untouched cells.
|
|
getCellByHeaderAndRow(2, 'PRIMARY_KEY_FIELD').click({ force: true })
|
|
getCellByHeaderAndRow(2, 'CHANGE_SUMMARY_COL').click({
|
|
force: true,
|
|
shiftKey: true
|
|
})
|
|
getCellByHeaderAndRow(2, 'B_COL').rightclick({ force: true })
|
|
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(2, 'A_COL')
|
|
.should('have.text', '3')
|
|
.and('not.have.class', 'htCommentCell')
|
|
// An untouched cell within the same reverted selection is left
|
|
// exactly as it was - proves Revert only acts on the cell(s) that
|
|
// were actually overwritten, not the whole selection.
|
|
getCellByHeaderAndRow(2, 'B_COL').should('have.text', '10')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('34 | Selecting a range with no overwritten cells does not show Revert', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(3, 'PRIMARY_KEY_FIELD').click({ force: true })
|
|
getCellByHeaderAndRow(3, 'CHANGE_SUMMARY_COL').click({
|
|
force: true,
|
|
shiftKey: true
|
|
})
|
|
getCellByHeaderAndRow(3, 'B_COL').rightclick({ force: true })
|
|
|
|
cy.get('.htContextMenu').should(($menu) => {
|
|
expect($menu.text()).not.to.include('Revert')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('35 | A selection spanning a newly-inserted row and an existing overwritten cell only reverts the existing row', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(0, 'A_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('999{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(0, 'A_COL').should('have.class', 'htCommentCell')
|
|
|
|
insertRowViaContextMenu(0, 'Insert Row below')
|
|
|
|
// New row lands at index 1 - select a range spanning both the
|
|
// existing overwritten row (0) and the brand-new row (1).
|
|
getCellByHeaderAndRow(0, 'A_COL').click({ force: true })
|
|
getCellByHeaderAndRow(1, 'A_COL').click({
|
|
force: true,
|
|
shiftKey: true
|
|
})
|
|
getCellByHeaderAndRow(0, 'A_COL').rightclick({ force: true })
|
|
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(0, 'A_COL')
|
|
.should('have.text', '1')
|
|
.and('not.have.class', 'htCommentCell')
|
|
// The new row has no original SAS value to revert to - it's simply
|
|
// left alone, no error.
|
|
getCellByHeaderAndRow(1, 'A_COL').should('have.text', '')
|
|
})
|
|
})
|
|
})
|
|
|
|
// Rows 7-9 (0-indexed 6-8) are seeded with a staggered pattern (see the
|
|
// mock's own comment): row 7 only overwrites FORMULA_HARD_COL, row 8
|
|
// only overwrites FORMULA_SOFT_COL, row 9 overwrites both - all three
|
|
// already show their raw-vs-computed mismatch on load, with no manual
|
|
// edit needed.
|
|
it('36 | Selecting a block spanning multiple pre-loaded overwritten cells across different rows and columns reverts only the actually-overwritten ones', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(6, 'FORMULA_HARD_COL')
|
|
.should('have.text', '70')
|
|
.and('have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(7, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '18')
|
|
.and('have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(8, 'FORMULA_HARD_COL')
|
|
.should('have.text', '90')
|
|
.and('have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(8, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '19')
|
|
.and('have.class', 'htCommentCell')
|
|
|
|
// Select the 3-row x 2-col block covering rows 6-8, columns
|
|
// FORMULA_HARD_COL through FORMULA_SOFT_COL.
|
|
getCellByHeaderAndRow(6, 'FORMULA_HARD_COL').click({ force: true })
|
|
getCellByHeaderAndRow(8, 'FORMULA_SOFT_COL').click({
|
|
force: true,
|
|
shiftKey: true
|
|
})
|
|
getCellByHeaderAndRow(7, 'FORMULA_HARD_COL').rightclick({
|
|
force: true
|
|
})
|
|
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(6, 'FORMULA_HARD_COL')
|
|
.should('have.text', '7771')
|
|
.and('not.have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(7, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '8882')
|
|
.and('not.have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(8, 'FORMULA_HARD_COL')
|
|
.should('have.text', '9991')
|
|
.and('not.have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(8, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '9992')
|
|
.and('not.have.class', 'htCommentCell')
|
|
|
|
// Cells within the same block that were never overwritten in the
|
|
// first place are left exactly as they were computed.
|
|
getCellByHeaderAndRow(6, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '17')
|
|
.and('not.have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(7, 'FORMULA_HARD_COL')
|
|
.should('have.text', '80')
|
|
.and('not.have.class', 'htCommentCell')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('37 | Selecting an entire column reverts every pre-loaded overwritten cell in it, leaving other columns and out-of-range rows untouched', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
// Select every row of FORMULA_HARD_COL only - rows 6, 8 and 9 are
|
|
// overwritten in it, rows 0-5 and 7 are not.
|
|
getCellByHeaderAndRow(0, 'FORMULA_HARD_COL').click({ force: true })
|
|
getCellByHeaderAndRow(9, 'FORMULA_HARD_COL').click({
|
|
force: true,
|
|
shiftKey: true
|
|
})
|
|
getCellByHeaderAndRow(3, 'FORMULA_HARD_COL').rightclick({
|
|
force: true
|
|
})
|
|
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(6, 'FORMULA_HARD_COL')
|
|
.should('have.text', '7771')
|
|
.and('not.have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(8, 'FORMULA_HARD_COL')
|
|
.should('have.text', '9991')
|
|
.and('not.have.class', 'htCommentCell')
|
|
getCellByHeaderAndRow(9, 'FORMULA_HARD_COL')
|
|
.should('have.text', '1111')
|
|
.and('not.have.class', 'htCommentCell')
|
|
|
|
// A row that was never overwritten in FORMULA_HARD_COL is left as
|
|
// its computed value.
|
|
getCellByHeaderAndRow(2, 'FORMULA_HARD_COL')
|
|
.should('have.text', '30')
|
|
.and('not.have.class', 'htCommentCell')
|
|
|
|
// FORMULA_SOFT_COL was never part of the selection - row 7's
|
|
// pre-loaded overwrite there must survive untouched. It's a live
|
|
// formula cell, so it still displays the computed value (18), not
|
|
// the raw seed (8882) - the comment is what carries the raw value,
|
|
// not the display text (see test 36's identical assertion).
|
|
getCellByHeaderAndRow(7, 'FORMULA_SOFT_COL')
|
|
.should('have.text', '18')
|
|
.and('have.class', 'htCommentCell')
|
|
})
|
|
})
|
|
})
|
|
|
|
// PLAIN_TEXT_COL has no DQ rule at all (not HARDFORMULA/SOFTFORMULA, not
|
|
// even NOTNULL) - unlike FORMULA_HARD_COL/FORMULA_SOFT_COL it can never be
|
|
// pre-loaded as already overwritten (nothing mutates a value between the
|
|
// dataSourceRaw snapshot and first render except a formula rule), so this
|
|
// proves the general revert path still works via a plain live edit on a
|
|
// column that's a string, not a number.
|
|
it('38 | Editing a plain character column with no DQ rule at all marks it overwritten, and Revert restores it', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL')
|
|
.should('have.text', 'note-2')
|
|
.and('not.have.class', 'htCommentCell')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type('edited by user{enter}')
|
|
})
|
|
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL')
|
|
.should('have.text', 'edited by user')
|
|
.and('have.class', 'htCommentCell')
|
|
.rightclick({ force: true })
|
|
|
|
cy.get('.htContextMenu').contains('Revert').click()
|
|
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL')
|
|
.should('have.text', 'note-2')
|
|
.and('not.have.class', 'htCommentCell')
|
|
})
|
|
})
|
|
})
|
|
|
|
// The info item has no callback (it's read-only display text), but
|
|
// Handsontable's Menu widget still treated any click landing inside it as
|
|
// "the item was activated" - closing the menu on left-click and, on
|
|
// right-click, preventDefault()-ing before the browser's own "Copy" menu
|
|
// could appear. Either way the user could never select/copy the text. If
|
|
// the dropdown were still closing, `.htDropdownMenu` would no longer
|
|
// exist and the `.should()` below would time out.
|
|
it('39 | Left-click or right-click inside the info dropdown does not close it', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
openColumnDropdown('FORMULA_HARD_COL')
|
|
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.include('NAME: FORMULA_HARD_COL')
|
|
})
|
|
|
|
cy.get('.htDropdownMenu').contains('NAME:').click()
|
|
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.include('NAME: FORMULA_HARD_COL')
|
|
})
|
|
|
|
cy.get('.htDropdownMenu').contains('NAME:').rightclick()
|
|
|
|
cy.get('.htDropdownMenu').should(($menu) => {
|
|
expect($menu.text()).to.include('NAME: FORMULA_HARD_COL')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// Row 1 (0-indexed): A_COL=2, B_COL=10 - see the HARDFORMULA/SOFTFORMULA
|
|
// tests' own comment above.
|
|
it("40 | Pasting a formula with column names translates them to this row's cell references and evaluates it, on a table with formulas enabled", () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL').click()
|
|
|
|
pasteTextIntoFocusedCell('=A_COL * B_COL')
|
|
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL').should('have.text', '20')
|
|
})
|
|
})
|
|
})
|
|
|
|
it('41 | Pasting the same formula text on a table without formulas enabled leaves it as literal, untranslated text', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
// force: true - same overlay-clone-layer reason as sortByColumn's
|
|
// own comment below; SOME_CHAR's header sort icon covers the cell.
|
|
getCellByHeaderAndRow(1, 'SOME_CHAR').click({ force: true })
|
|
|
|
pasteTextIntoFocusedCell('=SOME_BESTNUM * SOME_BESTNUM')
|
|
|
|
getCellByHeaderAndRow(1, 'SOME_CHAR').should(
|
|
'have.text',
|
|
'=SOME_BESTNUM * SOME_BESTNUM'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
it('42 | Pasting a formula referencing a column name inside a quoted string leaves the quoted text untouched', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL').click()
|
|
|
|
// A_COL needs a leading/trailing blank to be recognised as a
|
|
// column-name token (see substituteColumnReferences' own "no
|
|
// surrounding blanks" test) - matches the space-after-paren
|
|
// convention already used by this fixture's own CHANGE_SUMMARY_COL
|
|
// rule value ('=IF( DC.ROW_STATUS ="U",...)').
|
|
pasteTextIntoFocusedCell(
|
|
'=IF( A_COL > 0, "A_COL is positive", "negative")'
|
|
)
|
|
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL').should(
|
|
'have.text',
|
|
'A_COL is positive'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
it('43 | Pasting a formula directly into an open cell editor (double-click, then paste) also translates and evaluates it', () => {
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
|
|
|
|
clickOnEdit(() => {
|
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
timeout: longerCommandTimeout
|
|
}).then(() => {
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL')
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
pasteTextIntoFocusedCell('=A_COL * B_COL')
|
|
|
|
cy.focused().type('{enter}')
|
|
|
|
getCellByHeaderAndRow(1, 'PLAIN_TEXT_COL').should('have.text', '20')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// Handsontable virtualizes columns — with 18 columns on MPE_X_NEW, only
|
|
// the ones in the current viewport actually exist in the DOM. Scroll all
|
|
// the way right so REGEX_HARD_COL/REGEX_SOFT_COL/REGEX_BOTH_COL (the last
|
|
// three) render at all. Same technique already used in excel.cy.ts. Must
|
|
// be called (and re-settle) before any header/body query below, since
|
|
// scrolling replaces the previously-rendered column nodes.
|
|
const scrollGridRight = () => {
|
|
return cy
|
|
.get('#hotTable')
|
|
.find('div.ht_master.handsontable')
|
|
.find('div.wtHolder')
|
|
.scrollTo('right')
|
|
}
|
|
|
|
// Clicks a column header's sort indicator (Handsontable's multiColumnSorting
|
|
// plugin decorates every sortable header with a `.columnSorting` element -
|
|
// HEADER_SORT_CLASS in Handsontable's own source) to sort ascending by that
|
|
// column. force: true for the same reason as openColumnDropdown - an
|
|
// overlay clone layer can sit over the real target.
|
|
const sortByColumn = (headerText: string) => {
|
|
cy.get('.ht_clone_top .htCore thead tr th')
|
|
.filter((_, th) => Cypress.$(th).text().includes(headerText))
|
|
.last()
|
|
.find('.columnSorting')
|
|
.click({ force: true })
|
|
}
|
|
|
|
// Opens a column header's dropdown menu to reach its `info` item, which
|
|
// has a custom renderer showing NAME/LABEL/TYPE/LENGTH/FORMAT and, when the
|
|
// column has a HARDREGEX/SOFTREGEX rule, the applied pattern. Clicking the
|
|
// header text first selects the column - the renderer reads
|
|
// hot.getSelected() to decide which column to describe.
|
|
const openColumnDropdown = (headerText: string) => {
|
|
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.parents('th')
|
|
.filter((_, th) => Cypress.$(th).text().includes(headerText))
|
|
.last()
|
|
.as('targetHeader')
|
|
|
|
cy.get('@targetHeader').click({ force: true })
|
|
cy.get('@targetHeader').find('button.changeType').click({ force: true })
|
|
}
|
|
|
|
// Locates a body cell by its column's header text rather than a hardcoded
|
|
// childNodes index. Handsontable's hiddenColumns plugin (e.g. HIDDEN_COL,
|
|
// used by several demo columns ahead of REGEX_HARD_COL/REGEX_SOFT_COL in
|
|
// COLHEADERS) removes a hidden column's cells from BOTH the header row and
|
|
// every body row identically, so the header row's th index always lines up
|
|
// with the same-position childNode in any body row — this sidesteps having
|
|
// to know how many hidden columns precede the target. Re-queries both the
|
|
// header and body row fresh each call (rather than accepting a pre-fetched
|
|
// row) since virtualization/scrolling can replace either pane's nodes.
|
|
//
|
|
// The `.should()` retries until headerText actually shows up: Handsontable
|
|
// re-renders virtualized columns asynchronously after a scroll event, on
|
|
// its own schedule outside Cypress's command queue, so scrollTo() settling
|
|
// does not mean the target column has been rendered yet.
|
|
// Simulates a real clipboard paste into whichever element currently has
|
|
// DOM focus (Handsontable moves focus to its own internal textarea once a
|
|
// cell is selected/clicked) - Cypress has no built-in clipboard simulation,
|
|
// and Handsontable's CopyPaste plugin only listens for a native 'paste'
|
|
// DOM event with clipboardData, so a synthetic ClipboardEvent is
|
|
// dispatched directly rather than trying to drive the OS clipboard.
|
|
const pasteTextIntoFocusedCell = (text: string) => {
|
|
cy.focused().then(($el) => {
|
|
const dataTransfer = new DataTransfer()
|
|
dataTransfer.setData('text/plain', text)
|
|
|
|
const pasteEvent = new ClipboardEvent('paste', {
|
|
clipboardData: dataTransfer,
|
|
bubbles: true,
|
|
cancelable: true
|
|
})
|
|
|
|
$el[0].dispatchEvent(pasteEvent)
|
|
})
|
|
}
|
|
|
|
const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => {
|
|
return cy
|
|
.get('.ht_clone_top .htCore thead tr th')
|
|
.should(($ths) => {
|
|
const texts = [...$ths].map((th) => th.innerText.trim())
|
|
expect(texts).to.include(headerText)
|
|
})
|
|
.then(($ths) => {
|
|
const index = [...$ths].findIndex(
|
|
(th) => th.innerText.trim() === headerText
|
|
)
|
|
|
|
return cy
|
|
.get('.ht_master tbody tr')
|
|
.then((rows: any) => rows[rowIndex].childNodes[index])
|
|
.then((cell) => cy.get(cell))
|
|
})
|
|
}
|
|
|
|
// Reads the row-header gutter's text for the given row - this is the
|
|
// left-hand "select entire row" bar (rowHeaders callback in
|
|
// editor.component.ts), doubling as the edit-status indicator
|
|
// (+/-/±/blank). .ht_clone_left mirrors .ht_clone_top's frozen-clone role,
|
|
// just for the row axis instead of the column axis.
|
|
const getRowHeaderSymbol = (rowIndex: number) => {
|
|
return cy
|
|
.get('.ht_clone_left .htCore tbody tr')
|
|
.eq(rowIndex)
|
|
.find('th .rowHeader')
|
|
}
|
|
|
|
// The th itself (not its inner .rowHeader span) - carries the
|
|
// rowStatusAdded/rowStatusDeleted/rowStatusModified class that colors the
|
|
// row-header background (see afterGetRowHeader in editor.component.ts).
|
|
// Checking for the class rather than an exact computed colour, since the
|
|
// actual colour comes from a Clarity design-token CSS variable and isn't
|
|
// meant to be pinned here.
|
|
const getRowHeaderCell = (rowIndex: number) => {
|
|
return cy.get('.ht_clone_left .htCore tbody tr').eq(rowIndex).find('th')
|
|
}
|
|
|
|
// Right-clicks the given row's PRIMARY_KEY_FIELD cell to open the row's
|
|
// context menu, then clicks the given item. 'Insert Row above'/'below' are
|
|
// hidden when hotTable.readOnly is true, so this only works after
|
|
// clickOnEdit(). force: true - same reasoning as toggleColumnLabelsFromContextMenu
|
|
// in viewer-labels.cy.ts, the right-clicked cell can sit under an overlay
|
|
// clone layer that fails Cypress's actionability check.
|
|
const insertRowViaContextMenu = (
|
|
rowIndex: number,
|
|
menuItemText: 'Insert Row above' | 'Insert Row below'
|
|
) => {
|
|
getCellByHeaderAndRow(rowIndex, 'PRIMARY_KEY_FIELD').rightclick({
|
|
force: true
|
|
})
|
|
cy.get('.htContextMenu').contains(menuItemText).click()
|
|
}
|
|
|
|
// Opens the _____DELETE__THIS__RECORD_____ dropdown editor on the given cell
|
|
// and picks the given choice ('Yes'/'No') — same technique as
|
|
// coltype-delete-record.cy.ts (arrow -> autocompleteEditor choice list).
|
|
const setDeleteFlag = (
|
|
deleteCell: any,
|
|
value: 'Yes' | 'No',
|
|
callback?: any
|
|
) => {
|
|
cy.get(deleteCell)
|
|
.click({ force: true })
|
|
.then(($td) => {
|
|
cy.get('.htAutocompleteArrow', { withinSubject: $td })
|
|
.should('exist')
|
|
.click({ force: true })
|
|
|
|
cy.get('.autocompleteEditor .htCore tbody td')
|
|
.contains(value)
|
|
.click({ force: true })
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
})
|
|
}
|
|
|
|
const clickOnEdit = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
|
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
|
.should('not.exist')
|
|
.then(() => {
|
|
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
|
timeout: longerCommandTimeout
|
|
}).then((treeNodes: any) => {
|
|
let viyaLib
|
|
|
|
for (let node of treeNodes) {
|
|
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
viyaLib = node
|
|
break
|
|
}
|
|
}
|
|
|
|
cy.get(viyaLib).within(() => {
|
|
cy.get('.clr-tree-node-content-container > button').click()
|
|
|
|
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
|
for (let innerNode of innerNodes) {
|
|
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
innerNode.click()
|
|
break
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
const attachExcelFile = (excelFilename: string, callback?: any) => {
|
|
cy.get('.buttonBar button:last-child')
|
|
.click()
|
|
.then(() => {
|
|
cy.get('input[type="file"]#file-upload')
|
|
.attachFile(`/${fixturePath}/${excelFilename}`)
|
|
.then(() => {
|
|
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
|
modalBtn.click()
|
|
if (callback) callback()
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
const clickOnUploadPreview = (callback?: any) => {
|
|
cy.get('.buttonBar button.btn-primary.btn-upload-preview')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const confirmEditPreviewFile = (callback?: any) => {
|
|
cy.get('.modal-footer button.btn-success-outline')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitTable = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitTableMessage = (callback?: any) => {
|
|
cy.get('.modal-footer .btn.btn-sm.btn-success-outline')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitExcel = (callback?: any) => {
|
|
cy.get('.buttonBar button.preview-submit')
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const rejectExcel = (callback?: any) => {
|
|
cy.get('button', { timeout: longerCommandTimeout })
|
|
.should('contain', 'Approve')
|
|
.then((allButtons: any) => {
|
|
for (let approvalButton of allButtons) {
|
|
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
|
approvalButton.click()
|
|
break
|
|
}
|
|
}
|
|
|
|
cy.get('button.btn-danger')
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
.click()
|
|
.then(() => {
|
|
cy.get('.modal-footer button.btn-success-outline')
|
|
.click()
|
|
.then(() => {
|
|
cy.get('app-history')
|
|
.should('exist')
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
const visitPage = (url: string) => {
|
|
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
}
|