license import { Constructor , cloneableResult } from '../util.js' ; import { Suite } from '../suite.js' ; import { Spec } from '../spec.js' ; import { Topic } from '../topic.js' ; import { Test , TestRunContext , TestConfig , TestResult } from '../test.js' ; import { SuiteAddress } from '../suite.js' ; /**
 * IsolatedTest
 */ export interface IsolatedTestConfig extends TestConfig { isolated ? : boolean ; } export interface IsolatedTestResult extends TestResult { isolated ? : boolean ; } export interface IsolatedTestRunContext extends TestRunContext { isolated ? : boolean ; } export interface IsolatedTest { readonly isolated : boolean ; } enum IsolatedTestMessage { messagePort = 'RistrettoIsolatedTestMessagePort' , ready = 'RistrettoIsolatedTestReady' } /**
 * An isolated test is one that runs in a "clean room" context. Currently, this
 * means running in an iframe in a browser. This is useful to help test authors
 * control for global state that persists across test runs (for example, custom
 * element registration).
 *
 * For test runs isolated by iframes, the following steps are taken:
 *
 *  1. On wind-up, if the test is to be isolated, the implementation is replaced
 *  by one that generates an iframe pointing to the same URL but with specially
 *  crafted query params that inform the suite to run only one test, and informs
 *  the test that it is running in an isolated context.
 *  2. The test implementation is invoked in the main frame, causing an iframe
 *  (the isolated context) to be loaded.
 *  3. The test in the isolated frame notifies that it is ready to run.
 *  4. The test in the main frame posts a `MessagePort` to the test in the
 *  isolated frame.
 *  5. The test in the isolated frame receives the `MessagePort`, and invokes
 *  the actual test implementation.
 *  6. The test in the isolated frame posts the `TestResult` back through the
 *  `MessagePort` to the main frame.
 *  7. The test in the main frame resolves its implementation with the
 *  `TestResult` received from the isolated frame.
 */ export function IsolatedTest < T extends Constructor < Test > > ( TestImplementation ) { return class extends TestImplementation { /**
         * True if the test is configured to be isolated.
         */ get isolated ( ) : boolean { return ! ! ( this . config && this . config . isolated ) ; } protected config ! : IsolatedTestConfig ; /**
         * An isolated test run must post its results to the parent frame in order
         * to be completed. We override run to do this if we detect that we are
         * isolated.
         */ async run ( suite , ... args ) { const { queryParams } = suite ; let port ; if ( 'ristretto_isolated' in queryParams ) { port = await this . receiveMessagePort ( ) ; } const result = await super . run ( suite , ... args ) ; if ( 'ristretto_isolated' in queryParams && port != null ) { port . postMessage ( cloneableResult ( result ) ) ; } return result ; } protected async windUp ( context ) { const { suite } = context ; const { queryParams } = suite ; const isIsolated = 'ristretto_isolated' in queryParams ; const shouldBeIsolated = ! ! this . isolated ; context = await super . windUp ( context ) ; if ( ! shouldBeIsolated || isIsolated ) { return { ... context , isolated : false } ; } const address = suite . getAddressForTest ( this ) ; const implementation = async ( ) => { const isolatedResult = await this . isolatedRun ( address ) ; if ( ! ! isolatedResult . error ) { throw isolatedResult . error ; } } ; return { ... context , isolated : true , implementation } ; } protected async postProcess ( context , result ) { const superResult = await super . postProcess ( context , result ) ; return { ... superResult , isolated : context . isolated } ; } protected async receiveMessagePort ( ) { window . parent . postMessage ( IsolatedTestMessage . ready , window . location . origin ) ; return new Promise ( resolve => { const receiveMessage = ( event ) => { if ( event . data !== IsolatedTestMessage . messagePort ) { return ; } resolve ( event . ports [ 0 ] ) ; window . removeEventListener ( 'message' , receiveMessage ) ; } ; window . addEventListener ( 'message' , receiveMessage ) ; } ) as Promise < MessagePort > ; } protected async isolatedRun ( address ) { return new Promise ( resolve => { const channel = new MessageChannel ( ) ; const { port1 , port2 } = channel ; const url = new URL ( window . location . toString ( ) ) ; const iframe = document . createElement ( 'iframe' ) ; const receiveMessage = ( event ) => { if ( event . source !== iframe . contentWindow && event . data !== IsolatedTestMessage . ready ) { return ; } window . removeEventListener ( 'message' , receiveMessage ) ; iframe . contentWindow . postMessage ( IsolatedTestMessage . messagePort , '*' , [ port2 ] ) ; } ; const receiveResult = ( event ) => { if ( event . data == null ) { return ; } port1 . removeEventListener ( 'message' , receiveResult ) ; document . body . removeChild ( iframe ) ; resolve ( event . data ) ; } ; port1 . addEventListener ( 'message' , receiveResult ) ; port1 . start ( ) ; iframe . style . position = 'absolute' ; iframe . style . top = '-1000px' ; iframe . style . left = '-1000px' ; window . addEventListener ( 'message' , receiveMessage ) ; const searchPrefix = url . search ? `${ url . search }&` : '?' ; const uriAddress = encodeURIComponent ( JSON . stringify ( address ) ) ; url . search = `${ searchPrefix }ristretto_suite_address=${ uriAddress }&ristretto_isolated&ristretto_disable_reporting` ; document . body . appendChild ( iframe ) ; iframe . src = url . toString ( ) ; } ) as Promise < TestResult > ; } } as Constructor < Test & IsolatedTest > ; } ; /**
 * IsolatedTopic
 */ export interface IsolatedTopic { } /**
 * An isolated topic is a trivial extension of a `Topic` that mixes-in an
 * exstension to the `Topic`'s `Test` implementation.
 */ export function IsolatedTopic < T extends Constructor < Topic > > ( TopicImplementation ) { return class extends TopicImplementation { protected get TestImplementation ( ) { return IsolatedTest ( super . TestImplementation ) ; } } as Constructor < Topic & IsolatedTopic > ; } ; /**
 * IsolatedSpec
 */ export interface IsolatedSpec { } /**
 * An isolatable spec is a trivial extension of a `Spec` that mixes-in an
 * extension to the `Spec`'s `Topic` implementation.
 */ export function IsolatedSpec < S extends Constructor < Spec > > ( SpecImplementation ) { return class extends SpecImplementation { protected get TopicImplementation ( ) { return IsolatedTopic ( super . TopicImplementation ) ; } } as Constructor < Spec & IsolatedSpec > ; } ; export const Isolatable = IsolatedSpec ;