- zip entries carry flag bit 11 when the name is non-ASCII, so an extractor reads `BKP_` plus an accented filename back by name instead of CP437 - loadfile and stagedata name the staged CSV after the libds the request arrived with, before a hook can re-point the submission - stagedata writes that CSV at all (%mp_ds2csv does), so a grid submission's audit pack carries the same artefact as an upload's - loadfile writes no BKP_ file: loadfile.sas copies one only for a second, non-CSV fileref, which the mock does not accept - getauditfile says access is unenforced rather than that there is no permission store (MPE_SECURITY is seeded) - the special-missings clip no longer claims the estate carries a valid key
593 lines
21 KiB
TypeScript
593 lines
21 KiB
TypeScript
// Clip script: special missings inside Data Controller's validation rules.
|
|
//
|
|
// This is not a test - it is the recording script for the companion clip, and
|
|
// it deliberately pauses between steps so each beat is readable on video. It
|
|
// lives in cypress/clips (outside cypress/e2e) so the default spec pattern does
|
|
// not pick it up in CI.
|
|
//
|
|
// Record it with:
|
|
//
|
|
// npx cypress run --spec cypress/clips/special-missings-clip.cy.ts \
|
|
// --config video=true,viewportWidth=1280,viewportHeight=720
|
|
//
|
|
// The table is TESTDATA.DEMO_01 (seven columns, one rule each):
|
|
// ID (NOTNULL), AMOUNT (MINVAL 1), SCORE (MAXVAL 100),
|
|
// REF (SOFTREGEX /^[0-9]+$/), STATUS (SOFTSELECT TESTDATA.DEMO_01.STATUS),
|
|
// RATING (no rule), LAST_REVIEWED (date9.)
|
|
//
|
|
// One editing session, one submission. The rule scenes are played on row 1 and
|
|
// each value is put back afterwards, so the DIFF that follows carries only the
|
|
// two changes the clip is about; the review screens are reached through the
|
|
// app's own navigation rather than cy.visit, so nothing reloads mid-clip.
|
|
//
|
|
// The beats are deliberately short - the take is played back at ~1.4x on the
|
|
// way out, so a hold that looks tight here reads as a normal pause on the
|
|
// finished clip.
|
|
|
|
// Caption track: each mark opens a caption and carries the text it shows.
|
|
// The encoder turns consecutive marks into subtitle cues, so the captions are
|
|
// timed by the recording itself rather than by guessed offsets.
|
|
const BEATS = '/tmp/clip-beats.tsv'
|
|
|
|
const hostUrl = Cypress.env('hosturl')
|
|
const appLocation = Cypress.env('appLocation')
|
|
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
|
|
|
// A beat long enough to read on video.
|
|
const beat = (ms = 1500) => cy.wait(ms)
|
|
|
|
context('special missings clip (DEMO_01)', function () {
|
|
this.beforeEach(() => {
|
|
cy.visit(hostUrl + appLocation)
|
|
|
|
// Assumes the estate is already activated (the licensing spec registers a
|
|
// valid key); otherwise a licensing page would sit in shot.
|
|
cy.get('.nav-tree', { timeout: longerCommandTimeout }).should('exist')
|
|
})
|
|
|
|
it('records the demo in one editing session', () => {
|
|
cy.writeFile(BEATS, '', { flag: 'w' })
|
|
|
|
// ---- Scene 1: the table and its rules ----------------------------------
|
|
openTableFromTree('testdata', 'demo_01')
|
|
beat(1200)
|
|
|
|
clickOnEdit(() => {
|
|
// Seven columns, five rules: ID is the key, AMOUNT the floor, SCORE the
|
|
// ceiling, REF the pattern, STATUS the dropdown, and RATING and
|
|
// LAST_REVIEWED carry nothing.
|
|
cy.get('.ht_master tbody tr', { timeout: longerCommandTimeout }).should(
|
|
'have.length.greaterThan',
|
|
3
|
|
)
|
|
mark(
|
|
'grid',
|
|
'TESTDATA.DEMO_01 - eight columns, six rules. ID is the key, and RATING and LAST_REVIEWED carry none.'
|
|
)
|
|
beat(2600)
|
|
|
|
// ---- Scene 2: typing a special missing ------------------------------
|
|
// RATING has no rule, so this shows entry on its own. Every beat asserts
|
|
// the settled cell state before holding, so the recording always rests on
|
|
// the outcome (flagged or accepted) rather than on a blind pause that
|
|
// might land before the rule engine has run.
|
|
mark(
|
|
'entry',
|
|
'A numeric cell takes a special missing as a letter or an underscore, with or without a period - row 2 already holds .a.'
|
|
)
|
|
typeAndHold(0, 'RATING', 'a', 'accepted') // a single letter is taken
|
|
mark(
|
|
'reject',
|
|
'Two letters, or a letter mixed with a number, are refused. Red means it will not submit.'
|
|
)
|
|
typeAndHold(0, 'RATING', 'AB', 'rejected', 2200) // two letters are not
|
|
typeAndHold(0, 'RATING', '1a', 'rejected', 2200) // nor a number and a letter
|
|
typeAndHold(0, 'RATING', 'a', 'accepted') // leave it as a special missing
|
|
|
|
// ---- Scene 3: NOTNULL refuses both --------------------------------
|
|
// ID is the key, so NOTNULL applies. A special missing is not a value
|
|
// here: like a blank, it fails the rule (a real SAS NOT NULL constraint
|
|
// rejects a special missing as well), and only a number satisfies it -
|
|
// the original key, put back so the row carries no change into the DIFF.
|
|
mark(
|
|
'notnull_blank',
|
|
"ID is the table's primary key, so NOT NULL is applied to it automatically - a blank fails..."
|
|
)
|
|
typeAndHold(0, 'ID', '', 'rejected', 2200) // a blank fails NOTNULL
|
|
mark(
|
|
'notnull_missing',
|
|
'...and so does a special missing: a missing is not a value to NOT NULL.'
|
|
)
|
|
typeAndHold(0, 'ID', 'A', 'rejected', 2200) // so does a special missing
|
|
mark(
|
|
'notnull_number',
|
|
'A number satisfies it, and the row key goes back.'
|
|
)
|
|
typeAndHold(0, 'ID', '1', 'accepted') // a number is what it wants
|
|
|
|
// ---- Scene 4: the pattern still applies -----------------------------
|
|
// REF carries SOFTREGEX /^[0-9]+$/, and a special missing is not exempt
|
|
// from it. The amber cell is a soft rule warning rather than a block,
|
|
// which the caption on this beat says out loud - the pattern itself is
|
|
// only visible in the cell's native title, which a screencast does not
|
|
// capture.
|
|
mark(
|
|
'softregex',
|
|
'REF carries SOFTREGEX /^[0-9]+$/. Amber is a soft warning - it warns, it does not block.'
|
|
)
|
|
typeIntoCell(0, 'REF', 'A')
|
|
getCellByHeaderAndRow(0, 'REF').should('have.class', 'dc-warning-cell')
|
|
beat(3400)
|
|
typeIntoCell(0, 'REF', '1001') // put the reference back
|
|
getCellByHeaderAndRow(0, 'REF').should(
|
|
'not.have.class',
|
|
'dc-warning-cell'
|
|
)
|
|
beat(600)
|
|
|
|
// ---- Scene 5: the dropdown lists the missing -------------------------
|
|
// STATUS carries a SOFTSELECT whose list is taken from the column itself
|
|
// (the library.member.column form), and that column holds a special
|
|
// missing - so the dropdown offers it alongside the ordinary values, as
|
|
// the bare letter SAS produces for it.
|
|
mark(
|
|
'dropdown',
|
|
"STATUS carries a SOFTSELECT, and its list is the column's own values."
|
|
)
|
|
openDropdown(0, 'STATUS')
|
|
.should('have.length', 4)
|
|
.then(($items: any) => {
|
|
const texts = [...$items].map((td: any) => td.innerText.trim())
|
|
expect(texts).to.include('A')
|
|
})
|
|
beat(2400)
|
|
|
|
mark(
|
|
'dropdown_pick',
|
|
'So the special missing the column holds is offered as a bare letter, first - a missing sorts below every number.'
|
|
)
|
|
pickFromDropdown('A')
|
|
getCellByHeaderAndRow(0, 'STATUS').should('contain.text', 'A')
|
|
beat(1800)
|
|
typeIntoCell(0, 'STATUS', '1') // put the status back
|
|
beat(600)
|
|
|
|
// ---- Scene 6: a range rule compares in SAS order, missings included
|
|
mark(
|
|
'minval',
|
|
'AMOUNT has MINVAL 1. A missing sorts below every number, so it is below the floor.'
|
|
)
|
|
typeAndHold(0, 'AMOUNT', 'A', 'rejected', 2200) // a missing is below the floor
|
|
mark(
|
|
'maxval',
|
|
'SCORE has MAXVAL 100 - the same missing is below the ceiling, so it passes.'
|
|
)
|
|
typeAndHold(0, 'SCORE', 'A', 'accepted', 2200) // a missing is below the ceiling
|
|
mark(
|
|
'grade',
|
|
'GRADE takes MINVAL .A and MAXVAL .C. The missings have an order of their own: .B is inside the range, .D is outside it.'
|
|
)
|
|
typeAndHold(0, 'GRADE', '.B', 'accepted', 2400) // .B is between .A and .C
|
|
typeAndHold(0, 'GRADE', '.D', 'rejected', 2400) // .D is above .C
|
|
|
|
mark('abort', 'Submitting with an invalid cell aborts.')
|
|
|
|
// Submit while AMOUNT is invalid - the modal reports it.
|
|
submitTable(() => {
|
|
cy.get('.modal-body', { timeout: longerCommandTimeout }).should(
|
|
'contain.text',
|
|
'Invalid Values are Present'
|
|
)
|
|
beat(2400)
|
|
// Close the abort so the editor is clean for the next scene.
|
|
cy.get('clr-modal.clr-abort-modal .modal-footer button')
|
|
.contains('Close')
|
|
.click({ force: true })
|
|
beat(800)
|
|
})
|
|
|
|
// ---- Scene 7: put the test values back, then make the real change ----
|
|
mark(
|
|
'clean',
|
|
'Back to a clean row. Row 2 already holds .a, so this changes one special missing to another.'
|
|
)
|
|
typeIntoCell(0, 'AMOUNT', '120')
|
|
typeIntoCell(0, 'SCORE', '82')
|
|
typeIntoCell(0, 'GRADE', '.a')
|
|
typeIntoCell(0, 'RATING', '4')
|
|
beat(800)
|
|
|
|
// Row 2 (ID 2) already carries a special missing in RATING, so this is a
|
|
// change from one special missing to another, and the date column gives
|
|
// the formatted / unformatted switch something to switch.
|
|
typeAndHold(1, 'RATING', 'B', 'accepted')
|
|
typeIntoDateCell(1, 'LAST_REVIEWED', '2026-01-15')
|
|
beat(1500)
|
|
|
|
submitTable(() => {
|
|
cy.get('#submitBtn', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
.click()
|
|
beat(2500)
|
|
})
|
|
})
|
|
|
|
// ---- Scene 8: the queue, then the DIFF (in-app navigation) ------------
|
|
mark('submitted', 'Submitted - the queue shows it waiting for approval.')
|
|
goToReviewNav()
|
|
beat(1800)
|
|
|
|
// The submit queue lists oldest first, so the row we just created is last.
|
|
cy.get('app-submitter clr-datagrid clr-dg-row', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.should('exist')
|
|
.last()
|
|
.click({ force: true })
|
|
beat(1800)
|
|
|
|
cy.get('app-approve-details .card', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.should('be.visible')
|
|
beat(1000)
|
|
|
|
// The DIFF table is wider than the frame, so the two columns that matter
|
|
// (RATING and LAST_REVIEWED) sit off the right edge until it is scrolled.
|
|
scrollDiffToEnd()
|
|
beat(1200)
|
|
|
|
// Only those two cells changed: the rule scenes were put back, so the DIFF
|
|
// is the two changes the clip is about and nothing else.
|
|
getDiffCell('RATING')
|
|
.should('contain.text', '.b')
|
|
.should('have.class', 'ch')
|
|
getDiffCell('LAST_REVIEWED')
|
|
.should('contain.text', '15JAN2026')
|
|
.should('have.class', 'ch')
|
|
cy.get('app-approve-details .tableCont tbody tr td:not(.ch)').should(
|
|
'have.length.greaterThan',
|
|
3
|
|
)
|
|
mark(
|
|
'diff',
|
|
'The DIFF compares staged with base: one changed row, two changed cells.'
|
|
)
|
|
beat(2600)
|
|
|
|
// ---- Scene 9: the staged data -----------------------------------------
|
|
// What the approval is actually acting on: the staged row, still holding
|
|
// the special missing, before it reaches the base table.
|
|
mark(
|
|
'staged',
|
|
'The staged row, before approval - still holding the special missing.'
|
|
)
|
|
clickButton('VIEW STAGED DATA')
|
|
// 'Basic Submitted Details' is on the staged screen only - asserting
|
|
// 'Staged Data' alone would be satisfied by the button that was just
|
|
// clicked, which is how a beat can pass without ever leaving the DIFF.
|
|
cy.get('body', { timeout: longerCommandTimeout })
|
|
.should('contain.text', 'Basic Submitted Details')
|
|
.and('contain.text', 'Base Table')
|
|
beat(4200)
|
|
mark('staged_end', '')
|
|
|
|
// ---- Scene 10: the approver opens it, and switches the format ----------
|
|
goToReviewNav()
|
|
beat(1500)
|
|
openApproveTab()
|
|
beat(1500)
|
|
|
|
cy.get('app-approve clr-datagrid clr-dg-row a.color-green', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.should('exist')
|
|
.last()
|
|
.click({ force: true })
|
|
beat(1800)
|
|
|
|
cy.get('#acceptBtn', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.should('not.be.disabled')
|
|
beat(1000)
|
|
|
|
// Same scroll as the submitter view, so the date column is in frame when
|
|
// the format is switched.
|
|
scrollDiffToEnd()
|
|
beat(1200)
|
|
|
|
mark(
|
|
'approve',
|
|
'The approver opens the same submission. Hovering a changed cell shows the value it replaced.'
|
|
)
|
|
|
|
// Hovering the two changed cells shows what each replaced - that is how the
|
|
// two special missings are told apart, not just the before/after of one.
|
|
hoverDiffCell('RATING', 'Original value is: .a')
|
|
beat(2800)
|
|
|
|
mark('hover_date', '...including the date it replaced.')
|
|
hoverDiffCell('LAST_REVIEWED', 'Original value is: 29FEB2024')
|
|
beat(2800)
|
|
|
|
mark(
|
|
'toggle',
|
|
'The formatted / unformatted switch shows the value as SAS stores it: 24121.'
|
|
)
|
|
cy.get('.formatted-values-toggle').should('have.text', 'Formatted').click()
|
|
cy.get('.formatted-values-toggle').should('have.text', 'Unformatted')
|
|
getDiffCell('LAST_REVIEWED').should('contain.text', '24121')
|
|
getDiffCell('RATING').should('contain.text', '.b')
|
|
beat(2800)
|
|
|
|
cy.get('.formatted-values-toggle').click()
|
|
cy.get('.formatted-values-toggle').should('have.text', 'Formatted')
|
|
getDiffCell('LAST_REVIEWED').should('contain.text', '15JAN2026')
|
|
beat(1000)
|
|
|
|
// ---- Scene 11: approve, and the change is in the history --------------
|
|
mark('accepted', 'Accepted - the history records it as APPROVED.')
|
|
cy.get('#acceptBtn').click()
|
|
cy.url({ timeout: longerCommandTimeout }).should(
|
|
'include',
|
|
'/review/history'
|
|
)
|
|
cy.get('app-history clr-datagrid clr-dg-row', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.should('exist')
|
|
.first()
|
|
.should('contain.text', 'APPROVED')
|
|
beat(2800)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers (mirrored from the e2e specs so the clip drives the same UI paths)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Caption track. Each mark opens a caption and carries the text it shows; the
|
|
* encoder turns consecutive marks into subtitle cues, so the captions are timed
|
|
* by the recording itself rather than by guessed offsets.
|
|
*
|
|
* The timestamp has to be taken inside a `cy.then()`: Cypress evaluates a
|
|
* command's arguments when the command is *queued*, so `Date.now()` passed to
|
|
* `cy.writeFile` directly would give every mark the same value - the moment the
|
|
* spec body ran.
|
|
*/
|
|
const mark = (label: string, text: string) => {
|
|
cy.then(() => {
|
|
cy.writeFile(BEATS, `${label}\t${text}\t${Date.now()}\n`, { flag: 'a+' })
|
|
})
|
|
}
|
|
|
|
const typeIntoCell = (rowIndex: number, header: string, value: string) => {
|
|
getCellByHeaderAndRow(rowIndex, header)
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused().clear().type(`${value}{enter}`)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* A date-formatted column edits through an HTML date input, where cy.type()
|
|
* refuses anything but a bare YYYY-MM-DD string (so no {enter} in the same
|
|
* call). Set the value through the DOM and commit it with the Enter keydown
|
|
* that Handsontable listens for.
|
|
*/
|
|
const typeIntoDateCell = (rowIndex: number, header: string, value: string) => {
|
|
getCellByHeaderAndRow(rowIndex, header)
|
|
.dblclick({ force: true })
|
|
.then(() => {
|
|
cy.focused()
|
|
.then(($i: any) => {
|
|
const el = $i[0]
|
|
el.value = value
|
|
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
el.dispatchEvent(new Event('change', { bubbles: true }))
|
|
})
|
|
.trigger('keydown', { key: 'Enter', keyCode: 13, which: 13 })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Opens the selectbox on a cell and returns its list entries. Handsontable
|
|
* renders the arrow itself (`.htAutocompleteArrow`); the list is a
|
|
* `.handsontable.listbox` in the app document.
|
|
*/
|
|
const openDropdown = (rowIndex: number, header: string) => {
|
|
getCellByHeaderAndRow(rowIndex, header).within(() => {
|
|
cy.get('.htAutocompleteArrow').click({ force: true })
|
|
})
|
|
|
|
return cy.get('.handsontable.listbox td', { timeout: longerCommandTimeout })
|
|
}
|
|
|
|
/** Picks an entry from the open selectbox. */
|
|
const pickFromDropdown = (value: string) => {
|
|
cy.get('.handsontable.listbox td').contains(value).click({ force: true })
|
|
}
|
|
|
|
/** Reaches the review area through the app's own navigation (no reload). */
|
|
const goToReviewNav = () => {
|
|
cy.get('.nav-link', { timeout: longerCommandTimeout })
|
|
.contains('REVIEW')
|
|
.click({ force: true })
|
|
}
|
|
|
|
/** Opens the APPROVE tab within the review area. */
|
|
const openApproveTab = () => {
|
|
cy.get('.nav-link', { timeout: longerCommandTimeout })
|
|
.contains('APPROVE')
|
|
.click({ force: true })
|
|
}
|
|
|
|
/**
|
|
* Clicks a button by its visible text. The match is case-insensitive: the app
|
|
* uppercases button labels in CSS, so the rendered text and the DOM's
|
|
* textContent differ.
|
|
*/
|
|
const clickButton = (text: string) => {
|
|
cy.contains('button', new RegExp(text, 'i'), {
|
|
timeout: longerCommandTimeout
|
|
}).click({ force: true })
|
|
}
|
|
|
|
/**
|
|
* The DIFF table is wider than the recording frame, so scroll its container to
|
|
* the end - that is what puts the changed RATING and the date column on screen.
|
|
*/
|
|
const scrollDiffToEnd = () => {
|
|
cy.get('app-approve-details .tableCont', { timeout: longerCommandTimeout })
|
|
.should('exist')
|
|
.scrollTo('right', { duration: 1200 })
|
|
}
|
|
|
|
/**
|
|
* The DIFF on the review screen is a plain HTML table (`.tableCont`), not
|
|
* Handsontable: headers are `th`, cells `td`, and a changed cell also carries
|
|
* the `ch` class whose tooltip holds the value it replaced.
|
|
*/
|
|
const getDiffCell = (headerText: string) => {
|
|
return cy
|
|
.get('app-approve-details .tableCont thead tr th', {
|
|
timeout: longerCommandTimeout
|
|
})
|
|
.should(($ths) => {
|
|
const texts = [...$ths].map((th) => th.innerText.trim())
|
|
expect(texts).to.include(headerText)
|
|
})
|
|
.then(($ths) => {
|
|
const index = [...$ths].findIndex(
|
|
(th) => th.innerText.trim() === headerText
|
|
)
|
|
|
|
return cy
|
|
.get('app-approve-details .tableCont tbody tr')
|
|
.first()
|
|
.then(($tr: any) => $tr[0].childNodes[index])
|
|
.then((cell) => cy.get(cell))
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Reveals the value a changed DIFF cell replaced, and waits until it is really
|
|
* on screen.
|
|
*
|
|
* Clarity shows the tooltip through CSS :hover, which a synthetic
|
|
* `trigger('mouseover')` does NOT activate - the tooltip stays
|
|
* `visibility: hidden`, and a `contain.text` assertion still passes because the
|
|
* text is in the DOM. So this moves the real mouse (cypress-real-events) and
|
|
* then asserts the computed style. The first real move after another action can
|
|
* be swallowed, hence the repeat; if the tooltip ever fails to appear the
|
|
* recording fails rather than quietly showing nothing.
|
|
*/
|
|
const hoverDiffCell = (headerText: string, expected: string) => {
|
|
getDiffCell(headerText).realHover()
|
|
beat(300)
|
|
getDiffCell(headerText).realHover()
|
|
|
|
getDiffCell(headerText)
|
|
.find('.tooltip-content')
|
|
.should(($t) => {
|
|
const style = getComputedStyle($t[0] as HTMLElement)
|
|
expect(style.visibility, 'tooltip visibility').to.eq('visible')
|
|
expect(Number(style.opacity), 'tooltip opacity').to.be.greaterThan(0)
|
|
expect($t[0].textContent || '', 'tooltip text').to.contain(expected)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Types a value into a cell and holds on the settled result. The rule engine
|
|
* flags the cell (htInvalid) once the edit commits, so asserting the expected
|
|
* state before the hold means the recording always rests on the outcome, and
|
|
* waits for it however long the engine takes.
|
|
*
|
|
* @param expected 'rejected' when the rule engine should flag the cell,
|
|
* 'accepted' when the value should settle unflagged.
|
|
*/
|
|
const typeAndHold = (
|
|
rowIndex: number,
|
|
header: string,
|
|
value: string,
|
|
expected: 'accepted' | 'rejected',
|
|
hold = 1800
|
|
) => {
|
|
typeIntoCell(rowIndex, header, value)
|
|
|
|
getCellByHeaderAndRow(rowIndex, header).should(
|
|
expected === 'rejected' ? 'have.class' : 'not.have.class',
|
|
'htInvalid'
|
|
)
|
|
|
|
beat(hold)
|
|
}
|
|
|
|
const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => {
|
|
return cy
|
|
.get('.ht_clone_top .htCore thead tr th')
|
|
.should(($ths) => {
|
|
const texts = [...$ths].map((th) => th.innerText.trim())
|
|
expect(texts).to.include(headerText)
|
|
})
|
|
.then(($ths) => {
|
|
const index = [...$ths].findIndex(
|
|
(th) => th.innerText.trim() === headerText
|
|
)
|
|
|
|
return cy
|
|
.get('.ht_master tbody tr')
|
|
.then((rows: any) => rows[rowIndex].childNodes[index])
|
|
.then((cell) => cy.get(cell))
|
|
})
|
|
}
|
|
|
|
const clickOnEdit = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
|
.click()
|
|
.then(() => {
|
|
if (callback) callback()
|
|
})
|
|
}
|
|
|
|
const submitTable = (callback?: any) => {
|
|
cy.get('.btnCtrl button.btn-primary')
|
|
.click()
|
|
.then(() => {
|
|
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 libNode
|
|
|
|
for (let node of treeNodes) {
|
|
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
libNode = node
|
|
break
|
|
}
|
|
}
|
|
|
|
cy.get(libNode).within(() => {
|
|
cy.get('.clr-tree-node-content-container > button').click()
|
|
|
|
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
|
for (let innerNode of innerNodes) {
|
|
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
innerNode.click()
|
|
break
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|