The specs opened with `cy.visit(`${hostUrl}/SASLogon/logout`)`. Against a dev
server that only navigated the single-page app: every path answered index.html,
so nothing was logged out. With the app served by the server it talks to, the
route answers 403 - `desktopRestrict` in @sasjs/server refuses it by design, and
desktop is that server's default mode - so the visit failed the before hook and
every spec aborted on its first line.
`cy.clearSession()` clears cookies and localStorage, which is what the visit was
standing in for, and needs nothing from the server. The MODE=desktop line and its
comment go with it: the server already defaults to desktop, so the line changed
nothing and the comment described a mode the job never ran in.
Verified locally against a 4.14.0 estate with the build streamed to
/AppStream/clickme and the mock services seeded: viewer-labels 5/5 (the spec that
failed in CI), filtering 10/10.
Also in this push, three review findings on the harness itself:
- resolve-browser.sh searches ${DC_PATH:-$PATH} through its own loop, so the
test pins the whole decision to fixtures instead of inheriting a chromium from
the host; case 2 was passing here and failing on any machine with one.
- the Cypress step requires the exported path (`${CYPRESS_BROWSER:?...}`) rather
than falling back to a browser name, so a missing export fails with a message
naming the step that should have set it.
- the step asserts the app is served at /AppStream/clickme before the suite
starts, because a deploy that streamed no frontend would otherwise surface as
twenty-six navigation failures.
400 lines
12 KiB
TypeScript
400 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.clearSession()
|
|
/* 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(), '_')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('8 | downloads viewer PDF report', (done) => {
|
|
visitPage('view/data')
|
|
|
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
|
|
openDownloadModal(() => {
|
|
cy.get('select')
|
|
.select('PDF')
|
|
.then(() => {
|
|
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
|
button.trigger('click')
|
|
|
|
const id = button[0].id
|
|
|
|
checkForFileDownloaded(id, 'pdf', () => done())
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
this.afterEach(() => {
|
|
cy.clearSession()
|
|
})
|
|
})
|
|
|
|
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')
|
|
}
|