Files
dc/client/cypress/e2e/hook-programs.cy.ts
T
dc 60ddb475d7
Build / Build-and-ng-test (pull_request) Successful in 5m26s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m5s
Build / Build-and-test-development (pull_request) Successful in 29m17s
ci: run the hook-programs spec in the cypress job
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.
2026-09-27 23:28:42 +00:00

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()
})
}