DEMO_01 gains a GRADE column carrying MINVAL .A with MAXVAL .C, so the demo table can show a range written in special missings: .B is inside it and .D is outside. The clip's range-rule scene is rebuilt around the order - the same special missing fails AMOUNT (MINVAL 1, below every number), passes SCORE (MAXVAL 100, below the ceiling), and in GRADE .B is taken while .D is refused. The captions from the previous take claimed a range rule steps aside for a missing value, which the engine no longer does, so they are corrected along with the scene.
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)
|
|
|
|
// The mock estate carries a valid licence key (mock-storage/licence.json),
|
|
// so the app activates directly with no free-tier banner 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
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|