Files
dc/client/cypress/e2e/download.cy.ts
T
dc dff4bbdc09
Build / Build-and-ng-test (pull_request) Successful in 4m48s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m57s
Build / Build-and-test-development (pull_request) Successful in 28m58s
test(download): find the approval row by table, not by the stage URL
The case took the id from the stage route and looked for a download button
carrying it.  For an upload the route does not hold a table id at all - it holds
a load reference, the CI failure naming DC20261031-201657.470_601010_4449 - so
the button it looked for could never exist, however long it waited or however
many times the screen was reloaded.  That is why the row "never appeared" while
the approval was in fact there.

The row is now found the way hook-programs finds its own: by the table it names.
The id the download is named after is read from the button, which is the only
place that knows it.

Verified against the mock estate locally, with the licence the spec now plants:
7 passing, exit 0.
2026-10-31 20:24:58 +00:00

380 lines
12 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 downloadsFolder = Cypress.config('downloadsFolder')
/* set by test 1 and removed afterwards, since it is written per attempt */
let generatedExcel = ''
import { deleteDownloadsFolder } from '../util/deleteDownloadFolder'
context('download files test: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
/* the estate ships no licence, so it runs on the free tier - and the free
tier disables file upload, which is the whole point of the audit case.
Asking for a licensed estate here means the spec does not depend on
another spec having planted a key first: the key persists in the estate's
config, so whether this case could run depended on the order the specs
were sharded into. */
cy.loginAndUpdateValidKey(true)
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// beforeAll already logged in. Typing into the login form unconditionally
// failed with "the center of this element is hidden from view" whenever the
// session was live and the form was not on screen, so only fill it when it
// is actually shown.
cy.get('body').then(($body) => {
const usernameInput = $body.find('input.username')[0]
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
cy.get('input.username').type(username)
cy.get('input.password').type(password)
cy.get('.login-group button').click()
}
})
visitPage('home')
})
this.afterEach(() => {
deleteDownloadsFolder()
if (generatedExcel) {
cy.task('deleteFile', 'cypress/fixtures/excels/' + generatedExcel)
generatedExcel = ''
}
})
it('1 | downloads audit file', (done) => {
/* The approve grid renders a download button per table that is waiting for
approval, and a freshly seeded estate has none, so create one: upload a
fixture to the test table and submit it from the stage screen. The row is
then found on the approve screen by the table it names, because the stage
route does not carry that table's id.
The submitted row is left behind: CI recreates the appLoc and re-runs
makedata before the specs. On an estate reused by hand the rows accumulate
and the approve grid paginates, so a row pushed past the first page would
not be found - the table name still anchors the assertion, but it cannot
rescue a row that is not on the page. */
/* the fixture is renumbered before every attempt: an upload identical to
what the table already holds produces no changes, and a changeset with no
changes leaves nothing waiting for approval. On a retry that is exactly
what happens - the first attempt's rows are already in the table - so the
keys come from a random base and the upload is always an insert. */
generatedExcel = 'generated_' + Date.now() + '.xlsx'
cy.task('makeUniqueExcel', {
source: 'cypress/fixtures/excels/regular_excel.xlsx',
dest: 'cypress/fixtures/excels/' + generatedExcel
})
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile(generatedExcel)
submitExcel()
/* the stage route carries a load reference, not the table id: an upload
lands on /stage/DC20261031-201657.470_601010_4449 rather than on
/stage/<table id>, so taking the id from the URL looked for a button that
could never exist. The row is found by the table it names, and the id the
download is named after is read from the button itself. */
visitPage('review/approve')
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
'not.exist'
)
cy.get('clr-datagrid clr-dg-row', { timeout: longerCommandTimeout }).should(
($rows) => {
const texts = Array.from($rows).map((row: any) =>
row.innerText.replace(/\s+/g, ' ')
)
expect(
texts.some((text: string) => text.includes('MPE_X_TEST')),
`no approval row names MPE_X_TEST - rows: ${texts
.slice(0, 5)
.join(' | ')
.slice(0, 300)}`
).to.be.true
}
)
cy.get('clr-datagrid clr-dg-row')
.filter(':contains("MPE_X_TEST")')
.first()
.find('[aria-label="Download audit file"]')
.should('have.length', 1)
.then(($btn) => {
const tableId = $btn.attr('id') as string
expect(tableId, 'the approve button carries the table id').to.be.a(
'string'
)
cy.wrap($btn).click()
checkForFileDownloaded(tableId, 'zip', () => done())
})
})
it('2 | downloads viewer csv', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('CSV')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'csv', () => done())
})
})
})
})
it('3 | downloads viewer excel', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('EXCEL')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'xlsx', () => done())
})
})
})
})
it('4 | downloads viewer SAS Datalines', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('SAS')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'sas', () => done())
})
})
})
})
it('5 | downloads viewer SAS DDL', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('SAS_DDL')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'ddl', () => done(), '_')
})
})
})
})
it('6 | downloads viewer TSQL DDL', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('TSQL_DDL')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'ddl', () => done(), '_')
})
})
})
})
it('7 | downloads viewer PGSQL DDL', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('PGSQL_DDL')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'ddl', () => done(), '_')
})
})
})
})
this.afterEach(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
const checkForFileDownloaded = (
id: string,
extension: string,
callback?: any,
libDivider: string = '.'
) => {
cy.on('url:changed', (newUrl) => {
console.log('newUrl', newUrl)
})
id = id.replace('.', libDivider)
const filename = downloadsFolder + '/' + id + '.' + extension
// browser might take a while to download the file,
// so use "cy.readFile" to retry until the file exists
// and has length - and we assume that it has finished downloading then
cy.readFile(filename, { timeout: longerCommandTimeout })
.should('have.length.gt', 10)
.then((file) => {
if (callback) callback()
})
}
const openDownloadModal = (callback?: any) => {
/* filterSide is the viewer's options dropdown; it no longer carries a
dropdown-toggle class - see filter-panel.cy.ts and row-cell-limits.cy.ts.
Its entries are div[clrDropdownItem], not buttons, so select Download by its
text the way filter-panel.cy.ts selects Filter. */
cy.get('.btn.btn-sm.btn-outline.filterSide', {
timeout: longerCommandTimeout
})
.click()
.then(() => {
/* the click handler is on the clrDropdownItem element, not on the span
holding the text, and the whole label is matched: more than one entry
may mention Download, and an ambiguous cy.contains fails outright */
cy.contains('clr-dropdown-menu [clrDropdownItem]', /^\s*Download\s*$/, {
timeout: longerCommandTimeout
}).click()
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
}
}
console.log('viyaLib', viyaLib)
cy.get(viyaLib).within(() => {
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
}
}
})
})
})
})
}
const fixturePath = 'excels'
/**
* Uploads a fixture to the open table. The editor renders the file input inside
* its upload modal, which the Upload button in the button bar opens, so that
* button is clicked first - by its label, not by position, so a change to the
* bar's button order cannot break this.
*/
const attachExcelFile = (excelFilename: string) => {
cy.get('.buttonBar button', { timeout: longerCommandTimeout })
.contains('Upload')
.click()
cy.get('input[type="file"]#file-upload', { timeout: longerCommandTimeout })
.should('have.length', 1)
.attachFile(`/${fixturePath}/${excelFilename}`)
/* a parsed upload raises the info modal with this title. The modal component
is the same one aborts use, so the title is what distinguishes them: any
other text here - an abort, a validation error - fails the case with its own
message rather than being clicked through. */
cy.get('.clr-abort-modal .modal-title', { timeout: longerCommandTimeout })
.should('be.visible')
.invoke('text')
.should('include', 'Table Found')
cy.get('.clr-abort-modal .modal-footer button').contains('Close').click()
}
const submitExcel = () => {
cy.get('.buttonBar button.preview-submit', {
timeout: longerCommandTimeout
}).click()
/* the URL is already a stage route before the submit lands, so waiting on it
proves nothing; the editor leaving preview mode is the signal that the
submit was accepted */
cy.get('.buttonBar button.preview-submit', {
timeout: longerCommandTimeout
}).should('not.exist')
}