@ -1,25 +1,57 @@
import angular from 'angular'
import * as angular from 'angular'
import {
ICompileService ,
IScope ,
IRootScopeService ,
IIntervalService ,
IFlushPendingTasksService ,
} from 'angular'
import 'angular-mocks'
import 'angular-mocks'
import {
import {
getQueriesForElement ,
getQueriesForElement ,
prettyDOM ,
prettyDOM ,
fireEvent as dtlFireEvent ,
fireEvent as dtlFireEvent ,
wait as dtlWait ,
wait as dtlWait ,
queries as dtlQueries ,
Queries ,
BoundFunction ,
} from '@testing-library/dom'
} from '@testing-library/dom'
const mountedContainers = new Set ( )
const mountedContainers = new Set < HTMLElement > ( )
const mountedScopes = new Set ( )
const mountedScopes = new Set < IScope > ( )
type RenderOptions < Q extends Queries = typeof dtlQueries > = {
container? : HTMLElement
baseElement? : HTMLElement
queries? : Q
scope? : object
ignoreUnknownElements? : boolean
}
type RenderResult < Q extends Queries = typeof dtlQueries > = {
container : HTMLElement
baseElement : HTMLElement
debug : (
baseElement ? :
| HTMLElement
| DocumentFragment
| Array < HTMLElement | DocumentFragment > ,
) = > void
unmount : ( ) = > void
asFragment : ( ) = > DocumentFragment
$scope : IScope
} & { [ P in keyof Q ] : BoundFunction < Q [ P ] > }
function render (
function render (
ui ,
ui : string ,
{
{
container ,
container ,
baseElement = container ,
baseElement = container ,
queries ,
queries ,
scope ,
scope ,
ignoreUnknownElements ,
ignoreUnknownElements = false ,
} = { } ,
} : RenderOptions = { } ,
) {
) : RenderResult {
if ( ! baseElement ) {
if ( ! baseElement ) {
// default to document.body instead of documentElement to avoid output of potentially-large
// default to document.body instead of documentElement to avoid output of potentially-large
// head elements (such as JSS style blocks) in debug output
// head elements (such as JSS style blocks) in debug output
@ -34,8 +66,8 @@ function render(
// they're passing us a custom container or not.
// they're passing us a custom container or not.
mountedContainers . add ( container )
mountedContainers . add ( container )
const $rootScope = inject ('$rootScope' )
const $rootScope = inject <IRootScopeService > ('$rootScope' )
const $compile = inject ('$compile' )
const $compile = inject <ICompileService > ('$compile' )
const $scope = $rootScope . $new ( )
const $scope = $rootScope . $new ( )
Object . assign ( $scope , scope )
Object . assign ( $scope , scope )
@ -56,19 +88,18 @@ function render(
debug : ( el = baseElement ) = >
debug : ( el = baseElement ) = >
Array . isArray ( el )
Array . isArray ( el )
? // eslint-disable-next-line no-console
? // eslint-disable-next-line no-console
el . forEach ( e = > console . log ( prettyDOM ( e ) ) )
el . forEach ( e = > console . log ( prettyDOM ( e as HTMLElement ) ) )
: // eslint-disable-next-line no-console,
: // eslint-disable-next-line no-console,
console . log ( prettyDOM ( el ) ) ,
console . log ( prettyDOM ( el as HTMLElement ) ) ,
asFragment : ( ) = > {
asFragment : ( ) = > {
const source = container as HTMLElement
/* istanbul ignore if (jsdom limitation) */
/* istanbul ignore if (jsdom limitation) */
if ( typeof document . createRange === 'function' ) {
if ( typeof document . createRange === 'function' ) {
return document
return document . createRange ( ) . createContextualFragment ( source . innerHTML )
. createRange ( )
. createContextualFragment ( container . innerHTML )
}
}
const template = document . createElement ( 'template' )
const template = document . createElement ( 'template' )
template . innerHTML = container . innerHTML
template . innerHTML = source . innerHTML
return template . content
return template . content
} ,
} ,
$scope ,
$scope ,
@ -86,28 +117,28 @@ function cleanup() {
// maybe one day we'll expose this (perhaps even as a utility returned by render).
// maybe one day we'll expose this (perhaps even as a utility returned by render).
// but let's wait until someone asks for it.
// but let's wait until someone asks for it.
function cleanupAtContainer ( container ) {
function cleanupAtContainer ( container : HTMLElement ) {
if ( container . parentNode === document . body ) {
if ( container . parentNode === document . body ) {
document . body . removeChild ( container )
document . body . removeChild ( container )
}
}
mountedContainers . delete ( container )
mountedContainers . delete ( container )
}
}
function cleanupScope ( scope ) {
function cleanupScope ( scope : IScope ) {
scope . $destroy ( )
scope . $destroy ( )
mountedScopes . delete ( scope )
mountedScopes . delete ( scope )
}
}
function toCamel ( s ) {
function toCamel ( s : string ) {
return s
return s
. toLowerCase ( )
. toLowerCase ( )
. replace ( /-([a-z])/g , ( _ , letter ) = > letter . toUpperCase ( ) )
. replace ( /-([a-z])/g , ( _ , letter ) = > letter . toUpperCase ( ) )
}
}
function assertNoUnknownElements ( element ) {
function assertNoUnknownElements ( element : Element ) {
const { tagName } = element
const { tagName } = element
if ( tagName . includes ( '-' ) ) {
if ( tagName . includes ( '-' ) ) {
const $injector = inject ('$injector' )
const $injector = inject <angular.auto.IInjectorService > ('$injector' )
const directiveName = ` ${ toCamel ( tagName ) } Directive `
const directiveName = ` ${ toCamel ( tagName ) } Directive `
if ( ! $injector . has ( directiveName ) ) {
if ( ! $injector . has ( directiveName ) ) {
throw Error (
throw Error (
@ -118,7 +149,7 @@ function assertNoUnknownElements(element) {
Array . from ( element . children ) . forEach ( assertNoUnknownElements )
Array . from ( element . children ) . forEach ( assertNoUnknownElements )
}
}
function inject (name ) {
function inject <T = unknown > (name : string ) : T {
let service
let service
angular . mock . inject ( [
angular . mock . inject ( [
name ,
name ,
@ -126,39 +157,30 @@ function inject(name) {
service = injected
service = injected
} ,
} ,
] )
] )
return service
return ( service as unknown ) as T
}
}
function fireEvent ( . . . args ) {
const $rootScope = inject ( '$rootScope' )
const result = dtlFireEvent ( . . . args )
$rootScope . $digest ( )
return result
}
Object . keys ( dtlFireEvent ) . forEach ( key = > {
fireEvent [ key ] = ( . . . args ) = > {
const $rootScope = inject ( '$rootScope' )
const result = dtlFireEvent [ key ] ( . . . args )
$rootScope . $digest ( )
return result
}
} )
// AngularJS maps `mouseEnter` to `mouseOver` and `mouseLeave` to `mouseOut`
// AngularJS maps `mouseEnter` to `mouseOver` and `mouseLeave` to `mouseOut`
fireEvent . mouseEnter = fireEvent . mouseOver
// Create a copy so we can alter it
fireEvent . mouseLeave = fireEvent . mouseOut
const fireEvent = dtlFireEvent . bind ( null )
Object . assign ( fireEvent , dtlFireEvent )
fireEvent . mouseEnter = dtlFireEvent . mouseOver
fireEvent . mouseLeave = dtlFireEvent . mouseOut
function flush ( millis = 50 ) {
function flush ( millis = 50 ) {
const $browser = inject ( '$browser' )
const $flushPendingTasks = inject < IFlushPendingTasksService > (
const $rootScope = inject ( '$rootScope' )
'$flushPendingTasks' ,
const $interval = inject ( '$interval' )
)
const $rootScope = inject < IRootScopeService > ( '$rootScope' )
const $interval = inject < IIntervalService > ( '$interval' )
$interval . flush ( millis )
$interval . flush ( millis )
$browser . defer . flush ( millis )
$flushPendingTasks ( millis )
$rootScope . $digest ( )
$rootScope . $digest ( )
}
}
function wait ( callback , options = { } ) {
type WaitOptions = Parameters < typeof dtlWait > [ 1 ]
function wait ( callback ? : ( ) = > void , options : WaitOptions = { } ) {
return dtlWait ( ( ) = > {
return dtlWait ( ( ) = > {
flush ( options . interval )
flush ( options . interval )
if ( callback ) {
if ( callback ) {