hook-programs.cy.ts was in no CI spec list, so it never ran - the only end-to-end coverage of the new mock hook execution (mockHookSource plus the PRE/POST_EDIT_HOOK blocks) and of the loader's target re-registration check was dead weight. Add it to the development job's --spec list. It goes second, not last: csv-limited.cy.ts asserts the free tier and so must keep the first slot (before any spec applies a licence key), and test 2 reads the approval queue, which is paged oldest first - a changeset submitted by an earlier spec pushes its own row off the first page. The spec header records the constraint. Full list verified locally: 15 specs, 132 tests, all passing.
210 lines
6.6 KiB
TypeScript
210 lines
6.6 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.
|
|
//
|
|
// RUN ORDER MATTERS, and build.yaml reflects it: this spec must run after
|
|
// csv-limited.cy.ts (which asserts the free tier and so needs the first slot,
|
|
// before any spec applies a licence key) and before every spec that submits a
|
|
// changeset. Test 2 reads the approval queue, which is paged oldest first, and
|
|
// a changeset submitted by an earlier spec pushes this one off the first page.
|
|
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. Search the
|
|
// whole grid rather than one row: the queue also holds changesets from
|
|
// earlier specs in the same run, so position is not a stable anchor.
|
|
visitPage('review/approve')
|
|
|
|
cy.get('clr-datagrid clr-dg-row', { timeout: longerCommandTimeout }).should(
|
|
($rows) => {
|
|
const texts = Array.from($rows).map((row: any) =>
|
|
row.innerText.trim().replace(/\s+/g, ' ')
|
|
)
|
|
|
|
expect(
|
|
texts.some((text) => text.includes('TESTDATA.DEMO_ORDERS')),
|
|
`no approval row names TESTDATA.DEMO_ORDERS - rows: ${texts
|
|
.slice(0, 5)
|
|
.join(' | ')
|
|
.slice(0, 400)}`
|
|
).to.be.true
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
// ─── 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()
|
|
})
|
|
}
|