diff --git a/platform/src/EducationPlatformApp.js b/platform/src/EducationPlatformApp.js index bb54df0..dca1070 100644 --- a/platform/src/EducationPlatformApp.js +++ b/platform/src/EducationPlatformApp.js @@ -276,7 +276,7 @@ class EducationPlatformApp { this.outputLanguage = this.activity.outputLanguage; } - // Create panels for the given activites + // Create panels for the given activities for ( let apanel of this.activity.panels ){ var newPanel = this.createPanelForDefinitionId(apanel); @@ -480,7 +480,7 @@ class EducationPlatformApp { } } - // Invoke the actionFunction on compeletion of any conversions + // Invoke the actionFunction on completion of any conversions let actionFunctionPromise = new Promise( (resolve, reject) => { Promise.all( parameterPromises ).then( (values) => { @@ -523,12 +523,12 @@ class EducationPlatformApp { * @param {string} sourceType * @param {string} targetType * @param {string} parameterName name of the parameter for request - * @returns {Promise} promise for the converted paramter value + * @returns {Promise} promise for the converted parameter value */ convert(sourceValue, sourceType, targetType, parameterName){ let parameterPromise; - let typesPanelValuesMap = {}; // Types have to be distinct for mapping to the conversion function's paramters + let typesPanelValuesMap = {}; // Types have to be distinct for mapping to the conversion function's parameters typesPanelValuesMap[sourceType]= sourceValue; let conversionFunctionId = this.functionRegistry_find( Object.keys(typesPanelValuesMap), targetType ); @@ -558,11 +558,11 @@ class EducationPlatformApp { * @param {string} metamodelType * @param {string} targetType * @param {string} parameterName name of the parameter for request - * @returns {Promise} promise for the converted paramter value + * @returns {Promise} promise for the converted parameter value */ async convertIncludingMetamodel(sourceValue, sourceType, metamodelValue, metamodelType, targetType, parameterName){ let parameterPromise; - let typesPanelValuesMap = {}; // Types have to be distinct for mapping to the conversion function's paramters + let typesPanelValuesMap = {}; // Types have to be distinct for mapping to the conversion function's parameters typesPanelValuesMap[sourceType]= sourceValue; let conversionFunctionId; @@ -602,13 +602,17 @@ class EducationPlatformApp { * @param {string[]} conversionFunctions list of conversion function ids to check * @param {boolean} convertMetamodel when true try to convert the metamodel using a remote tool service conversion function * available to the ToolsManager. - * @param {string} parameterName the name of the parameter to use when convering the metamodel. + * @param {string} parameterName the name of the parameter to use when converting the metamodel. * @param {string[]} typeValueMap the type values map the metamodel input value is added to if a conversion function is found * @returns {string} the id of a conversion function to use, null if none found. */ async selectConversionFunctionConvertMetamodel(metamodelType, metamodelValue, conversionFunctions, convertMetamodel, parameterName, typeValueMap){ - let conversionFunctionId; - let functionsToCheck = [...conversionFunctions] + let conversionFunctionId = null; + let functionsToCheck = []; + + if (Array.isArray(conversionFunctions)){ + functionsToCheck = [...conversionFunctions]; + } while ( conversionFunctionId==null && functionsToCheck.length > 0){ let functionId = functionsToCheck.pop(); @@ -661,7 +665,7 @@ class EducationPlatformApp { * TODO: To be moved to the FunctionRegistry issue #40 * * @param {string} functionId the id of the action function - * @param {Object} typeValuesMap an object mapping action functions paramter types as keys to input values + * @param {Object} typeValuesMap an object mapping action functions parameter types as keys to input values * @param {string} parameterName name of the parameter * @returns Promise for the translated data * @@ -699,7 +703,7 @@ class EducationPlatformApp { * Requests the conversion function from the remote tool service * * @param {Object} parameters - * @param {ActionFunction} converstionFunction + * @param {ActionFunction} conversionFunction * @param {String} parameterName name of the parameter * @returns Promise for the translated data */ @@ -1017,7 +1021,7 @@ class EducationPlatformApp { /** * Poll for editor to become available. - * @param {String} statusUrl - the url for cheking the status of the editor panel. + * @param {String} statusUrl - the url for checking the status of the editor panel. * @param {String} editorInstanceUrl - the editor instance's url. * @param {String} editorPanelId - the id of the editor panel. * @param {String} editorActivityId - TODO remove as this can be found using editorPanelId to save having to specify in config. diff --git a/platform/test/spec/testEducationPlatformAppSpec.js b/platform/test/spec/testEducationPlatformAppSpec.js index 169e8d2..8a7922e 100644 --- a/platform/test/spec/testEducationPlatformAppSpec.js +++ b/platform/test/spec/testEducationPlatformAppSpec.js @@ -6,6 +6,7 @@ export var TOKEN_SERVER_URL = "test://ts.url"; import {EducationPlatformApp} from "../../src/EducationPlatformApp.js"; import { ActionFunction } from "../../src/ActionFunction.js"; import { Panel } from "../../src/Panel.js"; +import "jasmine-ajax"; describe("EducationPlatformApp", () => { @@ -148,7 +149,7 @@ describe("EducationPlatformApp", () => { const PARAM2_VALUE = "param2's contents"; const PARAM2_CONVERTED_VALUE = "param2's converted contents"; - // types the test action functions are exepecting + // types the test action functions are expecting const ACTION_FUNCTION_PARAM1_TYPE = "type1"; const ACTION_FUNCTION_PARAM2_TYPE = "type2"; const ACTION_FUNCTION_RESULT= "Test function result"; @@ -329,6 +330,401 @@ describe("EducationPlatformApp", () => { }) + describe("convert()", () => { + let platform; + let findConversionSpy; + + const FILE_CONTENTS = "Test file contents."; + const SOURCE_TYPE = "test-source-type"; + const TARGET_TYPE = "test-target-type"; + const PARAM_NAME = "test"; + const callConversionReturn = new Promise(function(resolve) { + resolve(true); + }) + + const CONVERSION_FUNCTION_ID = "conversion-function-id"; + + beforeEach(()=>{ + // Setup + findConversionSpy = spyOn( EducationPlatformApp.prototype, "functionRegistry_find"). + and.returnValue(CONVERSION_FUNCTION_ID); + + spyOn( EducationPlatformApp.prototype, "functionRegistry_callConversion").and.returnValue( + callConversionReturn); + + spyOn( EducationPlatformApp.prototype, "errorNotification"); + + platform = new EducationPlatformApp(); + + }) + + it("calls functionRegistry_callConversion on a conversion function being available", ()=>{ + // Call the target object + platform.convert(FILE_CONTENTS, SOURCE_TYPE, TARGET_TYPE, PARAM_NAME); + + // Check the expected results + expect(platform.functionRegistry_callConversion).toHaveBeenCalledWith( + CONVERSION_FUNCTION_ID, { [SOURCE_TYPE]: FILE_CONTENTS } , PARAM_NAME + ); + + expect(platform.errorNotification).not.toHaveBeenCalled(); + }) + + it("returns a promise on a conversion function being available", ()=> { + // Call the target object + const convertResult = platform.convert(FILE_CONTENTS, SOURCE_TYPE, TARGET_TYPE, PARAM_NAME); + + // Check the expected results + expect(convertResult).toEqual(callConversionReturn); + }) + + it("returns null and provides an error notification on a conversion function not being available", ()=> { + findConversionSpy.and.returnValue(null); + + // Call the target object + const convertResult = platform.convert(FILE_CONTENTS, SOURCE_TYPE, TARGET_TYPE, PARAM_NAME); + + // Check the expected results + expect(convertResult).toEqual(null); + expect(platform.errorNotification).toHaveBeenCalledWith(jasmine.stringMatching("(N|n)o conversion function")) + }) + + }) + + describe("convertIncludingMetamodel()", () => { + let platform; + let findConversionSpy; + + const FILE_CONTENTS = "Test file contents."; + const SOURCE_TYPE = "test-source-type"; + const TARGET_TYPE = "test-target-type"; + const MM_FILE_CONTENTS = "Test metamodel file contents." + const MM_TYPE = "test-metamodel-type"; + const PARAM_NAME = "test"; + const callConversionReturn = new Promise(function(resolve) { + resolve(true); + }) + + const CONVERSION_FUNCTION_ID = "conversion-function-id"; + + beforeEach(()=>{ + // Setup + findConversionSpy = spyOn( EducationPlatformApp.prototype, "functionRegistry_findPartial"). + and.returnValue([CONVERSION_FUNCTION_ID]); + + spyOn( EducationPlatformApp.prototype, "functionRegistry_callConversion").and.returnValue( + callConversionReturn); + + spyOn( EducationPlatformApp.prototype, "errorNotification"); + + platform = new EducationPlatformApp(); + + // platform - toolsManager + let toolsManagerSpy = jasmine.createSpyObj(['getActionFunction']); + toolsManagerSpy.getActionFunction.and.returnValue(new ActionFunction({ + parameters: [ + {name: "input", type: SOURCE_TYPE, instanceOf: "metamodel"}, + {name: "metamodel", type: MM_TYPE} + ] + })); + platform.toolsManager= toolsManagerSpy; + + }) + + it("calls functionRegistry_callConversion on a conversion function being available", async ()=> { + // Call the target object + await platform.convertIncludingMetamodel(FILE_CONTENTS, SOURCE_TYPE, MM_FILE_CONTENTS, MM_TYPE, TARGET_TYPE, PARAM_NAME); + + // Check the expected results + expect(platform.functionRegistry_callConversion).toHaveBeenCalledWith( + CONVERSION_FUNCTION_ID, {[SOURCE_TYPE]: FILE_CONTENTS, [MM_TYPE]: MM_FILE_CONTENTS } , PARAM_NAME + ); + + expect(platform.errorNotification).not.toHaveBeenCalled(); + }) + + it("returns a promise on a conversion function being available", async () => { + // Call the target object + const convertResult = platform.convertIncludingMetamodel(FILE_CONTENTS, SOURCE_TYPE, MM_FILE_CONTENTS, MM_TYPE, TARGET_TYPE, PARAM_NAME); + + // Check the expected results + await expectAsync(convertResult).toBePending(); + }) + + it("returns null and provides an error notification on a conversion function not being available", async () => { + findConversionSpy.and.returnValue(null); + + // Call the target object + const convertResult = await platform.convertIncludingMetamodel(FILE_CONTENTS, SOURCE_TYPE, MM_FILE_CONTENTS, MM_TYPE, TARGET_TYPE, PARAM_NAME); + + // Check the expected results + expect(convertResult).toEqual(null); + expect(platform.errorNotification).toHaveBeenCalledWith(jasmine.stringMatching("(N|n)o conversion function")) + }) + + }) + + describe("selectConversionFunctionConvertMetamodel()", () => { + let platform; + + const SOURCE_TYPE = "test-source-type"; + const FILE_CONTENTS = "Test file contents."; + const MM_FILE_CONTENTS = "Test metamodel file contents." + const MM_TARGET_TYPE = "test-metamodel-target-type"; + const PARAM_NAME = "test"; + + const CONVERSION_FUNCTION_ID = "conversion-function-id"; + const X_FUNCTION_ID = "x-function-id"; // Not interested + + let toolsManagerSpy; + + beforeEach( () => { + // Setup + spyOn( EducationPlatformApp.prototype, "errorNotification"); + + platform = new EducationPlatformApp(); + + // platform - toolsManager + toolsManagerSpy = jasmine.createSpyObj(['getActionFunction', 'getConversionFunction']); + + toolsManagerSpy.getActionFunction.and.callFake( (functionId) => { + let actionFunctionConfig; + + switch (functionId){ + case CONVERSION_FUNCTION_ID: + actionFunctionConfig = { + parameters: [ + {name: "input", type: SOURCE_TYPE, instanceOf: "metamodel"}, + {name: "metamodel", type: MM_TARGET_TYPE} + ] + }; + break; + + case X_FUNCTION_ID: + actionFunctionConfig = { + parameters: [ + {name: "input", type: SOURCE_TYPE, instanceOf: "metamodel"}, + {name: "metamodel", type: "X"} + ] + } + break; + default: + actionFunctionConfig = null; + } + + return new ActionFunction(actionFunctionConfig) + }) + platform.toolsManager = toolsManagerSpy; + + }) + + it("returns a function id if a conversion is possible without considering the metamodel", async () => { + const CONSIDER_MM = false; + let mm_type = MM_TARGET_TYPE; + const typeValueMap = { [SOURCE_TYPE]: FILE_CONTENTS } + + // Call the target object + let selectConversionResult = await platform.selectConversionFunctionConvertMetamodel(mm_type, MM_FILE_CONTENTS, [CONVERSION_FUNCTION_ID, X_FUNCTION_ID], CONSIDER_MM, PARAM_NAME, typeValueMap) + + // Check the expected results + expect(selectConversionResult).toEqual(CONVERSION_FUNCTION_ID); + }) + + it("returns null if a conversion is not possible without considering the metamodel", async () => { + const CONSIDER_MM = false; + let mm_type = MM_TARGET_TYPE; + const typeValueMap = { [SOURCE_TYPE]: FILE_CONTENTS } + + // Call the target object + let selectConversionResult = await platform.selectConversionFunctionConvertMetamodel(mm_type, MM_FILE_CONTENTS, [X_FUNCTION_ID, X_FUNCTION_ID], CONSIDER_MM, PARAM_NAME, typeValueMap) + + // Check the expected results + expect(selectConversionResult).toEqual(null); + }) + + it("returns a function id, converts the metamodel, and adds the converted metamodel value to the typeValueMap if a conversion is possible considering the metamodel", async ()=>{ + const CONSIDER_MM = true; + let mm_type = "test-metamodel-type"; + const typeValueMap = { [SOURCE_TYPE]: FILE_CONTENTS } + + const MM_CONVERSION_FUNCTION_ID = "metamodel-conversion-function-id"; + const MM_CONVERTED_CONTENTS = "Test converted metamodel contents."; + + const callConversionReturn = new Promise(function(resolve) { + resolve({data: MM_CONVERTED_CONTENTS}); + }) + spyOn( EducationPlatformApp.prototype, "functionRegistry_callConversion").and.returnValue( + callConversionReturn); + + toolsManagerSpy.getConversionFunction.and.returnValues(null, MM_CONVERSION_FUNCTION_ID); // Find possible conversion on the second call + + // Call the target object + let selectConversionResult = await platform.selectConversionFunctionConvertMetamodel(mm_type, MM_FILE_CONTENTS, [CONVERSION_FUNCTION_ID, X_FUNCTION_ID], CONSIDER_MM, PARAM_NAME, typeValueMap) + + // Check the expected results + expect(platform.functionRegistry_callConversion).toHaveBeenCalledWith( + MM_CONVERSION_FUNCTION_ID, { [mm_type]: MM_FILE_CONTENTS}, PARAM_NAME + ); + + expect(typeValueMap[MM_TARGET_TYPE]).toEqual(MM_CONVERTED_CONTENTS); + + expect(selectConversionResult).toEqual(CONVERSION_FUNCTION_ID); + }) + + it("returns null if no conversion is available considering the metamodel", async () => { + const CONSIDER_MM = true; + let mm_type = "test-metamodel-type"; + const typeValueMap = { [SOURCE_TYPE]: FILE_CONTENTS } + + spyOn( EducationPlatformApp.prototype, "functionRegistry_callConversion"); + + toolsManagerSpy.getConversionFunction.and.returnValues(null, null); // Do not find possible conversion + + // Call the target object + let selectConversionResult = await platform.selectConversionFunctionConvertMetamodel(mm_type, MM_FILE_CONTENTS, [CONVERSION_FUNCTION_ID, X_FUNCTION_ID], CONSIDER_MM, PARAM_NAME, typeValueMap) + + // Check the expected results + expect(platform.functionRegistry_callConversion).not.toHaveBeenCalled(); + + expect(typeValueMap[MM_TARGET_TYPE]).toEqual(undefined); + + expect(selectConversionResult).toEqual(null); + }) + }) + + describe("functionRegistry_call()", () => { + const TOOL_URL = "test://t1.url/toolfunction"; + const TOOL_RESPONSE = '{ "validationResult": "PASS", "output": "Test" }'; + const CONVERSION_FUNCTION_ID = "test-function-id"; + + const PARAMETER_1_NAME = "test-param-1"; + const PARAMETER_2_NAME = "language"; + + const PARAMETERS_INPUT = { + [PARAMETER_1_NAME]: "Parameter 1 value", + [PARAMETER_2_NAME]: "Parameter 2 value" + }; + + let platform; + + + beforeEach(()=>{ + // Setup + jasmine.Ajax.install(); + + platform = new EducationPlatformApp(); + + jasmine.Ajax.stubRequest(TOOL_URL).andReturn({ + "responseText": TOOL_RESPONSE, + "status": 200 + }); + + // platform - toolsManager + let toolsManagerSpy = jasmine.createSpyObj(['getActionFunction']); + toolsManagerSpy.getActionFunction.and.returnValue(new ActionFunction({ + path: TOOL_URL + })); + platform.toolsManager= toolsManagerSpy; + + }) + + afterEach(function() { + jasmine.Ajax.uninstall(); + }); + + it("returns the result via promise", async () => { + // Call the target object + const functionResponse = platform.functionRegistry_call(CONVERSION_FUNCTION_ID, PARAMETERS_INPUT); + + // Check the expected results + await expectAsync(functionResponse).toBeResolvedTo(TOOL_RESPONSE); + }) + + it("sends a request to the tool service url", async () => { + // Call the target object + platform.functionRegistry_call(CONVERSION_FUNCTION_ID, PARAMETERS_INPUT); + + // Check the expected results + const request = jasmine.Ajax.requests.mostRecent(); + expect(request.data()).toEqual( PARAMETERS_INPUT ); + }) + }) + + describe("functionRegistry_callConversion()", () => { + const TOOL_URL = "test://t1.url/toolfunction"; + const CONVERSION_FUNCTION_ID = "test-function-id"; + const PARAMETER_NAME = "testParameter1"; + const TYPE_MODEL = "type-model"; + const TYPE_METAMODEL = "type-metamodel"; + const MODEL_CONTENTS = "Parameter 1 model value"; + const METAMODEL_CONTENTS = "Parameter 2 metamodel value"; + + const TYPE_MAP_INPUT = { + [TYPE_MODEL]: MODEL_CONTENTS, + [TYPE_METAMODEL]: METAMODEL_CONTENTS + }; + + const CONVERSION_PARAM_IN = "input"; + const CONVERSION_PARAM_MM = "metamodel"; + const CONVERTED_MODEL = "Converted model contents."; + const TOOL_RESPONSE = `{"output": "${CONVERTED_MODEL}"}`; + + let platform; + + + beforeEach(()=>{ + // Setup + jasmine.Ajax.install(); + + platform = new EducationPlatformApp(); + + // xhr + jasmine.Ajax.stubRequest(TOOL_URL).andReturn({ + "responseText": TOOL_RESPONSE, + "status": 200 + }); + + // platform - toolsManager + let toolsManagerSpy = jasmine.createSpyObj(['getActionFunction']); + toolsManagerSpy.getActionFunction.and.returnValue(new ActionFunction({ + parameters: [ + {name: CONVERSION_PARAM_IN, type: TYPE_MODEL, instanceOf: "metamodel"}, + {name: CONVERSION_PARAM_MM, type: TYPE_METAMODEL} + ], + path: TOOL_URL + })); + platform.toolsManager= toolsManagerSpy; + }) + + afterEach(function() { + jasmine.Ajax.uninstall(); + }); + + it("sends a request to the tool service url", async () => { + const EXPECTED_REQUEST = { + [CONVERSION_PARAM_IN]: MODEL_CONTENTS, + [CONVERSION_PARAM_MM]: METAMODEL_CONTENTS + } + + // Call the target object + platform.functionRegistry_callConversion(CONVERSION_FUNCTION_ID, TYPE_MAP_INPUT, PARAMETER_NAME); + + // Check the expected results + const request = jasmine.Ajax.requests.mostRecent(); + expect(request.data()).toEqual( EXPECTED_REQUEST ); + }) + + it("returns the converted result via a promise", async () => { + const EXPECTED_RESPONSE = { name: PARAMETER_NAME, data: CONVERTED_MODEL }; // Format given by utility jsonRequestConversion() + + // Call the target object + const conversionResponse = platform.functionRegistry_callConversion(CONVERSION_FUNCTION_ID, TYPE_MAP_INPUT, PARAMETER_NAME); + + // Check the expected results + await expectAsync(conversionResponse).toBeResolvedTo(EXPECTED_RESPONSE); + }) + }) + describe("notification()", () => { let platform;