import * as url from 'url' ; import test , { ExecutionContext } from 'ava' ; import { EventEmitter2 } from 'eventemitter2' ; import * as sinon from 'sinon' ; import { Engine , NetworkData , ElementFound } from 'hint' ; import { ProblemLocation } from '@hint/utils-types' ; import { ISchemaValidationError } from '@hint/utils-json' ; import { Manifest , ManifestInvalidJSON , ManifestInvalidSchema , ManifestParsed , ManifestEvents } from '../src/types' ; import Parser from '../src/parser' ; const elementLinkEventName = 'element::link' ; const getElementLinkEventValue = ( relAttribute = 'manifest' , hrefAttribute = 'site.webmanifest' ) => { return { element : { getAttribute : ( value ) => { if ( value === 'href' ) { return hrefAttribute ; } return relAttribute ; } , nodeName : 'LINK' , resolveUrl : ( value ) => { return new url . URL ( value , 'https://example.com' ) . href ; } } , resource : 'https://example.com' } ; } ; const getEngine = ( ) : Engine < ManifestEvents > => { const engine = new EventEmitter2 ( { delimiter : '::' , maxListeners : 0 , wildcard : true } ) as Engine < ManifestEvents > ; engine . fetchContent = ( target , headers ? ) : Promise < NetworkData > => { return { } as Promise < NetworkData > ; } ; return engine ; } ; const fetchEndEventName = 'fetch::end::manifest' ; const fetchErrorEventName = 'fetch::error::manifest' ; const fetchStartEventName = 'fetch::start::manifest' ; const parseStartEventName = 'parse::start::manifest' ; const parseEndEventName = 'parse::end::manifest' ; const parseErrorSchemaEventName = 'parse::error::manifest::schema' ; const parseJSONErrorEventName = 'parse::error::manifest::json' ; const scanEndEventName = 'scan::end' ; const scanEndEventValue = { resource : 'https://example.com' } ; const createNetworkDataObject = ( manifestContent = '' , statusCode = 200 ) : NetworkData => { return { request : { headers : null , url : '' } , response : { body : { content : manifestContent , rawContent : null , rawResponse : null } , charset : '' , headers : { } , hops : [ ] , mediaType : '' , statusCode , url : '' } } as any ; } ; const createMissingTest = async ( t , relAttribute = 'manifest' , hrefAttribute = '' ) => { const elementLinkEventValue = getElementLinkEventValue ( relAttribute , hrefAttribute ) ; const sandbox = sinon . createSandbox ( ) ; const engine = getEngine ( ) ; const engineEmitAsyncSpy = sandbox . spy ( engine , 'emitAsync' ) ; new Parser ( engine ) ; await engine . emitAsync ( elementLinkEventName , elementLinkEventValue as ElementFound ) ; await engine . emitAsync ( scanEndEventName , scanEndEventValue ) ; t . is ( engineEmitAsyncSpy . callCount , 2 ) ; t . is ( engineEmitAsyncSpy . args [ 0 ] [ 0 ] , elementLinkEventName ) ; t . is ( engineEmitAsyncSpy . args [ 1 ] [ 0 ] , scanEndEventName ) ; t . is ( engineEmitAsyncSpy . args [ 1 ] [ 1 ] , scanEndEventValue ) ; sandbox . restore ( ) ; } ; const createParseTest = async ( t , manifestContent , expectedStartEventName , expectedEndEventName , verifyResult ) => { const elementEventValue = getElementLinkEventValue ( ) ; const sandbox = sinon . createSandbox ( ) ; const engine = getEngine ( ) ; const engineEmitAsyncSpy = sandbox . spy ( engine , 'emitAsync' ) ; const engineFetchContentStub = sandbox . stub ( engine , 'fetchContent' ) ; engineFetchContentStub . onCall ( 0 ) . resolves ( createNetworkDataObject ( manifestContent ) ) ; new Parser ( engine ) ; await engine . emitAsync ( elementLinkEventName , elementEventValue as ElementFound ) ; t . is ( engineEmitAsyncSpy . callCount , 5 ) ; t . is ( engineEmitAsyncSpy . args [ 0 ] [ 0 ] , elementLinkEventName ) ; t . is ( engineEmitAsyncSpy . args [ 1 ] [ 0 ] , fetchStartEventName ) ; t . is ( engineEmitAsyncSpy . args [ 2 ] [ 0 ] , fetchEndEventName ) ; t . is ( engineEmitAsyncSpy . args [ 3 ] [ 0 ] , expectedStartEventName ) ; t . is ( engineEmitAsyncSpy . args [ 4 ] [ 0 ] , expectedEndEventName ) ; verifyResult ( t , engineEmitAsyncSpy . args [ 4 ] [ 1 ] ) ; sandbox . restore ( ) ; } ; test ( `No event is emitted when no web app manifest file is specified` , async ( t ) => { const sandbox = sinon . createSandbox ( ) ; const engine = getEngine ( ) ; const engineEmitAsyncSpy = sandbox . spy ( engine , 'emitAsync' ) ; new Parser ( engine ) ; await engine . emitAsync ( scanEndEventName , scanEndEventValue ) ; t . is ( engineEmitAsyncSpy . callCount , 1 ) ; t . is ( engineEmitAsyncSpy . args [ 0 ] [ 0 ] , scanEndEventName ) ; t . is ( engineEmitAsyncSpy . args [ 0 ] [ 1 ] , scanEndEventValue ) ; sandbox . restore ( ) ; } ) ; test ( `No event is emitted when '<link rel="manifest" href=''>' is specified` , async ( t ) => { await createMissingTest ( t ) ; } ) ; test ( `No event is emitted when only a '<link rel="stylesheet"...>' is specified` , async ( t ) => { await createMissingTest ( t , 'stylesheet' , 'style.css' ) ; } ) ; test ( `'${ fetchErrorEventName }' event is emitted when the manifest cannot be fetched` , async ( t ) => { const elementEventValue = getElementLinkEventValue ( ) ; const sandbox = sinon . createSandbox ( ) ; const engine = getEngine ( ) ; const engineEmitAsyncSpy = sandbox . spy ( engine , 'emitAsync' ) ; const engineFetchContentStub = sandbox . stub ( engine , 'fetchContent' ) ; engineFetchContentStub . onCall ( 0 ) . throws ( createNetworkDataObject ( ) ) ; new Parser ( engine ) ; await engine . emitAsync ( elementLinkEventName , elementEventValue as ElementFound ) ; await engine . emitAsync ( scanEndEventName , scanEndEventValue ) ; t . is ( engineEmitAsyncSpy . callCount , 4 ) ; t . is ( engineEmitAsyncSpy . args [ 0 ] [ 0 ] , elementLinkEventName ) ; t . is ( engineEmitAsyncSpy . args [ 1 ] [ 0 ] , fetchStartEventName ) ; t . is ( engineEmitAsyncSpy . args [ 2 ] [ 0 ] , fetchErrorEventName ) ; t . not ( typeof ( engineEmitAsyncSpy . args [ 2 ] [ 1 ] as any ) . error , 'undefined' ) ; t . is ( engineEmitAsyncSpy . args [ 3 ] [ 0 ] , scanEndEventName ) ; sandbox . restore ( ) ; } ) ; test ( `'${ fetchEndEventName }' event is emitted when the response for the web app manifest has a failure status code` , async ( t ) => { const elementEventValue = getElementLinkEventValue ( ) ; const manifestContent = '500 Internal Server Error' ; const sandbox = sinon . createSandbox ( ) ; const engine = getEngine ( ) ; const engineEmitAsyncSpy = sandbox . spy ( engine , 'emitAsync' ) ; const engineFetchContentStub = sandbox . stub ( engine , 'fetchContent' ) ; engineFetchContentStub . onCall ( 0 ) . resolves ( createNetworkDataObject ( manifestContent , 500 ) ) ; new Parser ( engine ) ; await engine . emitAsync ( elementLinkEventName , elementEventValue as ElementFound ) ; await engine . emitAsync ( scanEndEventName , scanEndEventValue ) ; t . is ( engineEmitAsyncSpy . callCount , 4 ) ; t . is ( engineEmitAsyncSpy . args [ 0 ] [ 0 ] , elementLinkEventName ) ; t . is ( engineEmitAsyncSpy . args [ 1 ] [ 0 ] , fetchStartEventName ) ; t . is ( engineEmitAsyncSpy . args [ 2 ] [ 0 ] , fetchEndEventName ) ; t . is ( engineEmitAsyncSpy . args [ 3 ] [ 0 ] , scanEndEventName ) ; sandbox . restore ( ) ; } ) ; test ( `'${ parseEndEventName }' event is emitted when manifest content is valid` , async ( t ) => { const manifestContent = { name : '5' } ; const manifestContentParsed = { dir : 'auto' , display : 'browser' , name : '5' } as Manifest ; await createParseTest ( t , JSON . stringify ( manifestContent ) , parseStartEventName , parseEndEventName , ( tt , result ) => { tt . deepEqual ( result . parsedContent , manifestContentParsed ) ; } ) ; } ) ; test ( `'${ parseEndEventName }' event is emitted when valid shortcuts are included` , async ( t ) => { const manifestContent = { name : 'test' , shortcuts : [ { name : 'Test Shortcut' , url : 'https://example.com' } ] } ; const manifestContentParsed = { dir : 'auto' , display : 'browser' , name : 'test' , shortcuts : [ { name : 'Test Shortcut' , url : 'https://example.com' } ] } as Manifest ; await createParseTest ( t , JSON . stringify ( manifestContent ) , parseStartEventName , parseEndEventName , ( tt , result ) => { tt . deepEqual ( result . parsedContent , manifestContentParsed ) ; } ) ; } ) ; test ( `'${ parseEndEventName }' event includes location information` , async ( t ) => { const manifestContent = `{
    "name": "5"
};` ; await createParseTest ( t , manifestContent , parseStartEventName , parseEndEventName , ( tt , result ) => { const nameLocation = result . getLocation ( 'name' ) ; const valueLocation = result . getLocation ( 'name' , { at : 'value' } ) ; tt . is ( nameLocation && nameLocation . line , 1 ) ; tt . is ( nameLocation && nameLocation . column , 5 ) ; tt . is ( valueLocation && valueLocation . line , 1 ) ; tt . is ( valueLocation && valueLocation . column , 12 ) ; } ) ; } ) ; test ( `'${ parseJSONErrorEventName }' event is emitted when manifest content is not valid JSON` , async ( t ) => { const manifestContent = 'invalid' ; await createParseTest ( t , manifestContent , parseStartEventName , parseJSONErrorEventName , ( tt , result ) => { tt . not ( typeof result . error , 'undefined' ) ; } ) ; } ) ; test ( `'${ parseErrorSchemaEventName }' event is emitted when manifest content is not valid because of an additional property` , async ( t ) => { const expectedPrettifiedErrors = [ `'root' must NOT have additional properties. Additional property found 'additionalProperty'.` , `'root' must NOT have additional properties. Additional property found 'gcm_sender_id'.` , `'root' must NOT have additional properties. Additional property found 'unknown_proprietary_extension'.` , `'icons/0' must NOT have additional properties. Additional property found 'density'.` ] ; const manifestContent = { additionalProperty : 'x' , gcm_sender_id : { a : 5 } , icons : [ { density : 2 , src : '/a.png' } ] , unknown_proprietary_extension : 5 } ; await createParseTest ( t , JSON . stringify ( manifestContent ) , parseStartEventName , parseErrorSchemaEventName , ( tt , result ) => { tt . is ( result . prettifiedErrors . length , expectedPrettifiedErrors . length ) ; tt . true ( result . prettifiedErrors . every ( ( e ) => { return expectedPrettifiedErrors . includes ( e ) ; } ) ) ; } ) ; } ) ; test ( `'${ parseErrorSchemaEventName }' event includes location information` , async ( t ) => { const expectedLocations = { [ `'icons/0' must NOT have additional properties. Additional property found 'density'.` ] : { column : 9 , line : 4 } , [ `'root' must NOT have additional properties. Additional property found 'additionalProperty'.` ] : { column : 5 , line : 1 } , [ `'root' must NOT have additional properties. Additional property found 'gcm_sender_id'.` ] : { column : 5 , line : 2 } , [ `'root' must NOT have additional properties. Additional property found 'unknown_proprietary_extension'.` ] : { column : 5 , line : 7 } } ; const manifestContent = `{
    "additionalProperty": "x",
    "gcm_sender_id": { "a": 5 },
    "icons": [{
        "density": 2,
        "src": "/a.png"
    }],
    "unknown_proprietary_extension": 5
}` ; await createParseTest ( t , manifestContent , parseStartEventName , parseErrorSchemaEventName , ( tt , result ) => { result . errors . forEach ( ( error , i ) => { const message = result . prettifiedErrors [ i ] ; const expectedLocation = expectedLocations [ message ] ; if ( ! expectedLocation ) { tt . fail ( `Additional error reported: ${ message }` ) ; } tt . is ( error . location && error . location . line , expectedLocation . line , `Incorrect error line for: ${ message }` ) ; tt . is ( error . location && error . location . column , expectedLocation . column , `Incorrect error column for: ${ message }` ) ; } ) ; } ) ; } ) ; test ( 'It does not emit redundant fetch events' , async ( t ) => { const elementEventValue = getElementLinkEventValue ( ) ; const sandbox = sinon . createSandbox ( ) ; const engine = getEngine ( ) ; const networkData = createNetworkDataObject ( `{ "name": "5" };` ) ; const engineEmitAsyncSpy = sandbox . spy ( engine , 'emitAsync' ) ; const engineFetchContentStub = sandbox . stub ( engine , 'fetchContent' ) ; engineFetchContentStub . onCall ( 0 ) . resolves ( networkData ) ; new Parser ( engine ) ; await engine . emitAsync ( fetchEndEventName , { element : null , ... networkData , resource : 'https://example.com/site.webmanifest' } ) ; await engine . emitAsync ( elementLinkEventName , elementEventValue as ElementFound ) ; t . log ( engineEmitAsyncSpy . args ) ; t . is ( engineEmitAsyncSpy . callCount , 4 ) ; t . is ( engineEmitAsyncSpy . args [ 0 ] [ 0 ] , fetchEndEventName ) ; t . is ( engineEmitAsyncSpy . args [ 1 ] [ 0 ] , parseStartEventName ) ; t . is ( engineEmitAsyncSpy . args [ 2 ] [ 0 ] , parseEndEventName ) ; t . is ( engineEmitAsyncSpy . args [ 3 ] [ 0 ] , elementLinkEventName ) ; sandbox . restore ( ) ; } ) ;