Adds PDF to getrawdata: the staged, filtered dataset is rendered by mp_ds2pdf and streamed with contenttype=PDF, so the Download modal gains a typeset report - landscape, SAS formats and labels applied, headers repeated on every page, 'Page n of m', and the filter, user and timestamp in the footer. The report is capped at 2000 observations, about 75 landscape pages, and the footer states the truncation when it bites. The cap is one macro variable driving both maxobs= and the footer text, so the two cannot disagree, and the footer itself is composed by mpe_pdffootnote so the truncation wording can be asserted without reading text out of a PDF. @sasjs/core goes 7.0.3 -> 7.1.0 for the macro; the bump also brings mp_validatecol ISPATH, two mp_dirlist guards and an mx_execute test. On Viya the report renders but the download does not arrive: mp_streamfile fails ahead of the PDF branch inside mp_binarycopy (RECFM=N with lrecl=1 needs 256), which is upstream of this change and left alone here.
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.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(), '_')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
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.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')
|
|
}
|