The JS mock services carried MPE_TABLES.pre_edit_hook / post_edit_hook values but never acted on them, so a table configured with hooks behaved differently in the mock than in production. Add mockHookSource() to dcMockUtils (resolving a hook program name to the source of its .js counterpart on the Drive) and run the hook where the SAS backend does: - editors/getdata runs the pre-edit hook after filtering, letting it reassign visibleRows / visibleColumns (the work.OUT contract) - so an empty mirror can display the live rows of the table it points at - editors/stagedata and editors/loadfile run the post-edit hook before the MPE_SUBMIT row is written, letting it reassign libref / dsn (the call symputx contract) - so a changeset submitted against a mirror is raised against the real table Seed a demo pair in makedata (TESTDATA.DEMO_ORDERS plus an empty TESTDATA.DEMO_MIRROR with both hooks) and cover it with a Cypress spec.
190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
// Marks this file as an ES module (rather than a global script) so its
|
|
// top-level consts don't collide, under the TS type-checker, with the same
|
|
// names declared in other spec files.
|
|
export {}
|
|
|
|
const hostUrl = Cypress.env('hosturl')
|
|
const appLocation = Cypress.env('appLocation')
|
|
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
|
|
|
context('hook program tests: ', function () {
|
|
this.beforeAll(() => {
|
|
cy.loginAndUpdateValidKey(true)
|
|
})
|
|
|
|
this.beforeEach(() => {
|
|
cy.visit(hostUrl + appLocation)
|
|
|
|
visitPage('home')
|
|
})
|
|
|
|
// TESTDATA.DEMO_MIRROR is an empty mirror of TESTDATA.DEMO_ORDERS, registered
|
|
// with a PRE_EDIT_HOOK and a POST_EDIT_HOOK (services/hooks/demo_mirror_*.js).
|
|
// The mock services run those hooks the way the SAS backend %includes the
|
|
// real hook programs - see dcMockUtils.mockHookSource.
|
|
|
|
it('1 | PRE_EDIT_HOOK: an empty mirror displays the live rows of its target table', () => {
|
|
openTableFromTree('testdata', 'demo_mirror')
|
|
|
|
// The mirror holds no rows of its own: everything on screen comes from the
|
|
// pre-edit hook, which loads TESTDATA.DEMO_ORDERS.
|
|
cy.get('#hotTable .ht_master tbody tr', {
|
|
timeout: longerCommandTimeout
|
|
}).should('have.length', 4)
|
|
|
|
cy.get('#hotTable .ht_master tbody tr')
|
|
.first()
|
|
.should('contain.text', 'Acme Corp')
|
|
|
|
cy.get('#hotTable .ht_master tbody tr')
|
|
.last()
|
|
.should('contain.text', 'Delta Systems')
|
|
|
|
// The grid is validated against the MIRROR's own rule set (AMOUNT <= 2000),
|
|
// not the real table's (AMOUNT <= 100000) - that is the point of routing
|
|
// edits through a mirror.
|
|
clickOnEdit(() => {
|
|
editCell(0, 'AMOUNT', '5000')
|
|
})
|
|
|
|
cy.get('#hotTable .ht_master tbody tr')
|
|
.first()
|
|
.find('td.htInvalid')
|
|
.should('exist')
|
|
})
|
|
|
|
it('2 | POST_EDIT_HOOK: a change submitted against the mirror is raised against the target', () => {
|
|
openTableFromTree('testdata', 'demo_mirror')
|
|
|
|
clickOnEdit(() => {
|
|
editCell(0, 'AMOUNT', '750')
|
|
})
|
|
|
|
submitTable(() => {
|
|
cy.get('textarea.submit-reason', { timeout: longerCommandTimeout }).type(
|
|
'hook routing test'
|
|
)
|
|
submitTableMessage()
|
|
})
|
|
|
|
// A successful submit sends the app to the staged-data page; wait for that
|
|
// before navigating away, or the app's own redirect wins the race.
|
|
cy.url({ timeout: longerCommandTimeout }).should('include', '/stage/')
|
|
|
|
// The post-edit hook re-points the changeset at TESTDATA.DEMO_ORDERS, so the
|
|
// approval queue names the real table rather than the mirror.
|
|
visitPage('review/approve')
|
|
|
|
cy.get('clr-datagrid clr-dg-row', { timeout: longerCommandTimeout })
|
|
.last()
|
|
.should('contain.text', 'TESTDATA.DEMO_ORDERS')
|
|
})
|
|
})
|
|
|
|
// ─── helpers (declared at the bottom, per the other specs) ───────────────────
|
|
|
|
const visitPage = (url: string) => {
|
|
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
}
|
|
|
|
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.wait(300)
|
|
|
|
cy.get(
|
|
'.clr-tree-node-content-container .clr-treenode-content p'
|
|
).click()
|
|
|
|
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
|
for (let innerNode of innerNodes) {
|
|
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
innerNode.click()
|
|
break
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
|
timeout: longerCommandTimeout
|
|
}).should('exist')
|
|
}
|
|
|
|
const clickOnEdit = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
// Edits a single cell, locating the column by its header label. Handsontable
|
|
// renders the frozen header in a separate clone pane (.ht_clone_top) - the
|
|
// header row inside .ht_master is kept visibility:hidden - so the labels are
|
|
// read from the clone.
|
|
const editCell = (rowIndex: number, colName: string, value: string) => {
|
|
cy.get('#hotTable .ht_clone_top .htCore thead th', {
|
|
timeout: longerCommandTimeout
|
|
}).then((headers: any) => {
|
|
let colIndex = -1
|
|
for (let i = 0; i < headers.length; i++) {
|
|
if (
|
|
String(headers[i].innerText || '')
|
|
.trim()
|
|
.toUpperCase() === colName.toUpperCase()
|
|
) {
|
|
colIndex = i
|
|
break
|
|
}
|
|
}
|
|
expect(colIndex, `column ${colName} present`).to.be.greaterThan(-1)
|
|
|
|
cy.get('#hotTable .ht_master tbody tr')
|
|
.eq(rowIndex)
|
|
.then((row: any) => {
|
|
cy.get(row[0].childNodes[colIndex])
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.clear()
|
|
.type(value + '{enter}')
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
const submitTable = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitTableMessage = (callback?: any) => {
|
|
cy.get('.modal-footer .btn.btn-sm.btn-success-outline', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|