import { expect } from "chai" ; import { forEach , isArray , includes , keys } from "lodash" ; import { TestModelVersion , generateModel , expectProperty , expectExists , downloadLibraries , } from "@ui5-language-assistant/test-utils" ; import { UI5SemanticModel , UnresolvedType , } from "@ui5-language-assistant/semantic-model-types" ; import { forEachSymbol } from "../src/utils" ; import { generate } from "../src/api" ; import { isObject , getFQN , expectModelObjectsEqual , } from "./utils/model-test-utils" ; context ( "The ui5-language-assistant semantic model package API" , ( ) => { const TYPE_PROPERTIES = [ "type" , "altTypes" ] ; const RECURSIVE_TYPE_KINDS = [ "UI5Class" , "UI5Interface" , "UI5Enum" , "UI5Namespace" , "UI5Typedef" , ] ; const RECURSIVE_PROPERTIES = { UI5Namespace : [ "namespaces" , "classes" ] , UI5Class : [ "extends" , "implements" ] , "*" : [ "parent" ] , } ; const PARENT_EXCLUDE_PROPERTIES = { UI5Class : [ "extends" , "implements" ] , UI5Prop : [ "default" ] , "*" : [ "parent" ] . concat ( TYPE_PROPERTIES ) , } ; function hasPropertyForKind ( map , kind , propertyName ) { return ( includes ( map [ kind ] , propertyName ) || includes ( map [ "*" ] , propertyName ) ) ; } function assertRootSymbolsParent ( model ) { forEachSymbol ( model , ( symbol , fqn ) => { expectProperty ( symbol , "parent" , `Symbol ${ fqn } does not have a parent property` ) ; const parent = symbol . parent ; if ( fqn . indexOf ( "." ) >= 0 && fqn . indexOf ( "." ) !== fqn . length - 1 ) { expectExists ( parent , `Symbol ${ fqn } does not have a parent` ) ; const parentFqn = getFQN ( model , parent ) ; expectExists ( parentFqn , `Parent ${ parent . kind } ${ parent . name } of ${ fqn } not found in the model` ) ; expectProperty ( symbol , "name" , `Symbol ${ fqn } does not have a name property` ) ; expect ( parentFqn + "." + symbol . name ) . to . equal ( fqn ) ; } else { expect ( parent , `Symbol ${ fqn } has a parent` ) . to . be . undefined ; } } ) ; } function assertSymbolPropertiesParent ( model ) { runOnValue ( { value : model as unknown , fqn : "model" , parent : undefined as unknown , deep : true , } , function assertSymbolPropertiesParentInner ( params ) { const value = params . value ; const kind = value . kind ; const expectedParent = params . parent ; const fqn = params . fqn ; const deep = params . deep ; if ( expectedParent !== undefined ) { expectProperty ( value , "parent" , `${ fqn } does not have a parent property` ) ; expect ( value . parent , `${ fqn } does not have a parent` ) . to . exist ; expectModelObjectsEqual ( model , value . parent , expectedParent , `${ fqn } has unexpected parent` ) ; } if ( ! deep ) { return ; } forEach ( value , ( propertyValue , propertyName ) => { if ( hasPropertyForKind ( PARENT_EXCLUDE_PROPERTIES , kind , propertyName ) ) { return ; } const isRecursiveProperty = hasPropertyForKind ( RECURSIVE_PROPERTIES , kind , propertyName ) ; runOnValue ( { value : propertyValue , fqn : `${ fqn }.${ propertyName }` , parent : value , deep : ! isRecursiveProperty , } , assertSymbolPropertiesParentInner ) ; } ) ; } ) ; } type Params = { value : unknown ; fqn : string ; } ; type ObjectWithKind = Record < string , unknown > & { kind : string ; } ; param params param action function runOnValue < T extends Params > ( params , action ) { const value = params . value ; const fqn = params . fqn ; if ( isArray ( value ) ) { for ( let i = 0 ; i < value . length ; ++ i ) { runOnValue ( { ... params , value : value [ i ] , fqn : `${ fqn }[${ i }]` , } , action ) ; } } else if ( isObject ( value ) ) { if ( "kind" in value && typeof value . kind === "string" ) { action ( { ... params , value : value as ObjectWithKind , } ) ; } else { forEach ( value , ( mapValue , mapKey ) => { runOnValue ( { ... params , value : mapValue , fqn : `${ fqn }[${ mapKey }]` , } , action ) ; } ) ; } } } function assertTypesAreResolved ( model ) { runOnValue ( { value : model as unknown , fqn : "model" } , function assertTypesAreResolvedInner ( params ) { const fqn = params . fqn ; const value = params . value ; const kind = value . kind ; forEach ( value , ( propertyValue , propertyName ) => { runOnValue ( { value : value [ propertyName ] , fqn : `${ fqn }.${ propertyName }` } , ( params ) => { if ( includes ( TYPE_PROPERTIES , propertyName ) ) { const type = params . value ; expect ( type . kind , `Unresolved type ${ ( ( type as unknown ) as UnresolvedType ) . name } found in ${ params . fqn }` ) . to . not . equal ( "UnresolvedType" ) ; if ( includes ( RECURSIVE_TYPE_KINDS , type . kind ) ) { return ; } } if ( hasPropertyForKind ( RECURSIVE_PROPERTIES , kind , propertyName ) ) { return ; } runOnValue ( { value : params . value , fqn : `${ params . fqn }` } , assertTypesAreResolvedInner ) ; } ) ; } ) ; } ) ; } function createModelConsistencyTests ( version ) { describe ( `Model generated from ${ version }` , ( ) => { before ( async ( ) => { await downloadLibraries ( version ) ; } ) ; it ( `is created successfully in strict mode` , async ( ) => { const model = await generateModel ( { version , downloadLibs : false , modelGenerator : generate , } ) ; expect ( model ) . to . exist ; } ) ; it ( `is created successfully in non-strict mode` , async ( ) => { const model = await generateModel ( { version , downloadLibs : false , strict : false , modelGenerator : generate , } ) ; expect ( model ) . to . exist ; } ) ; describe ( "model consistency" , ( ) => { let model ; before ( async ( ) => { model = await generateModel ( { version , downloadLibs : false , modelGenerator : generate , } ) ; } ) ; it ( `has correct parent on root symbols` , async ( ) => { assertRootSymbolsParent ( model ) ; } ) ; it ( `has correct parent on symbols' properties` , async ( ) => { assertSymbolPropertiesParent ( model ) ; } ) ; it ( `has only resolved types` , async ( ) => { assertTypesAreResolved ( model ) ; } ) ; } ) ; } ) ; } const versions = [ "1.60.14" , "1.74.0" , "1.71.14" ] ; for ( const version of versions ) { createModelConsistencyTests ( version ) ; } describe ( "returned model is frozen" , ( ) => { const readOnlyMessageMatcher = "read only" ; const objectNotExtensibleMatcher = "not extensible" ; const cannotDeleteMatcher = "Cannot delete" ; let model ; before ( async ( ) => { model = await generateModel ( { version : "1.74.0" , modelGenerator : generate , } ) ; } ) ; it ( "cannot change first-level member of the model" , ( ) => { expect ( ( ) => { model . namespaces = { } ; } ) . to . throw ( TypeError , readOnlyMessageMatcher ) ; } ) ; it ( "cannot change property of internal member of the model" , ( ) => { const firstClass = model . classes [ keys ( model . classes ) [ 0 ] ] ; expect ( firstClass ) . to . exist ; expect ( firstClass . name ) . to . exist ; expect ( ( ) => { firstClass . name = "abc" ; } ) . to . throw ( TypeError , readOnlyMessageMatcher ) ; } ) ; it ( "cannot add first-level property on the model" , ( ) => { expect ( ( ) => { ( model as any ) . myProperty = 1 ; } ) . to . throw ( TypeError , objectNotExtensibleMatcher ) ; } ) ; it ( "cannot add property on internal member of the model" , ( ) => { const firstClass = model . classes [ keys ( model . classes ) [ 0 ] ] ; expect ( firstClass ) . to . exist ; expect ( ( ) => { ( firstClass as any ) . myProperty = "abc" ; } ) . to . throw ( TypeError , objectNotExtensibleMatcher ) ; } ) ; it ( "cannot remove first-level property from the model" , ( ) => { expect ( ( ) => { delete model . namespaces ; } ) . to . throw ( TypeError , cannotDeleteMatcher ) ; } ) ; it ( "cannot remove property from internal member of the model" , ( ) => { const firstClass = model . classes [ keys ( model . classes ) [ 0 ] ] ; expect ( firstClass ) . to . exist ; expect ( firstClass . name ) . to . exist ; expect ( ( ) => { delete firstClass . name ; } ) . to . throw ( TypeError , cannotDeleteMatcher ) ; } ) ; } ) ; describe ( "API JSON fixes" , ( ) => { let model ; before ( async ( ) => { model = await generateModel ( { version : "1.74.0" , modelGenerator : generate , } ) ; } ) ; it ( "sets content as the default aggregation for sap.ui.core.mvc.View" , ( ) => { const view = model . classes [ "sap.ui.core.mvc.View" ] ; expect ( view , "sap.ui.core.mvc.View" ) . to . exist ; expectExists ( view . defaultAggregation , "defaultAggregation" ) ; expect ( view . defaultAggregation . name ) . to . equal ( "content" ) ; } ) ; } ) ; } ) ;