import * as assert from 'assert' ; import { Application } from 'spectron' ; import { v4 as uuid } from 'uuid' ; import { load } from 'js-yaml' ; import * as Selectors from './selectors' ; import * as CLI from './cli' ; import * as Common from './common' ; import * as ReplExpect from './repl-expect' ; import * as SidecarExpect from './sidecar-expect' ; import { keys } from './keys' ; export interface AppAndCount { app : Application ; count : number ; splitIndex ? : number ; } /**
 * subset means that it is ok for struct1 to be a subset of struct2
 * so: every key in struct1 must be in struct2, but not vice versa
 *
 */ const sameStruct = ( struct1 , struct2 , subset = false ) : boolean => { if ( struct1 === struct2 ) { return true ; } else if ( typeof struct1 !== typeof struct2 ) { return false ; } for ( const key in struct1 ) { if ( ! ( key in struct2 ) ) { console . log ( `!(${ key } in struct2)` ) ; return false ; } else if ( struct1 [ key ] . constructor === RegExp ) { if ( ! struct1 [ key ] . test ( struct2 [ key ] ) ) { return false ; } } else if ( typeof struct1 [ key ] === 'function' ) { if ( ! struct1 [ key ] ( struct2 [ key ] ) ) { return false ; } } else if ( typeof struct1 [ key ] !== typeof struct2 [ key ] ) { console . log ( `typeof struct1[${ key }] !== typeof struct2[${ key }] ${ typeof struct1 [ key ] } ${ typeof struct2 [ key ] }` ) ; console . log ( struct1 ) ; console . log ( struct2 ) ; return false ; } else if ( typeof struct1 [ key ] === 'object' ) { if ( ! sameStruct ( struct1 [ key ] , struct2 [ key ] , subset ) ) { return false ; } } else if ( struct1 [ key ] !== struct2 [ key ] ) { console . log ( `struct1[${ key }] !== struct2[${ key }] ${ struct1 [ key ] } ${ struct2 [ key ] }` ) ; return false ; } } if ( subset ) return true ; for ( const key in struct2 ) { if ( ! ( key in struct1 ) ) { console . log ( `!(${ key } in struct1)` ) ; return false ; } else if ( typeof struct1 [ key ] === 'function' ) { if ( ! struct1 [ key ] ( struct2 [ key ] ) ) { return false ; } } else if ( typeof struct1 [ key ] !== typeof struct2 [ key ] ) { console . log ( `typeof struct1[${ key }] !== typeof struct2[${ key }] ${ typeof struct1 [ key ] } ${ typeof struct2 [ key ] }` ) ; console . log ( struct1 ) ; console . log ( struct2 ) ; return false ; } else if ( typeof struct2 [ key ] === 'object' ) { if ( ! sameStruct ( struct1 [ key ] , struct2 [ key ] , subset ) ) { return false ; } } else if ( struct1 [ key ] !== struct2 [ key ] ) { console . log ( `struct1[${ key }] !== struct2[${ key }] ${ struct1 [ key ] } ${ struct2 [ key ] }` ) ; return false ; } } return true ; } ; export const expectSubset = ( struct1 , failFast = true ) => ( str ) => { try { const ok = sameStruct ( struct1 , JSON . parse ( str ) , true ) ; if ( failFast ) { assert . ok ( ok ) ; } return true ; } catch ( err ) { console . error ( 'Error comparing subset for actual value=' + str ) ; throw err ; } } ; /** is the given struct2 the same as the given struct2 (given as a string) */ export const expectStruct = ( struct1 , noParse = false , failFast = true ) => ( str ) => { try { const ok = sameStruct ( struct1 , noParse ? str : JSON . parse ( str ) ) ; if ( failFast ) { assert . ok ( ok ) ; } return ok ; } catch ( err ) { console . error ( 'Error comparing structs for actual value=' + str ) ; throw err ; } } ; export const expectYAML = ( struct1 , subset = false , failFast = true ) => ( str ) => { try { const struct2 = load ( str ) ; const ok = sameStruct ( struct1 , typeof struct2 === 'object' ? struct2 : JSON . parse ( typeof struct2 === 'number' ? `${ struct2 }` : struct2 ) , subset ) ; if ( failFast ) { assert . ok ( ok ) ; } return ok ; } catch ( err ) { if ( failFast ) { return false ; } else { console . error ( 'Error comparing subset for actual value=' + str ) ; throw err ; } } } ; export const expectYAMLSubset = ( struct1 , failFast = true ) => expectYAML ( struct1 , true , failFast ) ; /** is the given actual array the same as the given expected array? */ export const expectArray = ( expected , failFast = true , subset = false ) => ( actual ) => { if ( ! Array . isArray ( actual ) ) { actual = [ actual ] ; } const matchFn = function ( u , i ) { return u === expected [ i ] ; } ; const ok = ! subset ? actual . length === expected . length && actual . every ( matchFn ) : actual . some ( matchFn ) ; if ( ! ok ) { console . error ( `array mismatch; expected=${ expected } actual=${ actual }` ) ; } if ( failFast ) { assert . ok ( ok ) ; } else { return ok ; } } ; /** Expand the fold on the given line of a monaco editor */ export async function clickToExpandMonacoFold ( res , lineIdx ) { const container = res . splitIndex !== undefined ? `${ Selectors . PROMPT_BLOCK_N_FOR_SPLIT ( res . count , res . splitIndex ) } .kui--tab-content:not([hidden])` : `${ Selectors . PROMPT_BLOCK_N ( res . count ) } .kui--tab-content:not([hidden])` ; console . log ( 'MF1' ) ; const selector = `${ container } .monaco-editor-wrapper` ; const wrapper = await res . app . client . $ ( selector ) ; await wrapper . waitForExist ( { timeout : CLI . waitTimeout } ) ; console . log ( 'MF2' ) ; const marginOverlaysSelector = `.margin-view-overlays > div:nth-child(${ lineIdx + 1 })` ; const foldingSelector = `${ marginOverlaysSelector } .codicon-folding-collapsed` ; const foldingClickable = await wrapper . $ ( foldingSelector ) ; console . log ( 'MF3' ) ; await foldingClickable . waitForExist ( { timeout : CLI . waitTimeout } ) ; console . log ( 'MF4' ) ; await foldingClickable . click ( ) ; console . log ( 'MF5' ) ; const unfoldingSelector = `${ marginOverlaysSelector } .codicon-folding-expanded` ; const unfoldingClickable = await wrapper . $ ( unfoldingSelector ) ; await unfoldingClickable . waitForExist ( { timeout : CLI . waitTimeout } ) ; console . log ( 'MF6' ) ; } /** get the monaco editor text */ export const getValueFromMonaco = async ( res , container ? ) => { if ( ! container ) { container = res . splitIndex !== undefined ? `${ Selectors . PROMPT_BLOCK_N_FOR_SPLIT ( res . count , res . splitIndex ) } .kui--tab-content:not([hidden])` : `${ Selectors . PROMPT_BLOCK_N ( res . count ) } .kui--tab-content:not([hidden])` ; } const selector = `${ container } .monaco-editor-wrapper` ; try { await res . app . client . $ ( selector ) . then ( _ => _ . waitForExist ( { timeout : CLI . waitTimeout } ) ) ; } catch ( err ) { console . error ( 'cannot find editor' , err ) ; await res . app . client . $ ( Selectors . SIDECAR ( res . count , res . splitIndex ) ) . then ( _ => _ . getHTML ( ) ) . then ( html => { console . log ( 'here is the content of the sidecar:' ) ; console . log ( html ) ; } ) ; throw err ; } return res . app . client . execute ( selector => { try { return ( ( document . querySelector ( selector ) as any ) as { getValueForTests : ( ) => string ; } ) . getValueForTests ( ) ; } catch ( err ) { console . error ( 'error in getValueFromMonaco1' , err ) ; } } , selector ) ; } ; export const waitForXtermInput = ( app , N ) => { const selector = `${ Selectors . PROMPT_BLOCK_N ( N ) } .xterm-helper-textarea` ; return app . client . $ ( selector ) . then ( _ => _ . waitForExist ( ) ) ; } ; export const expectText = ( app , expectedText , exact = true , timeout = CLI . waitTimeout ) => async ( selector ) => { let idx = 0 ; await app . client . waitUntil ( async ( ) => { const actualText = await app . client . $ ( selector ) . then ( _ => _ . getText ( ) ) ; if ( ++ idx > 5 ) { console . error ( `still waiting for text; actualText=${ actualText } expectedText=${ expectedText } selector=${ selector }` ) ; } if ( exact ) { return actualText === expectedText ; } else { if ( actualText . indexOf ( expectedText ) < 0 ) { console . error ( `Expected string not found: expected=${ expectedText } actual=${ actualText }` ) ; } return actualText . indexOf ( expectedText ) >= 0 ; } } , { timeout } ) ; return app ; } ; return export async function tabCount ( app ) { const topTabs = await app . client . $$ ( Selectors . TOP_TAB ) ; return topTabs . length ; } /** Close all except the first tab */ export function closeAllExceptFirstTab ( this , expectedInitialNTabs = 2 ) { it ( 'should close all but first tab' , async ( ) => { try { await this . app . client . waitUntil ( async ( ) => { const currentCount = await tabCount ( this . app ) ; return currentCount === expectedInitialNTabs ; } ) ; let nTabs = await tabCount ( this . app ) ; while ( nTabs > 1 ) { const N = nTabs -- ; await this . app . client . $ ( Selectors . TOP_TAB_CLOSE_N ( N ) ) . then ( _ => _ . click ( ) ) ; await this . app . client . waitUntil ( async ( ) => { const currentCount = await tabCount ( this . app ) ; return currentCount === N - 1 ; } ) ; } } catch ( err ) { await Common . oops ( this , true ) ( err ) ; } } ) ; } export function uniqueFileForSnapshot ( ) { return `/tmp/${ uuid ( ) }.kui` ; } /** Click the specified button action button */ export async function clickBlockActionButton ( res , buttonSelector ) { const N = res . count ; const prompt = await res . app . client . $ ( Selectors . PROMPT_N ( N ) ) ; await prompt . scrollIntoView ( ) ; await prompt . moveTo ( ) ; const removeButton = await res . app . client . $ ( buttonSelector ) ; await removeButton . scrollIntoView ( ) ; await removeButton . waitForDisplayed ( { timeout : CLI . waitTimeout } ) ; await removeButton . click ( ) ; } /** Click the close button on a block, and expect it to be gone */ export async function removeBlock ( res ) { return clickBlockActionButton ( res , Selectors . BLOCK_REMOVE_BUTTON ( res . count ) ) ; } /** Click the section button on a block, and expect it to be a section */ export async function markBlockAsSection ( res ) { return clickBlockActionButton ( res , Selectors . BLOCK_SECTION_BUTTON ( res . count ) ) ; } /** Click the section button on a block, and expect it to be a section */ export async function copyBlockLink ( res ) { return clickBlockActionButton ( res , Selectors . BLOCK_LINK_BUTTON ( res . count ) ) ; } /** Click the section button on a block, and expect it to be a section */ export async function rerunCommand ( res ) { return clickBlockActionButton ( res , Selectors . COMMAND_RERUN_BUTTON ( res . count ) ) ; } /** Switch sidecar tab */ export function switchToTab ( mode ) { return async ( res ) => { const tab = await res . app . client . $ ( Selectors . SIDECAR_MODE_BUTTON ( res . count , mode , res . splitIndex ) ) ; await tab . waitForDisplayed ( ) ; await tab . click ( ) ; await res . app . client . $ ( Selectors . SIDECAR_MODE_BUTTON_SELECTED ( res . count , mode , res . splitIndex ) ) . then ( _ => _ . waitForDisplayed ( ) ) ; return res ; } ; } /** Output of the given block */ export function outputOf ( res ) { return res . app . client . $ ( Selectors . OUTPUT_N ( res . count , res . splitIndex ) ) . then ( _ => _ . getText ( ) ) ; } /**
 * Execute `command` and expect a table with `name`
 *
 */ export async function doList ( ctx , command , name ) { try { const tableRes = await CLI . command ( command , ctx . app ) ; return ReplExpect . okWithCustom < string > ( { selector : Selectors . BY_NAME ( name ) } ) ( tableRes ) ; } catch ( err ) { await Common . oops ( ctx , true ) ( err ) ; } } export async function openSidecarByClick ( ctx , selector , name , mode ? , activationId ? , splitIndex = 2 , inNotebook = false ) { const app = ctx . app ; const currentSplitCount = ( await app . client . $$ ( Selectors . SPLITS ) ) . length ; const currentLastBlockIdx = currentSplitCount === splitIndex ? ( await CLI . lastBlock ( app , splitIndex , 1 , inNotebook ) ) . count : - 1 ; const clickOn = await app . client . $ ( selector ) ; await clickOn . waitForExist ( { timeout : CLI . waitTimeout } ) ; await clickOn . scrollIntoView ( false ) ; await clickOn . click ( ) ; await ReplExpect . splitCount ( splitIndex ) ( app ) ; await app . client . waitUntil ( async ( ) => { const sidecarRes = await CLI . lastBlock ( app , splitIndex , 1 , inNotebook ) ; return sidecarRes . count > currentLastBlockIdx ; } ) ; const sidecarRes = await CLI . lastBlock ( app , splitIndex , 1 , inNotebook ) ; await SidecarExpect . open ( sidecarRes ) . then ( SidecarExpect . showing ( name , activationId ) ) ; if ( mode ) { await SidecarExpect . mode ( mode ) ( sidecarRes ) ; } return sidecarRes ; } /**
 * Execute `command` and expect a table with `name`,
 * and then open sidecar by clicking the `name`
 *
 */ export async function listAndOpenSidecarNoWait ( ctx , command , name , mode ? ) { const selector = `${ await doList ( ctx , command , name ) } [data-value="${ name }"].clickable` ; return openSidecarByClick ( ctx , selector , name , mode ) ; } /**
 * Click sidecar mode button by `mode`
 *
 */ export async function clickSidecarModeButton ( ctx , res , mode ) { return ctx . app . client . $ ( Selectors . SIDECAR_MODE_BUTTON ( res . count , mode , res . splitIndex ) ) . then ( async ( _ ) => { await _ . waitForDisplayed ( ) ; await _ . click ( ) ; } ) ; } /**
 * Click a sidecar button with `selector`
 *
 */ export async function clickSidecarButtonCustomized ( ctx , res , selector ) { return ctx . app . client . $ ( `${ Selectors . SIDECAR ( res . count , res . splitIndex ) } ${ selector }` ) . then ( async ( _ ) => { await _ . waitForDisplayed ( ) ; await _ . click ( ) ; } ) ; } export function doCancel ( this , cmd = '' ) { return this . app . client . $ ( Selectors . CURRENT_PROMPT_BLOCK ) . then ( async ( _ ) => { await _ . waitForExist ( ) ; return _ . getAttribute ( 'data-input-count' ) ; } ) . then ( count => parseInt ( count , 10 ) ) . then ( count => this . app . client . keys ( cmd ) . then ( ( ) => this . app . client . keys ( keys . ctrlC ) ) . then ( ( ) => ( { app : this . app , count : count } ) ) . then ( ReplExpect . blank ) . then ( ( ) => this . app . client . $ ( Selectors . PROMPT_N ( count ) ) ) . then ( _ => _ . getText ( ) ) . then ( input => assert . strictEqual ( input , cmd ) ) ) . catch ( Common . oops ( this , true ) ) ; } export function switchToTopLevelTabViaClick ( ctx , N ) { return ctx . app . client . $ ( Selectors . TOP_TAB_N_CLICKABLE ( N ) ) . then ( _ => _ . click ( ) ) . then ( ( ) => ctx . app . client . $ ( Selectors . TAB_SELECTED_N ( N ) ) ) . then ( _ => _ . waitForDisplayed ( ) ) . catch ( Common . oops ( ctx , true ) ) ; } export function doClear ( this , residualBlockCount = 1 , splitIndex = 1 ) { return CLI . commandInSplit ( 'clear' , this . app , splitIndex ) . then ( ( ) => ReplExpect . consoleToBeClear ( this . app , residualBlockCount , splitIndex ) ) . then ( ( ) => SidecarExpect . closed ) . catch ( Common . oops ( this , true ) ) ; } export function clickNewTabButton ( ctx , expectedNewTabIndex ) { return ctx . app . client . $ ( Selectors . tabButtonSelector ) . then ( _ => _ . click ( ) ) . then ( ( ) => ctx . app . client . $ ( Selectors . TAB_SELECTED_N ( expectedNewTabIndex ) ) ) . then ( _ => _ . waitForDisplayed ( ) ) . then ( ( ) => CLI . waitForRepl ( ctx . app ) ) ; }