2018-04-06 00:03:57 +02:00
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Copyright ( c ) Microsoft Corporation . All rights reserved .
* Licensed under the MIT License . See License . txt in the project root for license information .
* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
import * as path from 'path' ;
import * as cp from 'child_process' ;
2018-04-12 09:55:19 +02:00
import * as os from 'os' ;
2019-05-22 16:41:26 +02:00
import * as fs from 'fs' ;
2019-06-04 16:17:36 +02:00
import * as mkdirp from 'mkdirp' ;
2018-04-06 00:03:57 +02:00
import { tmpName } from 'tmp' ;
2021-07-06 13:15:43 +02:00
import { IDriver , connect as connectElectronDriver , IDisposable , IElement , Thenable , ILocalizedStrings , ILocaleInfo } from './driver' ;
2020-02-04 17:23:27 +01:00
import { connect as connectPlaywrightDriver , launch } from './playwrightDriver' ;
2019-08-14 15:50:14 +02:00
import { Logger } from './logger' ;
2019-05-22 16:41:26 +02:00
import { ncp } from 'ncp' ;
2019-07-12 21:55:45 +02:00
import { URI } from 'vscode-uri' ;
2018-04-06 00:03:57 +02:00
2019-08-14 15:50:14 +02:00
const repoPath = path . join ( __dirname , '../../..' ) ;
2018-04-06 00:03:57 +02:00
2019-07-20 02:12:11 +02:00
function getDevElectronPath ( ) : string {
const buildPath = path . join ( repoPath , '.build' ) ;
const product = require ( path . join ( repoPath , 'product.json' ) ) ;
switch ( process . platform ) {
case 'darwin' :
return path . join ( buildPath , 'electron' , ` ${ product . nameLong } .app ` , 'Contents' , 'MacOS' , 'Electron' ) ;
case 'linux' :
return path . join ( buildPath , 'electron' , ` ${ product . applicationName } ` ) ;
case 'win32' :
return path . join ( buildPath , 'electron' , ` ${ product . nameShort } .exe ` ) ;
default :
throw new Error ( 'Unsupported platform.' ) ;
}
}
function getBuildElectronPath ( root : string ) : string {
switch ( process . platform ) {
case 'darwin' :
return path . join ( root , 'Contents' , 'MacOS' , 'Electron' ) ;
case 'linux' : {
const product = require ( path . join ( root , 'resources' , 'app' , 'product.json' ) ) ;
return path . join ( root , product . applicationName ) ;
}
case 'win32' : {
const product = require ( path . join ( root , 'resources' , 'app' , 'product.json' ) ) ;
return path . join ( root , ` ${ product . nameShort } .exe ` ) ;
}
default :
throw new Error ( 'Unsupported platform.' ) ;
}
}
2018-04-06 00:03:57 +02:00
function getDevOutPath ( ) : string {
return path . join ( repoPath , 'out' ) ;
}
function getBuildOutPath ( root : string ) : string {
switch ( process . platform ) {
case 'darwin' :
return path . join ( root , 'Contents' , 'Resources' , 'app' , 'out' ) ;
default :
return path . join ( root , 'resources' , 'app' , 'out' ) ;
}
}
2021-11-26 18:08:23 +01:00
async function connect ( connectDriver : typeof connectElectronDriver | typeof connectPlaywrightDriver , child : cp.ChildProcess | undefined , outPath : string , handlePath : string , logger : Logger ) : Promise < Code > {
2018-04-06 00:03:57 +02:00
let errCount = 0 ;
while ( true ) {
try {
const { client , driver } = await connectDriver ( outPath , handlePath ) ;
2021-11-23 10:31:46 +01:00
return new Code ( client , driver , logger , child ? . pid ) ;
2018-04-06 00:03:57 +02:00
} catch ( err ) {
if ( ++ errCount > 50 ) {
2019-07-20 02:16:04 +02:00
if ( child ) {
child . kill ( ) ;
}
2018-04-06 00:03:57 +02:00
throw err ;
}
// retry
2021-11-26 18:08:23 +01:00
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
2018-04-06 00:03:57 +02:00
}
}
}
2018-04-06 00:06:56 +02:00
// Kill all running instances, when dead
const instances = new Set < cp.ChildProcess > ( ) ;
process . once ( 'exit' , ( ) = > instances . forEach ( code = > code . kill ( ) ) ) ;
2018-04-11 12:20:37 +02:00
export interface SpawnOptions {
codePath? : string ;
workspacePath : string ;
userDataDir : string ;
extensionsPath : string ;
2018-04-18 15:45:27 +02:00
logger : Logger ;
2018-05-02 12:00:31 +02:00
verbose? : boolean ;
2018-04-11 12:20:37 +02:00
extraArgs? : string [ ] ;
2018-06-05 15:24:41 +02:00
log? : string ;
2019-04-10 22:32:45 +02:00
remote? : boolean ;
2019-07-20 02:12:11 +02:00
web? : boolean ;
2021-07-08 19:10:25 +02:00
headless? : boolean ;
2020-02-04 17:23:27 +01:00
browser ? : 'chromium' | 'webkit' | 'firefox' ;
2018-04-11 12:20:37 +02:00
}
2018-04-12 09:55:19 +02:00
async function createDriverHandle ( ) : Promise < string > {
if ( 'win32' === os . platform ( ) ) {
const name = [ . . . Array ( 15 ) ] . map ( ( ) = > Math . random ( ) . toString ( 36 ) [ 3 ] ) . join ( '' ) ;
return ` \\ \\ . \\ pipe \\ ${ name } ` ;
} else {
return await new Promise < string > ( ( c , e ) = > tmpName ( ( err , handlePath ) = > err ? e ( err ) : c ( handlePath ) ) ) ;
}
}
2018-04-06 00:03:57 +02:00
export async function spawn ( options : SpawnOptions ) : Promise < Code > {
2018-04-12 09:55:19 +02:00
const handle = await createDriverHandle ( ) ;
2018-04-06 00:03:57 +02:00
2020-02-04 17:23:27 +01:00
let child : cp.ChildProcess | undefined ;
2021-02-03 19:20:44 +01:00
copyExtension ( options . extensionsPath , 'vscode-notebook-tests' ) ;
2020-06-08 00:27:47 +02:00
2020-02-04 17:23:27 +01:00
if ( options . web ) {
2021-06-15 08:33:26 +02:00
await launch ( options . userDataDir , options . workspacePath , options . codePath , options . extensionsPath , Boolean ( options . verbose ) ) ;
2021-11-26 18:08:23 +01:00
return connect ( connectPlaywrightDriver . bind ( connectPlaywrightDriver , options ) , child , '' , handle , options . logger ) ;
2020-02-26 11:55:43 +01:00
}
2018-04-06 00:03:57 +02:00
2021-06-11 12:19:17 +02:00
const env = { . . . process . env } ;
2020-02-26 11:55:43 +01:00
const codePath = options . codePath ;
2021-11-10 08:13:56 +01:00
const logsPath = path . join ( repoPath , '.build' , 'logs' , options . remote ? 'smoke-tests-remote' : 'smoke-tests' ) ;
2020-02-26 11:55:43 +01:00
const outPath = codePath ? getBuildOutPath ( codePath ) : getDevOutPath ( ) ;
2018-05-02 12:00:31 +02:00
2020-02-26 11:55:43 +01:00
const args = [
options . workspacePath ,
2020-04-29 12:40:04 +02:00
'--skip-release-notes' ,
2021-06-09 20:36:42 +02:00
'--skip-welcome' ,
2020-02-26 11:55:43 +01:00
'--disable-telemetry' ,
2020-06-09 10:29:01 +02:00
'--no-cached-data' ,
2020-02-26 11:55:43 +01:00
'--disable-updates' ,
2021-02-17 15:09:40 +01:00
'--disable-keytar' ,
2020-02-26 11:55:43 +01:00
'--disable-crash-reporter' ,
2021-06-03 10:21:19 +02:00
'--disable-workspace-trust' ,
2020-02-26 11:55:43 +01:00
` --extensions-dir= ${ options . extensionsPath } ` ,
` --user-data-dir= ${ options . userDataDir } ` ,
2021-11-10 08:13:56 +01:00
` --logsPath= ${ logsPath } ` ,
2020-02-26 11:55:43 +01:00
'--driver' , handle
] ;
2021-02-23 19:04:47 +01:00
if ( process . platform === 'linux' ) {
args . push ( '--disable-gpu' ) ; // Linux has trouble in VMs to render properly with GPU enabled
}
2020-02-26 11:55:43 +01:00
if ( options . remote ) {
// Replace workspace path with URI
args [ 0 ] = ` -- ${ options . workspacePath . endsWith ( '.code-workspace' ) ? 'file' : 'folder' } -uri=vscode-remote://test+test/ ${ URI . file ( options . workspacePath ) . path } ` ;
if ( codePath ) {
// running against a build: copy the test resolver extension
2021-02-03 19:20:44 +01:00
copyExtension ( options . extensionsPath , 'vscode-test-resolver' ) ;
2020-02-04 17:23:27 +01:00
}
2020-02-26 11:55:43 +01:00
args . push ( '--enable-proposed-api=vscode.vscode-test-resolver' ) ;
const remoteDataDir = ` ${ options . userDataDir } -server ` ;
mkdirp . sync ( remoteDataDir ) ;
2021-02-03 19:20:44 +01:00
if ( codePath ) {
// running against a build: copy the test resolver extension into remote extensions dir
const remoteExtensionsDir = path . join ( remoteDataDir , 'extensions' ) ;
mkdirp . sync ( remoteExtensionsDir ) ;
copyExtension ( remoteExtensionsDir , 'vscode-notebook-tests' ) ;
}
2020-02-26 11:55:43 +01:00
env [ 'TESTRESOLVER_DATA_FOLDER' ] = remoteDataDir ;
2021-11-10 08:13:56 +01:00
env [ 'TESTRESOLVER_LOGS_FOLDER' ] = path . join ( logsPath , 'server' ) ;
2020-02-26 11:55:43 +01:00
}
2018-06-05 15:24:41 +02:00
2021-07-08 19:10:25 +02:00
const spawnOptions : cp.SpawnOptions = { env } ;
2020-06-08 00:27:47 +02:00
2020-05-20 19:01:16 +02:00
args . push ( '--enable-proposed-api=vscode.vscode-notebook-tests' ) ;
2020-02-26 11:55:43 +01:00
if ( ! codePath ) {
args . unshift ( repoPath ) ;
}
2018-04-11 12:20:37 +02:00
2020-02-26 11:55:43 +01:00
if ( options . verbose ) {
args . push ( '--driver-verbose' ) ;
2021-07-08 19:10:25 +02:00
spawnOptions . stdio = [ 'ignore' , 'inherit' , 'inherit' ] ;
2020-02-26 11:55:43 +01:00
}
2018-04-06 18:07:29 +02:00
2020-02-26 11:55:43 +01:00
if ( options . log ) {
args . push ( '--log' , options . log ) ;
2019-07-20 02:12:11 +02:00
}
2020-02-04 17:23:27 +01:00
2020-02-26 11:55:43 +01:00
if ( options . extraArgs ) {
args . push ( . . . options . extraArgs ) ;
}
const electronPath = codePath ? getBuildElectronPath ( codePath ) : getDevElectronPath ( ) ;
child = cp . spawn ( electronPath , args , spawnOptions ) ;
instances . add ( child ) ;
child . once ( 'exit' , ( ) = > instances . delete ( child ! ) ) ;
2021-11-26 18:08:23 +01:00
return connect ( connectElectronDriver , child , outPath , handle , options . logger ) ;
2018-04-11 16:20:09 +02:00
}
2021-02-03 19:20:44 +01:00
async function copyExtension ( extensionsPath : string , extId : string ) : Promise < void > {
const dest = path . join ( extensionsPath , extId ) ;
if ( ! fs . existsSync ( dest ) ) {
2020-05-20 19:01:16 +02:00
const orig = path . join ( repoPath , 'extensions' , extId ) ;
2021-02-23 19:04:47 +01:00
await new Promise < void > ( ( c , e ) = > ncp ( orig , dest , err = > err ? e ( err ) : c ( ) ) ) ;
2020-05-20 19:01:16 +02:00
}
}
2018-04-13 09:45:06 +02:00
async function poll < T > (
2018-12-10 11:27:48 +01:00
fn : ( ) = > Thenable < T > ,
2018-04-13 09:45:06 +02:00
acceptFn : ( result : T ) = > boolean ,
timeoutMessage : string ,
retryCount : number = 200 ,
retryInterval : number = 100 // millis
) : Promise < T > {
let trial = 1 ;
2018-05-02 12:41:42 +02:00
let lastError : string = '' ;
2018-04-13 09:45:06 +02:00
while ( true ) {
if ( trial > retryCount ) {
2018-05-02 12:41:42 +02:00
console . error ( '** Timeout!' ) ;
console . error ( lastError ) ;
2021-11-17 23:00:01 +01:00
console . error ( ` Timeout: ${ timeoutMessage } after ${ ( retryCount * retryInterval ) / 1000 } seconds. ` ) ;
2018-04-13 09:45:06 +02:00
throw new Error ( ` Timeout: ${ timeoutMessage } after ${ ( retryCount * retryInterval ) / 1000 } seconds. ` ) ;
}
let result ;
try {
result = await fn ( ) ;
if ( acceptFn ( result ) ) {
return result ;
2018-05-02 12:41:42 +02:00
} else {
lastError = 'Did not pass accept function' ;
2018-04-13 09:45:06 +02:00
}
2021-06-11 12:19:17 +02:00
} catch ( e : any ) {
2018-05-02 12:41:42 +02:00
lastError = Array . isArray ( e . stack ) ? e . stack . join ( os . EOL ) : e . stack ;
2018-04-13 09:45:06 +02:00
}
await new Promise ( resolve = > setTimeout ( resolve , retryInterval ) ) ;
trial ++ ;
}
}
2018-04-11 16:20:09 +02:00
export class Code {
private _activeWindowId : number | undefined = undefined ;
2021-11-17 23:00:01 +01:00
driver : IDriver ;
2018-04-11 16:20:09 +02:00
constructor (
2019-08-15 16:52:23 +02:00
private client : IDisposable ,
2018-04-11 17:25:37 +02:00
driver : IDriver ,
2021-11-23 10:31:46 +01:00
readonly logger : Logger ,
private readonly pid : number | undefined
2018-04-11 17:25:37 +02:00
) {
2018-04-18 15:45:27 +02:00
this . driver = new Proxy ( driver , {
get ( target , prop , receiver ) {
2018-06-08 14:42:38 +02:00
if ( typeof prop === 'symbol' ) {
throw new Error ( 'Invalid usage' ) ;
}
2019-08-14 15:50:14 +02:00
const targetProp = ( target as any ) [ prop ] ;
if ( typeof targetProp !== 'function' ) {
return targetProp ;
2018-04-11 17:25:37 +02:00
}
2018-04-18 15:45:27 +02:00
2019-08-14 15:50:14 +02:00
return function ( this : any , . . . args : any [ ] ) {
2018-04-18 15:45:27 +02:00
logger . log ( ` ${ prop } ` , . . . args . filter ( a = > typeof a === 'string' ) ) ;
2019-08-14 15:50:14 +02:00
return targetProp . apply ( this , args ) ;
2018-04-18 15:45:27 +02:00
} ;
}
} ) ;
2018-04-11 17:25:37 +02:00
}
2018-04-11 16:20:09 +02:00
2018-04-18 08:14:38 +02:00
async capturePage ( ) : Promise < string > {
const windowId = await this . getActiveWindowId ( ) ;
return await this . driver . capturePage ( windowId ) ;
}
2018-04-11 17:25:51 +02:00
async waitForWindowIds ( fn : ( windowIds : number [ ] ) = > boolean ) : Promise < void > {
2018-04-13 09:45:06 +02:00
await poll ( ( ) = > this . driver . getWindowIds ( ) , fn , ` get window ids ` ) ;
2018-04-11 17:25:51 +02:00
}
2018-04-11 17:25:37 +02:00
async dispatchKeybinding ( keybinding : string ) : Promise < void > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
await this . driver . dispatchKeybinding ( windowId , keybinding ) ;
}
2019-01-03 02:28:38 +01:00
async exit ( ) : Promise < void > {
2021-11-25 14:37:22 +01:00
return new Promise < void > ( ( resolve , reject ) = > {
let done = false ;
// Start the exit flow via driver
const exitPromise = this . driver . exitApplication ( ) . then ( veto = > {
if ( veto ) {
done = true ;
reject ( new Error ( 'Smoke test exit call resulted in unexpected veto' ) ) ;
}
} ) ;
// If we know the `pid` of the smoke tested application
// use that as way to detect the exit of the application
const pid = this . pid ;
if ( typeof pid === 'number' ) {
( async ( ) = > {
let killCounter = 0 ;
while ( ! done ) {
killCounter ++ ;
if ( killCounter > 40 ) {
done = true ;
reject ( new Error ( 'Smoke test exit call did not terminate main process after 20s, giving up' ) ) ;
}
try {
process . kill ( pid , 0 ) ; // throws an exception if the main process doesn't exist anymore.
await new Promise ( resolve = > setTimeout ( resolve , 500 ) ) ;
} catch ( error ) {
done = true ;
resolve ( ) ;
}
}
} ) ( ) ;
}
// Otherwise await the exit promise (web).
else {
( async ( ) = > {
2021-11-23 10:31:46 +01:00
try {
2021-11-25 14:37:22 +01:00
await exitPromise ;
resolve ( ) ;
2021-11-23 10:31:46 +01:00
} catch ( error ) {
2021-11-25 14:37:22 +01:00
reject ( new Error ( ` Smoke test exit call resulted in error: ${ error } ` ) ) ;
2021-11-23 10:31:46 +01:00
}
2021-11-25 14:37:22 +01:00
} ) ( ) ;
}
} ) ;
2019-01-03 02:28:38 +01:00
}
2021-01-27 15:43:59 +01:00
async waitForTextContent ( selector : string , textContent? : string , accept ? : ( result : string ) = > boolean , retryCount? : number ) : Promise < string > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
2018-12-28 22:15:41 +01:00
accept = accept || ( result = > textContent !== undefined ? textContent === result : ! ! result ) ;
2018-05-03 14:39:57 +02:00
return await poll (
( ) = > this . driver . getElements ( windowId , selector ) . then ( els = > els . length > 0 ? Promise . resolve ( els [ 0 ] . textContent ) : Promise . reject ( new Error ( 'Element not found for textContent' ) ) ) ,
s = > accept ! ( typeof s === 'string' ? s : '' ) ,
2021-01-27 15:43:59 +01:00
` get text content ' ${ selector } ' ` ,
retryCount
2018-05-03 14:39:57 +02:00
) ;
2018-04-11 16:20:09 +02:00
}
2021-05-28 11:38:10 +02:00
async waitAndClick ( selector : string , xoffset? : number , yoffset? : number , retryCount : number = 200 ) : Promise < void > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
2021-05-28 11:38:10 +02:00
await poll ( ( ) = > this . driver . click ( windowId , selector , xoffset , yoffset ) , ( ) = > true , ` click ' ${ selector } ' ` , retryCount ) ;
2018-04-11 16:20:09 +02:00
}
async waitAndDoubleClick ( selector : string ) : Promise < void > {
const windowId = await this . getActiveWindowId ( ) ;
2018-04-13 09:45:06 +02:00
await poll ( ( ) = > this . driver . doubleClick ( windowId , selector ) , ( ) = > true , ` double click ' ${ selector } ' ` ) ;
2018-04-11 16:20:09 +02:00
}
2018-04-11 17:25:37 +02:00
async waitForSetValue ( selector : string , value : string ) : Promise < void > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
2018-04-13 09:45:06 +02:00
await poll ( ( ) = > this . driver . setValue ( windowId , selector , value ) , ( ) = > true , ` set value ' ${ selector } ' ` ) ;
2018-04-11 16:20:09 +02:00
}
async waitForElements ( selector : string , recursive : boolean , accept : ( result : IElement [ ] ) = > boolean = result = > result . length > 0 ) : Promise < IElement [ ] > {
const windowId = await this . getActiveWindowId ( ) ;
2018-04-13 09:45:06 +02:00
return await poll ( ( ) = > this . driver . getElements ( windowId , selector , recursive ) , accept , ` get elements ' ${ selector } ' ` ) ;
2018-04-11 16:20:09 +02:00
}
2018-04-13 10:11:14 +02:00
async waitForElement ( selector : string , accept : ( result : IElement | undefined ) = > boolean = result = > ! ! result , retryCount : number = 200 ) : Promise < IElement > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
2018-04-13 10:11:14 +02:00
return await poll < IElement > ( ( ) = > this . driver . getElements ( windowId , selector ) . then ( els = > els [ 0 ] ) , accept , ` get element ' ${ selector } ' ` , retryCount ) ;
2018-04-11 16:20:09 +02:00
}
2018-04-13 09:45:06 +02:00
async waitForActiveElement ( selector : string , retryCount : number = 200 ) : Promise < void > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
2018-04-13 09:45:06 +02:00
await poll ( ( ) = > this . driver . isActiveElement ( windowId , selector ) , r = > r , ` is active element ' ${ selector } ' ` , retryCount ) ;
2018-04-11 16:20:09 +02:00
}
2018-04-11 16:48:42 +02:00
async waitForTitle ( fn : ( title : string ) = > boolean ) : Promise < void > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
2018-04-13 09:45:06 +02:00
await poll ( ( ) = > this . driver . getTitle ( windowId ) , fn , ` get title ` ) ;
2018-04-11 16:20:09 +02:00
}
2018-04-11 17:25:37 +02:00
async waitForTypeInEditor ( selector : string , text : string ) : Promise < void > {
2018-04-11 16:20:09 +02:00
const windowId = await this . getActiveWindowId ( ) ;
2018-04-13 09:45:06 +02:00
await poll ( ( ) = > this . driver . typeInEditor ( windowId , selector , text ) , ( ) = > true , ` type in editor ' ${ selector } ' ` ) ;
2018-04-11 16:20:09 +02:00
}
2018-04-18 11:53:40 +02:00
async waitForTerminalBuffer ( selector : string , accept : ( result : string [ ] ) = > boolean ) : Promise < void > {
const windowId = await this . getActiveWindowId ( ) ;
await poll ( ( ) = > this . driver . getTerminalBuffer ( windowId , selector ) , accept , ` get terminal buffer ' ${ selector } ' ` ) ;
}
2018-05-02 12:29:39 +02:00
async writeInTerminal ( selector : string , value : string ) : Promise < void > {
const windowId = await this . getActiveWindowId ( ) ;
await poll ( ( ) = > this . driver . writeInTerminal ( windowId , selector , value ) , ( ) = > true , ` writeInTerminal ' ${ selector } ' ` ) ;
}
2021-07-06 13:15:43 +02:00
async getLocaleInfo ( ) : Promise < ILocaleInfo > {
const windowId = await this . getActiveWindowId ( ) ;
return await this . driver . getLocaleInfo ( windowId ) ;
}
2021-06-21 13:54:16 +02:00
async getLocalizedStrings ( ) : Promise < ILocalizedStrings > {
const windowId = await this . getActiveWindowId ( ) ;
return await this . driver . getLocalizedStrings ( windowId ) ;
}
2018-04-11 16:20:09 +02:00
private async getActiveWindowId ( ) : Promise < number > {
if ( typeof this . _activeWindowId !== 'number' ) {
const windows = await this . driver . getWindowIds ( ) ;
this . _activeWindowId = windows [ 0 ] ;
}
return this . _activeWindowId ;
}
dispose ( ) : void {
2019-08-15 16:52:23 +02:00
this . client . dispose ( ) ;
2018-04-11 16:20:09 +02:00
}
}
export function findElement ( element : IElement , fn : ( element : IElement ) = > boolean ) : IElement | null {
const queue = [ element ] ;
while ( queue . length > 0 ) {
const element = queue . shift ( ) ! ;
if ( fn ( element ) ) {
return element ;
}
queue . push ( . . . element . children ) ;
}
return null ;
}
export function findElements ( element : IElement , fn : ( element : IElement ) = > boolean ) : IElement [ ] {
const result : IElement [ ] = [ ] ;
const queue = [ element ] ;
while ( queue . length > 0 ) {
const element = queue . shift ( ) ! ;
if ( fn ( element ) ) {
result . push ( element ) ;
}
queue . push ( . . . element . children ) ;
}
return result ;
2019-08-09 20:21:40 +02:00
}