diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5a9c61d988e097c71f99ba71d770ccc48048bb61..d0efc9eeb300ef9886c72d307271edd438fbb8f2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,5 @@ stages: - test - - deploy coverage: stage: test script: diff --git a/angular.json b/angular.json index 493ea8b3791d817fe16e24ace1a1eb2fe26ab8ad..dbbca1144b54d9cdde098ce0a2ad8a4f3f3e5654 100644 --- a/angular.json +++ b/angular.json @@ -12,7 +12,7 @@ "schematics": {}, "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-builders/custom-webpack:browser", "options": { "outputPath": "www", "index": "src/index.html", diff --git a/package-lock.json b/package-lock.json index 66ea230ca881df5f30dc73f0a35f083e3db47792..fe6181bec9658ac5173245e8066493471c5b5181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mc_frontend", - "version": "1.5.7", + "version": "1.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -94,6 +94,27 @@ "worker-plugin": "3.2.0" }, "dependencies": { + "coverage-istanbul-loader": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/coverage-istanbul-loader/-/coverage-istanbul-loader-2.0.3.tgz", + "integrity": "sha512-LiGRvyIuzVYs3M1ZYK1tF0HekjH0DJ8zFdUwAZq378EJzqOgToyb1690dp3TAUlP6Y+82uu42LRjuROVeJ54CA==", + "requires": { + "convert-source-map": "^1.7.0", + "istanbul-lib-instrument": "^4.0.0", + "loader-utils": "^1.2.3", + "merge-source-map": "^1.1.0", + "schema-utils": "^2.6.1" + } + }, + "schema-utils": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", + "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, "webpack-merge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", @@ -1512,6 +1533,31 @@ "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", "dev": true }, + "coverage-istanbul-loader": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/coverage-istanbul-loader/-/coverage-istanbul-loader-2.0.3.tgz", + "integrity": "sha512-LiGRvyIuzVYs3M1ZYK1tF0HekjH0DJ8zFdUwAZq378EJzqOgToyb1690dp3TAUlP6Y+82uu42LRjuROVeJ54CA==", + "dev": true, + "requires": { + "convert-source-map": "^1.7.0", + "istanbul-lib-instrument": "^4.0.0", + "loader-utils": "^1.2.3", + "merge-source-map": "^1.1.0", + "schema-utils": "^2.6.1" + }, + "dependencies": { + "schema-utils": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", + "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + } + } + }, "find-cache-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz", @@ -3715,9 +3761,9 @@ } }, "@types/jasmine": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.16.tgz", - "integrity": "sha512-056oRlBBp7MDzr+HoU5su099s/s7wjZ3KcHxLfv+Byqb9MwdLUvsfLgw1VS97hsh3ddxSPyQu+olHMnoVTUY6g==", + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.9.tgz", + "integrity": "sha512-KNL2Fq6GRmty2j6+ZmueT/Z/dkctLNH+5DFoGHNDtcgt7yME9NZd8x2p81Yuea1Xux/qAryDd3zVLUoKpDz1TA==", "dev": true }, "@types/jasminewd2": { @@ -5349,38 +5395,15 @@ } }, "connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, "requires": { "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", "utils-merge": "1.0.1" - }, - "dependencies": { - "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.1", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", - "unpipe": "~1.0.0" - } - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", - "dev": true - } } }, "connect-history-api-fallback": { @@ -6219,9 +6242,9 @@ "integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ==" }, "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", + "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==" }, "core-js-compat": { "version": "3.6.4", @@ -6266,69 +6289,6 @@ } } }, - "coverage-istanbul-loader": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/coverage-istanbul-loader/-/coverage-istanbul-loader-2.0.3.tgz", - "integrity": "sha512-LiGRvyIuzVYs3M1ZYK1tF0HekjH0DJ8zFdUwAZq378EJzqOgToyb1690dp3TAUlP6Y+82uu42LRjuROVeJ54CA==", - "requires": { - "convert-source-map": "^1.7.0", - "istanbul-lib-instrument": "^4.0.0", - "loader-utils": "^1.2.3", - "merge-source-map": "^1.1.0", - "schema-utils": "^2.6.1" - }, - "dependencies": { - "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "schema-utils": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", - "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -6602,9 +6562,9 @@ } }, "date-format": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz", - "integrity": "sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", "dev": true }, "debug": { @@ -7642,9 +7602,9 @@ } }, "flatted": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", - "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, "flush-write-stream": { @@ -9284,6 +9244,11 @@ } } }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" + }, "istanbul-lib-hook": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", @@ -9293,6 +9258,27 @@ "append-transform": "^1.0.0" } }, + "istanbul-lib-instrument": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", + "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", + "requires": { + "@babel/core": "^7.7.5", + "@babel/parser": "^7.7.5", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "istanbul-lib-report": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", @@ -9415,9 +9401,9 @@ } }, "jasmine-core": { - "version": "2.99.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", - "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", "dev": true }, "jasmine-spec-reporter": { @@ -9546,18 +9532,17 @@ } }, "karma": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/karma/-/karma-4.1.0.tgz", - "integrity": "sha512-xckiDqyNi512U4dXGOOSyLKPwek6X/vUizSy2f3geYevbLj+UIdvNwbn7IwfUIL2g1GXEPWt/87qFD1fBbl/Uw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-4.4.1.tgz", + "integrity": "sha512-L5SIaXEYqzrh6b1wqYC42tNsFMx2PWuxky84pK9coK09MvmL7mxii3G3bZBh/0rvD27lqDd0le9jyhzvwif73A==", "dev": true, "requires": { "bluebird": "^3.3.0", "body-parser": "^1.16.1", - "braces": "^2.3.2", - "chokidar": "^2.0.3", + "braces": "^3.0.2", + "chokidar": "^3.0.0", "colors": "^1.1.0", "connect": "^3.6.0", - "core-js": "^2.2.0", "di": "^0.0.1", "dom-serialize": "^2.2.0", "flatted": "^2.0.0", @@ -9565,7 +9550,7 @@ "graceful-fs": "^4.1.2", "http-proxy": "^1.13.0", "isbinaryfile": "^3.0.0", - "lodash": "^4.17.11", + "lodash": "^4.17.14", "log4js": "^4.0.0", "mime": "^2.3.1", "minimatch": "^3.0.2", @@ -9580,17 +9565,122 @@ "useragent": "2.3.0" }, "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", + "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.3.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "mime": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.3.tgz", - "integrity": "sha512-QgrPRJfE+riq5TPZMcHZOtm8c6K/yYrMbKIoRfapfiGLxS8OTeIfRhUGW5LU7MlRa52KOAGCfUNruqLrIBvWZw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "readdirp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", + "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.7" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, @@ -9614,20 +9704,28 @@ } }, "karma-jasmine": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", - "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=", - "dev": true - }, - "karma-jasmine-html-reporter": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", - "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.1.1.tgz", + "integrity": "sha512-pxBmv5K7IkBRLsFSTOpgiK/HzicQT3mfFF+oHAC7nxMfYKhaYFgxOa5qjnHW4sL5rUnmdkSajoudOnnOdPyW4Q==", "dev": true, "requires": { - "karma-jasmine": "^1.0.2" + "jasmine-core": "^3.5.0" + }, + "dependencies": { + "jasmine-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + } } }, + "karma-jasmine-html-reporter": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.5.2.tgz", + "integrity": "sha512-ILBPsXqQ3eomq+oaQsM311/jxsypw5/d0LnZXj26XkfThwq7jZ55A2CFSKJVA5VekbbOGvMyv7d3juZj0SeTxA==", + "dev": true + }, "karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -9795,16 +9893,16 @@ } }, "log4js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.2.0.tgz", - "integrity": "sha512-1dJ2ORJcdqbzxvzKM2ceqPBh4O6bbICJpB4dvSEUoMcb14s8MqQ/54zNPqekuN5yjGtxO3GUDTvZfQOQhwdqnA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", + "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", "dev": true, "requires": { "date-format": "^2.0.0", "debug": "^4.1.1", "flatted": "^2.0.0", - "rfdc": "^1.1.2", - "streamroller": "^1.0.5" + "rfdc": "^1.1.4", + "streamroller": "^1.0.6" }, "dependencies": { "debug": { @@ -9817,9 +9915,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } @@ -12776,9 +12874,9 @@ } }, "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==", "dev": true }, "socket.io-client": { @@ -13207,16 +13305,16 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, "streamroller": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.5.tgz", - "integrity": "sha512-iGVaMcyF5PcUY0cPbW3xFQUXnr9O4RZXNBBjhuLZgrjLO4XCLLGfx4T2sGqygSeylUjwgWRsnNbT9aV0Zb8AYw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", + "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", "dev": true, "requires": { "async": "^2.6.2", "date-format": "^2.0.0", "debug": "^3.2.6", "fs-extra": "^7.0.1", - "lodash": "^4.17.11" + "lodash": "^4.17.14" }, "dependencies": { "debug": { @@ -13229,9 +13327,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } diff --git a/package.json b/package.json index 48c5b8885124a8895a39091667fa2a8a317a97fc..7b0dac3a4bea4e87c8d62dcbabfc8f365dccd878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mc_frontend", - "version": "1.7.0", + "version": "1.7.2", "author": "Ionic Framework", "homepage": "https://ionicframework.com/", "scripts": { @@ -9,6 +9,7 @@ "build": "ng build", "test": "ng test --code-coverage --watch=false", "test-debug": "ng test --watch=true --browsers=Chrome", + "test-cov": "ng test --code-coverage --watch=true", "lint": "ng lint", "e2e": "ng e2e" }, @@ -37,7 +38,7 @@ "cordova-plugin-splashscreen": "^5.0.3", "cordova-plugin-statusbar": "^2.4.3", "cordova-plugin-whitelist": "^1.3.4", - "core-js": "^2.6.11", + "core-js": "^3.6.4", "rxjs": "^6.5.4", "tslib": "^1.11.1", "webpack": "^4.42.0", @@ -53,17 +54,17 @@ "@angular/compiler-cli": "^9.0.4", "@angular/language-service": "^7.2.16", "@ionic/angular-toolkit": "^2.2.0", - "@types/jasmine": "~2.8.8", + "@types/jasmine": "^3.5.9", "@types/jasminewd2": "^2.0.8", "@types/node": "^12.0.12", "codelyzer": "^5.2.1", - "jasmine-core": "~2.99.1", + "jasmine-core": "^3.5.0", "jasmine-spec-reporter": "~4.2.1", - "karma": "~4.1.0", + "karma": "^4.4.1", "karma-chrome-launcher": "^3.1.0", "karma-coverage-istanbul-reporter": "^2.0.6", - "karma-jasmine": "~1.1.2", - "karma-jasmine-html-reporter": "^0.2.2", + "karma-jasmine": "^3.1.1", + "karma-jasmine-html-reporter": "^1.5.2", "protractor": "^5.4.3", "ts-node": "^8.1.1", "tslint": "~5.16.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index acaeaf910dc5f86f022420fdcb24e2c87c25efba..14588461f3a7c86aadf4eaf903f4b2ecf0ee7bca 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -22,6 +22,10 @@ const routes: Routes = [ { path: 'doc-voc-unit', loadChildren: './doc-voc-unit/doc-voc-unit.module#DocVocUnitPageModule' }, { path: 'doc-exercises', loadChildren: './doc-exercises/doc-exercises.module#DocExercisesPageModule' }, { path: 'doc-software', loadChildren: './doc-software/doc-software.module#DocSoftwarePageModule' }, + { + path: 'semantics', + loadChildren: () => import('./semantics/semantics.module').then( m => m.SemanticsPageModule) + }, diff --git a/src/app/app.component.html b/src/app/app.component.html index bb13d3a2cec40d6817f066c272b4582cfcf4b621..cdc28190686103353c9fba737e5e8d7277a9b7c4 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -14,32 +14,39 @@ - + {{'HOME' | translate}} - + {{ 'EXERCISE_GENERATE' | translate }} - + {{ 'EXERCISE_LIST' | translate }} - + {{ 'TEST' | translate }} + + + + {{ 'SEMANTICS' | translate }} + + + @@ -47,35 +54,35 @@ - + {{ 'ABOUT' | translate }} - + {{ 'DOC_SOFTWARE' | translate }} - + {{ 'DOC_EXERCISES' | translate }} - + {{ 'DOC_VOC_UNIT' | translate }} - + {{ 'SOURCES' | translate }} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 0de89b174c93c9b5bc59b25eff85d0579718cca7..48459e89de7da279ceb3cdfe237ea4d2296bfd4e 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,5 +1,5 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {TestBed, async} from '@angular/core/testing'; +import {TestBed, async, ComponentFixture} from '@angular/core/testing'; import {MenuController, Platform} from '@ionic/angular'; import {SplashScreen} from '@ionic-native/splash-screen/ngx'; @@ -13,9 +13,14 @@ import {TranslateTestingModule} from './translate-testing/translate-testing.modu import {APP_BASE_HREF} from '@angular/common'; import {Subscription} from 'rxjs'; import {HelperService} from './helper.service'; -import MockMC from './models/mock'; +import {CorpusService} from './corpus.service'; +import Spy = jasmine.Spy; +import MockMC from './models/mockMC'; describe('AppComponent', () => { + let statusBarSpy, splashScreenSpy, platformReadySpy, fixture: ComponentFixture, + appComponent: AppComponent; + class PlatformStub { backButton = { subscribeWithPriority(priority: number, callback: () => (Promise | void)): Subscription { @@ -30,7 +35,6 @@ describe('AppComponent', () => { } } - let statusBarSpy, splashScreenSpy, platformReadySpy, fixture; beforeEach(async(() => { platformReadySpy = Promise.resolve(); statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']); @@ -49,11 +53,16 @@ describe('AppComponent', () => { {provide: SplashScreen, useValue: splashScreenSpy}, {provide: Platform, useClass: PlatformStub}, {provide: APP_BASE_HREF, useValue: '/'}, - {provide: MenuController} + {provide: MenuController}, + {provide: CorpusService, useValue: {initCorpusService: () => Promise.resolve()}}, + { + provide: HelperService, + useValue: {makeGetRequest: () => Promise.resolve(MockMC.apiResponseCorporaGet)} + } ], - }).compileComponents(); - spyOn(HelperService, 'makeGetRequest').and.returnValue(Promise.resolve(MockMC.apiResponseCorporaGet)); + }).compileComponents().then(); fixture = TestBed.createComponent(AppComponent); + appComponent = fixture.componentInstance; })); it('should create the app', () => { @@ -69,6 +78,20 @@ describe('AppComponent', () => { expect(splashScreenSpy.hide).toHaveBeenCalled(); }); + it('should close the menu', () => { + const closeSpy: Spy = spyOn(appComponent.menuCtrl, 'close').and.returnValue(Promise.resolve(true)); + appComponent.closeMenu(true); + expect(closeSpy).toHaveBeenCalledTimes(1); + }); + + it('should initialize the translations', () => { + spyOn(appComponent.translate, 'getBrowserLang').and.returnValue(undefined); + const languageSpy: Spy = spyOn(appComponent.translate, 'getDefaultLang').and.returnValue('de'); + appComponent.initTranslate(); + expect(appComponent.translate.currentLang).toBe('de'); + expect(languageSpy).toHaveBeenCalledTimes(1); + }); + // TODO: add more tests! }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 86696ff6f3879420bceaa767d5990061a1a47945..8a667b4d0af299dfe5530507cbee82eea74450ef 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,7 +13,6 @@ import {CorpusService} from './corpus.service'; }) export class AppComponent { public configMC = configMC; - public HelperService = HelperService; constructor(platform: Platform, public statusBar: StatusBar, diff --git a/src/app/author-detail/author-detail.page.html b/src/app/author-detail/author-detail.page.html index 2d35e198a491e3910efc66c8e79ade4a894d01da..e0abc7abe199abc6f01a4a502e328103db5dcf66 100644 --- a/src/app/author-detail/author-detail.page.html +++ b/src/app/author-detail/author-detail.page.html @@ -2,13 +2,13 @@ - - {{ state.currentSetup.currentAuthor?.name }} + + {{ state.currentSetup.currentAuthor?.name }} @@ -19,7 +19,7 @@ - + diff --git a/src/app/exercise/exercise.page.spec.ts b/src/app/exercise/exercise.page.spec.ts index 4f82b71ab3dee6708a0cb34fecad693ad81812de..b429901672b5570ade25dd710bf6486b7a4e2f7b 100644 --- a/src/app/exercise/exercise.page.spec.ts +++ b/src/app/exercise/exercise.page.spec.ts @@ -1,18 +1,25 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; - import {ExercisePage} from './exercise.page'; import {HttpClientModule} from '@angular/common/http'; import {IonicStorageModule} from '@ionic/storage'; -import {RouterModule} from '@angular/router'; +import {ActivatedRoute, RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {APP_BASE_HREF} from '@angular/common'; -import {CorpusService} from '../corpus.service'; -import {ToastController} from '@ionic/angular'; +import {of} from 'rxjs'; +import {AnnisResponse} from '../models/annisResponse'; +import {ExerciseType, MoodleExerciseType} from '../models/enum'; +import MockMC from '../models/mockMC'; +import Spy = jasmine.Spy; +import configMC from '../../configMC'; describe('ExercisePage', () => { - let component: ExercisePage; + let exercisePage: ExercisePage; let fixture: ComponentFixture; + let checkSpy: Spy; + const activatedRouteMock: any = {queryParams: of({eid: 'eid', type: ExerciseType.cloze.toString()})}; + let getRequestSpy: Spy; + let h5pSpy: Spy; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -25,19 +32,57 @@ describe('ExercisePage', () => { ], providers: [ {provide: APP_BASE_HREF, useValue: '/'}, + {provide: ActivatedRoute, useValue: activatedRouteMock} ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - .compileComponents(); - })); - - beforeEach(() => { + .compileComponents().then(); fixture = TestBed.createComponent(ExercisePage); - component = fixture.componentInstance; + exercisePage = fixture.componentInstance; + exercisePage.helperService.applicationState.next(exercisePage.helperService.deepCopy(MockMC.applicationState)); + h5pSpy = spyOn(exercisePage.exerciseService, 'initH5P').and.returnValue(Promise.resolve()); + checkSpy = spyOn(exercisePage.corpusService, 'checkAnnisResponse').and.returnValue(Promise.resolve()); + getRequestSpy = spyOn(exercisePage.helperService, 'makeGetRequest').and.returnValue(Promise.resolve( + new AnnisResponse({exercise_type: MoodleExerciseType.cloze.toString()}))); fixture.detectChanges(); - }); + })); it('should create', () => { - expect(component).toBeTruthy(); + expect(exercisePage).toBeTruthy(); + }); + + it('should be initialized', (done) => { + const loadExerciseSpy: Spy = spyOn(exercisePage, 'loadExercise'); + checkSpy.and.callFake(() => Promise.reject()); + exercisePage.ngOnInit().then(() => { + expect(loadExerciseSpy).toHaveBeenCalledTimes(0); + done(); + }); + }); + + it('should load the exercise', (done) => { + exercisePage.loadExercise().then(() => { + expect(exercisePage.corpusService.exercise.type).toBe(ExerciseType.cloze); + getRequestSpy.and.returnValue(Promise.resolve(new AnnisResponse({exercise_type: MoodleExerciseType.markWords.toString()}))); + exercisePage.loadExercise().then(() => { + expect(h5pSpy).toHaveBeenCalledWith(configMC.excerciseTypePathMarkWords); + getRequestSpy.and.callFake(() => Promise.reject()); + exercisePage.loadExercise().then(() => { + }, () => { + activatedRouteMock.queryParams = of({eid: '', type: ExerciseType.matching.toString()}); + exercisePage.loadExercise().then(() => { + expect(h5pSpy).toHaveBeenCalledWith(ExerciseType.matching.toString()); + activatedRouteMock.queryParams = of({ + eid: '', + type: exercisePage.exerciseService.vocListString + }); + exercisePage.loadExercise().then(() => { + expect(h5pSpy).toHaveBeenCalledWith(exercisePage.exerciseService.fillBlanksString); + done(); + }); + }); + }); + }); + }); }); }); diff --git a/src/app/exercise/exercise.page.ts b/src/app/exercise/exercise.page.ts index d3273cdb8d039dc0f4ffefb3270e543c986c5639..141d4dd615656bec6d4dc29f6a5da754dcf8134a 100644 --- a/src/app/exercise/exercise.page.ts +++ b/src/app/exercise/exercise.page.ts @@ -21,8 +21,6 @@ import {Storage} from '@ionic/storage'; }) export class ExercisePage implements OnInit { - HelperService = HelperService; - constructor(public navCtrl: NavController, public activatedRoute: ActivatedRoute, public translateService: TranslateService, @@ -32,49 +30,61 @@ export class ExercisePage implements OnInit { public helperService: HelperService, public corpusService: CorpusService, public storage: Storage) { - this.corpusService.checkAnnisResponse().then(() => { - this.loadExercise(); - }, () => { - }); } - loadExercise(): void { - this.activatedRoute.queryParams.subscribe((params: object) => { - if (params['eid']) { - let url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath; - const httpParams: HttpParams = new HttpParams().set('eid', params['eid']); - HelperService.makeGetRequest(this.http, this.toastCtrl, url, httpParams).then((ar: AnnisResponse) => { - HelperService.applicationState.pipe(take(1)).subscribe((as: ApplicationState) => { - as.mostRecentSetup.annisResponse = ar; - this.helperService.saveApplicationState(as).then(); - this.corpusService.annisResponse = ar; - const met: MoodleExerciseType = MoodleExerciseType[ar.exercise_type]; - this.corpusService.exercise.type = ExerciseType[met.toString()]; - // this will be called via GET request from the h5p standalone javascript library - url = `${configMC.backendBaseUrl}${configMC.backendApiH5pPath}` + - `?eid=${this.corpusService.annisResponse.exercise_id}&lang=${this.translateService.currentLang}`; - this.storage.set(configMC.localStorageKeyH5P, url).then(); - const exerciseTypePath: string = this.corpusService.exercise.type === ExerciseType.markWords ? - 'mark_words' : 'drag_text'; - this.exerciseService.initH5P(exerciseTypePath); + loadExercise(): Promise { + return new Promise((resolve, reject) => { + this.activatedRoute.queryParams.subscribe((params: object) => { + if (params['eid']) { + let url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath; + const httpParams: HttpParams = new HttpParams().set('eid', params['eid']); + this.helperService.makeGetRequest(this.http, this.toastCtrl, url, httpParams).then((ar: AnnisResponse) => { + this.helperService.applicationState.pipe(take(1)).subscribe((as: ApplicationState) => { + as.mostRecentSetup.annisResponse = ar; + this.helperService.saveApplicationState(as).then(); + this.corpusService.annisResponse = ar; + const met: MoodleExerciseType = MoodleExerciseType[ar.exercise_type]; + this.corpusService.exercise.type = ExerciseType[met.toString()]; + // this will be called via GET request from the h5p standalone javascript library + url = `${configMC.backendBaseUrl}${configMC.backendApiH5pPath}` + + `?eid=${this.corpusService.annisResponse.exercise_id}&lang=${this.translateService.currentLang}`; + this.storage.set(configMC.localStorageKeyH5P, url).then(); + const exerciseTypePath: string = this.corpusService.exercise.type === ExerciseType.markWords ? + configMC.excerciseTypePathMarkWords : 'drag_text'; + this.exerciseService.initH5P(exerciseTypePath).then(() => { + return resolve(); + }); + }); + }, () => { + return reject(); }); - }, () => { - }); - } else { - const exerciseType: string = params['type']; - const exerciseTypePath: string = exerciseType === this.exerciseService.vocListString ? - this.exerciseService.fillBlanksString : exerciseType; - const file: string = params['file']; - const lang: string = this.translateService.currentLang; - this.storage.set(configMC.localStorageKeyH5P, - HelperService.baseUrl + '/assets/h5p/' + exerciseType + '/content/' + file + '_' + lang + '.json') - .then(); - this.exerciseService.initH5P(exerciseTypePath); - } + } else { + const exerciseType: string = params['type']; + const exerciseTypePath: string = exerciseType === this.exerciseService.vocListString ? + this.exerciseService.fillBlanksString : exerciseType; + const file: string = params['file']; + const lang: string = this.translateService.currentLang; + this.storage.set(configMC.localStorageKeyH5P, + this.helperService.baseUrl + '/assets/h5p/' + exerciseType + '/content/' + file + '_' + lang + '.json') + .then(); + this.exerciseService.initH5P(exerciseTypePath).then(() => { + return resolve(); + }); + } + }); }); } - ngOnInit() { + ngOnInit(): Promise { + return new Promise(resolve => { + this.corpusService.checkAnnisResponse().then(() => { + this.loadExercise().then(() => { + return resolve(); + }); + }, () => { + return resolve(); + }); + }); } } diff --git a/src/app/helper.service.spec.ts b/src/app/helper.service.spec.ts index ef3bc2396191f9de365f9c411a674c8370c25f7a..dfd162bfa060a21a29966cdcd3830836d2d3b1eb 100644 --- a/src/app/helper.service.spec.ts +++ b/src/app/helper.service.spec.ts @@ -4,21 +4,205 @@ import {HelperService} from './helper.service'; import {IonicStorageModule} from '@ionic/storage'; import {TranslateTestingModule} from './translate-testing/translate-testing.module'; import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TranslateHttpLoader} from '@ngx-translate/http-loader'; +import {Observable, of, range} from 'rxjs'; +import Spy = jasmine.Spy; +import {PartOfSpeechValue} from './models/enum'; +import {NavController, ToastController} from '@ionic/angular'; +import configMC from '../configMC'; +import {AppRoutingModule} from './app-routing.module'; +import {APP_BASE_HREF} from '@angular/common'; +import {ApplicationState} from './models/applicationState'; +import {take} from 'rxjs/operators'; +import {HttpErrorResponse, HttpParams} from '@angular/common/http'; +import MockMC from './models/mockMC'; describe('HelperService', () => { + let helperService: HelperService; beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, IonicStorageModule.forRoot(), TranslateTestingModule, + AppRoutingModule, + ], + providers: [ + {provide: APP_BASE_HREF, useValue: '/'}, ], - providers: [], }); + helperService = TestBed.inject(HelperService); }); it('should be created', () => { - const service: HelperService = TestBed.get(HelperService); - expect(service).toBeTruthy(); + expect(helperService).toBeTruthy(); + }); + + it('should test IE11 mode', () => { + // @ts-ignore + // tslint:disable-next-line:no-string-literal + window['MSInputMethodContext'] = true; + // tslint:disable-next-line:no-string-literal + document['documentMode'] = true; + const helperService2: HelperService = new HelperService(helperService.http, helperService.storage, helperService.translate); + expect(helperService2.isIE11).toBe(true); + }); + + it('should create a translate loader', () => { + const thl: TranslateHttpLoader = HelperService.createTranslateLoader(helperService.http); + expect(thl.suffix).toBe('.json'); + }); + + it('should shuffle an array', () => { + const array: number[] = [0, 1]; + const firstNumberArray: number[] = []; + range(0, 100).forEach(() => { + firstNumberArray.push(HelperService.shuffle(array)[0]); + }); + expect(firstNumberArray).toContain(1); + }); + + it('should create a deep copy', () => { + expect(helperService.deepCopy(undefined)).toBe(undefined); + const oldDate: Date = new Date(1); + const newDate: Date = helperService.deepCopy(oldDate); + newDate.setTime(10000); + expect(oldDate.getTime()).toBe(1); + const oldArray: number[] = [0]; + const newArray: number[] = helperService.deepCopy(oldArray); + newArray[0] = 1; + expect(oldArray[0]).toBe(0); + const oldObject: any = {unchanged: true}; + const newObject: any = helperService.deepCopy(oldObject); + newObject.unchanged = false; + expect(oldObject.unchanged).toBe(true); + }); + + it('should get a delayed translation', (done) => { + const key = 'TEST'; + let translateSpy: Spy; + helperService.getDelayedTranslation(helperService.translate, key).then((value: string) => { + expect(value).toBe(key); + translateSpy.and.returnValue(of(key.toLowerCase())); + helperService.getDelayedTranslation(helperService.translate, key).then((newValue: string) => { + expect(newValue).toBe(key.toLowerCase()); + done(); + }); + }); + translateSpy = spyOn(helperService.translate, 'get').and.returnValue(of(key)); + }); + + it('should get enum values', () => { + const enumValues: string[] = helperService.getEnumValues(PartOfSpeechValue); + enumValues.forEach((ev: string) => expect(PartOfSpeechValue[ev]).toBeTruthy()); + expect(enumValues.length).toBe(7); + }); + + it('should go to a specific page', (done) => { + function checkNavigation(navFunctions: any[], pageUrls: string[], navController: NavController, navSpy: Spy): Promise { + return new Promise(resolve => { + range(0, navFunctions.length).forEach(async (idx: number) => { + await navFunctions[idx](navController); + expect(navSpy).toHaveBeenCalledWith(pageUrls[idx]); + }); + return resolve(); + }); + } + + const navCtrl: NavController = TestBed.inject(NavController); + const forwardSpy: Spy = spyOn(navCtrl, 'navigateForward').and.returnValue(Promise.resolve(true)); + const navFnArr: any[] = [helperService.goToAuthorDetailPage, helperService.goToDocExercisesPage, helperService.goToDocSoftwarePage, + helperService.goToDocVocUnitPage, helperService.goToExerciseListPage, helperService.goToExerciseParametersPage, + helperService.goToImprintPage, helperService.goToInfoPage, helperService.goToPreviewPage, helperService.goToSourcesPage, + helperService.goToTextRangePage, helperService.goToVocabularyCheckPage, helperService.goToKwicPage]; + const pageUrlArr: string[] = [configMC.pageUrlAuthorDetail, configMC.pageUrlDocExercises, configMC.pageUrlDocSoftware, + configMC.pageUrlDocVocUnit, configMC.pageUrlExerciseList, configMC.pageUrlExerciseParameters, configMC.pageUrlImprint, + configMC.pageUrlInfo, configMC.pageUrlPreview, configMC.pageUrlSources, configMC.pageUrlTextRange, + configMC.pageUrlVocabularyCheck, configMC.pageUrlKwic]; + checkNavigation(navFnArr, pageUrlArr, navCtrl, forwardSpy).then(() => { + helperService.goToAuthorPage(navCtrl).then(() => { + expect(helperService.isVocabularyCheck).toBeFalsy(); + helperService.goToShowTextPage(navCtrl, true).then(() => { + expect(helperService.isVocabularyCheck).toBe(true); + const rootSpy: Spy = spyOn(navCtrl, 'navigateRoot').and.returnValue(Promise.resolve(true)); + helperService.goToHomePage(navCtrl).then(() => { + expect(rootSpy).toHaveBeenCalledWith(configMC.pageUrlHome); + helperService.goToTestPage(navCtrl).then(() => { + expect(rootSpy).toHaveBeenCalledWith(configMC.pageUrlTest); + done(); + }); + }); + }); + }); + }); + }); + + it('should initialize the application state', (done) => { + function updateState(newState: ApplicationState): Promise { + return new Promise(resolve => { + helperService.applicationStateCache = null; + helperService.initApplicationState(); + helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => { + resolve(state); + }); + }); + } + + const localStorageSpy: Spy = spyOn(helperService.storage, 'get').withArgs(configMC.localStorageKeyApplicationState) + .and.returnValue(Promise.resolve(JSON.stringify(new ApplicationState()))); + updateState(new ApplicationState()).then((state: ApplicationState) => { + expect(state).toBeTruthy(); + localStorageSpy.and.returnValue(Promise.resolve(new ApplicationState({exerciseList: []}))); + updateState(new ApplicationState({exerciseList: []})).then((state2: ApplicationState) => { + expect(state2.exerciseList.length).toBe(0); + helperService.initApplicationState(); + helperService.applicationState.pipe(take(1)).subscribe((state3: ApplicationState) => { + expect(state3.exerciseList.length).toBe(0); + done(); + }); + }); + }); + }); + + it('should make a get request', (done) => { + const toastCtrl: ToastController = TestBed.inject(ToastController); + spyOn(toastCtrl, 'create').and.returnValue(Promise.resolve({present: () => Promise.resolve()} as HTMLIonToastElement)); + const httpSpy: Spy = spyOn(helperService.http, 'get').and.returnValue(of(0)); + helperService.makeGetRequest(helperService.http, toastCtrl, '', new HttpParams()).then((result: number) => { + expect(httpSpy).toHaveBeenCalledTimes(1); + expect(result).toBe(0); + httpSpy.and.returnValue(new Observable(subscriber => subscriber.error(new HttpErrorResponse({status: 500})))); + helperService.makeGetRequest(helperService.http, toastCtrl, '', new HttpParams()).then(() => { + }, (error: HttpErrorResponse) => { + expect(error.status).toBe(500); + done(); + }); + }); + }); + + it('should make a post request', (done) => { + const toastCtrl: ToastController = TestBed.inject(ToastController); + spyOn(toastCtrl, 'create').and.returnValue(Promise.resolve({present: () => Promise.resolve()} as HTMLIonToastElement)); + const httpSpy: Spy = spyOn(helperService.http, 'post').and.returnValue(of(0)); + helperService.makePostRequest(helperService.http, toastCtrl, '', new FormData()).then((result: number) => { + expect(httpSpy).toHaveBeenCalledTimes(1); + expect(result).toBe(0); + httpSpy.and.returnValue(new Observable(subscriber => subscriber.error(new HttpErrorResponse({status: 500})))); + helperService.makePostRequest(helperService.http, toastCtrl, '', new FormData()).then(() => { + }, (error: HttpErrorResponse) => { + expect(error.status).toBe(500); + done(); + }); + }); + }); + + it('should save the application state', (done) => { + helperService.saveApplicationState(helperService.deepCopy(MockMC.applicationState)).then(() => { + helperService.storage.get(configMC.localStorageKeyApplicationState).then((jsonString: string) => { + const state: ApplicationState = JSON.parse(jsonString) as ApplicationState; + expect(state.mostRecentSetup.annisResponse.nodes.length).toBe(1); + done(); + }); + }); }); }); diff --git a/src/app/helper.service.ts b/src/app/helper.service.ts index f4db2c3f17da049a2c3ad00a3ad78f4911c4f559..43c4e572e5eb5063c90b1967a2fab5c266d386ad 100644 --- a/src/app/helper.service.ts +++ b/src/app/helper.service.ts @@ -16,12 +16,12 @@ import configMC from '../configMC'; providedIn: 'root' }) export class HelperService { - - public static applicationState: ReplaySubject = null; - private static applicationStateCache: ApplicationState = null; - public static baseUrl: string = location.protocol.concat('//').concat(window.location.host) + + public static generalErrorAlertMessage: string; + public applicationState: ReplaySubject = null; + public applicationStateCache: ApplicationState = null; + public baseUrl: string = location.protocol.concat('//').concat(window.location.host) + window.location.pathname.split('/').slice(0, -1).join('/'); - public static caseMap: { [rawValue: string]: CaseValue } = { + public caseMap: { [rawValue: string]: CaseValue } = { Nom: CaseValue.nominative, Gen: CaseValue.genitive, Dat: CaseValue.dative, @@ -30,11 +30,11 @@ export class HelperService { Voc: CaseValue.vocative, Loc: CaseValue.locative, }; - public static corpusUpdateCompletedString: string; - public static currentError: HttpErrorResponse; - public static currentLanguage: Language; - public static currentPopover: any; - public static dependencyMap: { [rawValue: string]: DependencyValue } = { + public corpusUpdateCompletedString: string; + public currentError: HttpErrorResponse; + public currentLanguage: Language; + public currentPopover: HTMLIonPopoverElement; + public dependencyMap: { [rawValue: string]: DependencyValue } = { acl: DependencyValue.adjectivalClause, advcl: DependencyValue.adverbialClauseModifier, advmod: DependencyValue.adverbialModifier, @@ -75,16 +75,14 @@ export class HelperService { vocative: DependencyValue.vocative, xcomp: DependencyValue.clausalComplement, }; - public static generalErrorAlertMessage: string; - public static isIE11: boolean = !!(window as any).MSInputMethodContext && !!(document as any).documentMode; - public static isLoading = false; - public static isDevMode = ['localhost'].indexOf(window.location.hostname) > -1; // set this to "false" for simulated production mode - public static isVocabularyCheck = false; - public static languages: Language[] = [new Language({ - name: 'English', - shortcut: 'en' - }), new Language({name: 'Deutsch', shortcut: 'de'})]; - public static partOfSpeechMap: { [rawValue: string]: PartOfSpeechValue } = { + public isIE11: boolean = !!(window as any).MSInputMethodContext && !!(document as any).documentMode; + public isDevMode = ['localhost'].indexOf(window.location.hostname) > -1; // set this to "false" for simulated production mode + public isVocabularyCheck = false; + public languages: Language[] = [ + new Language({name: 'English', shortcut: 'en'}), + new Language({name: 'Deutsch', shortcut: 'de'})]; + public openRequests: string[] = []; + public partOfSpeechMap: { [rawValue: string]: PartOfSpeechValue } = { ADJ: PartOfSpeechValue.adjective, ADP: PartOfSpeechValue.preposition, ADV: PartOfSpeechValue.adverb, @@ -105,19 +103,67 @@ export class HelperService { }; constructor(public http: HttpClient, - private storage: Storage, + public storage: Storage, public translate: TranslateService, ) { this.initConfig(); this.initLanguage(); + this.initApplicationState(); } // The translate loader needs to know where to load i18n files in Ionic's static asset pipeline. - static createTranslateLoader(http: HttpClient) { + static createTranslateLoader(http: HttpClient): TranslateHttpLoader { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); } - static delayedTranslation(translate: TranslateService, key: string) { + /** + * Shuffles array in place. + * @param array items An array containing the items. + */ + static shuffle(array: Array): Array { + let j, x, i; + for (i = array.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + x = array[i]; + array[i] = array[j]; + array[j] = x; + } + return array; + } + + deepCopy(obj: object): any { + let copy; + // Handle the 3 simple types, and null or undefined + if (null === obj || 'object' !== typeof obj) { + return obj; + } + // Handle Date + if (obj instanceof Date) { + copy = new Date(); + copy.setTime(obj.getTime()); + return copy; + } + // Handle Array + if (obj instanceof Array) { + copy = []; + for (let i = 0, len = obj.length; i < len; i++) { + copy[i] = this.deepCopy(obj[i]); + } + return copy; + } + // Handle Object + if (obj instanceof Object) { + copy = {}; + for (const attr in obj) { + if (obj.hasOwnProperty(attr)) { + copy[attr] = this.deepCopy(obj[attr]); + } + } + return copy; + } + } + + getDelayedTranslation(translate: TranslateService, key: string) { return new Promise(resolve => { translate.get(key).subscribe((value: string) => { // check if we got the correct translated value @@ -132,165 +178,117 @@ export class HelperService { }); } - static getEnumValues(target: any): string[] { + getEnumValues(target: any): string[] { return Object.keys(target).filter((value, index, array) => { return index % 2 !== 0; }); } - static goToAuthorPage(navCtrl: NavController): Promise { - HelperService.isVocabularyCheck = false; - return navCtrl.navigateForward('/author'); + goToAuthorDetailPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlAuthorDetail); } - static goToAuthorDetailPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/author-detail'); + goToAuthorPage(navCtrl: NavController): Promise { + this.isVocabularyCheck = false; + return navCtrl.navigateForward(configMC.pageUrlAuthor); } - static goToDocExercisesPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/doc-exercises'); + goToDocExercisesPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlDocExercises); } - static goToDocSoftwarePage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/doc-software'); + goToDocSoftwarePage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlDocSoftware); } - static goToDocVocUnitPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/doc-voc-unit'); + goToDocVocUnitPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlDocVocUnit); } - static goToExerciseListPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/exercise-list'); + goToExerciseListPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlExerciseList); } - static goToExerciseParametersPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('exercise-parameters'); + goToExerciseParametersPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlExerciseParameters); } - static goToHomePage(navCtrl: NavController): Promise { - return navCtrl.navigateRoot('/home'); + goToHomePage(navCtrl: NavController): Promise { + return navCtrl.navigateRoot(configMC.pageUrlHome); } - static goToImprintPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/imprint'); + goToImprintPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlImprint); } - static goToInfoPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/info'); + goToInfoPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlInfo); } - static goToPreviewPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('preview'); + goToKwicPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlKwic); } - static goToShowTextPage(navCtrl: NavController, isVocabularyCheck: boolean = false): Promise { - return new Promise((resolve) => { - navCtrl.navigateForward('/show-text').then((result: boolean) => { - HelperService.isVocabularyCheck = isVocabularyCheck; - return resolve(result); - }); - }); + goToPreviewPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlPreview); } - static goToSourcesPage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/sources'); + goToSemanticsPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlSemantics); } - static goToTestPage(navCtrl: NavController): Promise { - return navCtrl.navigateRoot('/test'); - } - - static goToTextRangePage(navCtrl: NavController): Promise { - return navCtrl.navigateForward('/text-range'); - } - - static goToVocabularyCheckPage(navCtrl: NavController): Promise { + goToShowTextPage(navCtrl: NavController, isVocabularyCheck: boolean = false): Promise { return new Promise((resolve) => { - navCtrl.navigateForward('/vocabulary-check').then((result: boolean) => { + navCtrl.navigateForward(configMC.pageUrlShowText).then((result: boolean) => { + this.isVocabularyCheck = isVocabularyCheck; return resolve(result); }); }); } - static loadTranslations(translate: TranslateService) { - // dirty hack to wait until the translation loader is initialized in IE11 - HelperService.delayedTranslation(translate, 'CORPUS_UPDATE_COMPLETED').then((value: string) => { - HelperService.corpusUpdateCompletedString = value; - }); - HelperService.delayedTranslation(translate, 'ERROR_GENERAL_ALERT').then((value: string) => { - HelperService.generalErrorAlertMessage = value; - }); + goToSourcesPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlSources); } - static makeGetRequest(http: HttpClient, toastCtrl: ToastController, url: string, params: HttpParams, - errorMessage: string = HelperService.generalErrorAlertMessage): Promise { - HelperService.currentError = null; - // dirty hack to avoid ExpressionChangedAfterItHasBeenCheckedError - setTimeout(() => { - HelperService.isLoading = true; - }, 0); - return new Promise(((resolve, reject) => { - http.get(url, {params}).subscribe((result: any) => { - // dirty hack to avoid ExpressionChangedAfterItHasBeenCheckedError - setTimeout(() => { - HelperService.isLoading = false; - }, 0); - return resolve(result); - }, async (error: HttpErrorResponse) => { - // dirty hack to avoid ExpressionChangedAfterItHasBeenCheckedError - setTimeout(() => { - HelperService.isLoading = false; - }, 0); - HelperService.currentError = error; - const toast: HTMLIonToastElement = await toastCtrl.create({ - message: errorMessage, - duration: 3000, - position: 'top' - }).catch() as HTMLIonToastElement; - toast.present().then(() => { - }, () => { - }); - return reject(error); - }); - })); + goToTestPage(navCtrl: NavController): Promise { + return navCtrl.navigateRoot(configMC.pageUrlTest); } - /** - * Shuffles array in place. - * @param array items An array containing the items. - */ - static shuffle(array: Array) { - let j, x, i; - for (i = array.length - 1; i > 0; i--) { - j = Math.floor(Math.random() * (i + 1)); - x = array[i]; - array[i] = array[j]; - array[j] = x; - } - return array; + goToTextRangePage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlTextRange); + } + + goToVocabularyCheckPage(navCtrl: NavController): Promise { + return navCtrl.navigateForward(configMC.pageUrlVocabularyCheck); + } + + handleRequestError(toastCtrl: ToastController, error: HttpErrorResponse, errorMessage: string, url: string): void { + this.openRequests.splice(this.openRequests.indexOf(url), 1); + this.currentError = error; + this.showToast(toastCtrl, errorMessage).then(); } initApplicationState(): void { - HelperService.applicationState = new ReplaySubject(1); - if (!HelperService.applicationStateCache) { + this.applicationState = new ReplaySubject(1); + if (!this.applicationStateCache) { this.storage.get(configMC.localStorageKeyApplicationState).then((jsonString: string) => { - HelperService.applicationStateCache = new ApplicationState({ + this.applicationStateCache = new ApplicationState({ currentSetup: new TextData(), exerciseList: [] }); if (jsonString) { const state: ApplicationState = JSON.parse(jsonString) as ApplicationState; state.exerciseList = state.exerciseList ? state.exerciseList : []; - HelperService.applicationStateCache = state; + this.applicationStateCache = state; } - HelperService.applicationState.next(HelperService.applicationStateCache); + this.applicationState.next(this.applicationStateCache); }); } else { - HelperService.applicationState.next(HelperService.applicationStateCache); + this.applicationState.next(this.applicationStateCache); } } - initConfig() { + initConfig(): void { if (!configMC.backendBaseUrl) { const part1: string = location.protocol.concat('//').concat(window.location.host); configMC.backendBaseUrl = part1.concat(configMC.backendBaseApiPath).concat('/'); @@ -300,18 +298,66 @@ export class HelperService { initLanguage(): void { // dirty hack to wait for the translateService intializing setTimeout(() => { - HelperService.currentLanguage = HelperService.languages.find(x => x.shortcut === this.translate.currentLang); - HelperService.loadTranslations(this.translate); + this.currentLanguage = this.languages.find(x => x.shortcut === this.translate.currentLang); + this.loadTranslations(this.translate); }); } - saveApplicationState(mrs: ApplicationState) { + loadTranslations(translate: TranslateService): void { + // dirty hack to wait until the translation loader is initialized in IE11 + this.getDelayedTranslation(translate, 'CORPUS_UPDATE_COMPLETED').then((value: string) => { + this.corpusUpdateCompletedString = value; + }); + this.getDelayedTranslation(translate, 'ERROR_GENERAL_ALERT').then((value: string) => { + HelperService.generalErrorAlertMessage = value; + }); + } + + makeGetRequest(http: HttpClient, toastCtrl: ToastController, url: string, params: HttpParams, + errorMessage: string = HelperService.generalErrorAlertMessage): Promise { + return new Promise(((resolve, reject) => { + this.currentError = null; + this.openRequests.push(url); + http.get(url, {params}).subscribe((result: any) => { + this.openRequests.splice(this.openRequests.indexOf(url), 1); + return resolve(result); + }, async (error: HttpErrorResponse) => { + this.handleRequestError(toastCtrl, error, errorMessage, url); + return reject(error); + }); + })); + } + + makePostRequest(http: HttpClient, toastCtrl: ToastController, url: string, formData: FormData, + errorMessage: string = HelperService.generalErrorAlertMessage): Promise { + return new Promise(((resolve, reject) => { + this.currentError = null; + this.openRequests.push(url); + http.post(url, formData).subscribe((result: any) => { + this.openRequests.splice(this.openRequests.indexOf(url), 1); + return resolve(result); + }, async (error: HttpErrorResponse) => { + this.handleRequestError(toastCtrl, error, errorMessage, url); + return reject(error); + }); + })); + } + + saveApplicationState(state: ApplicationState): Promise { return new Promise((resolve) => { - HelperService.applicationStateCache = mrs; - HelperService.applicationState.next(HelperService.applicationStateCache); - this.storage.set(configMC.localStorageKeyApplicationState, JSON.stringify(mrs)).then(() => { + this.applicationStateCache = state; + this.applicationState.next(this.applicationStateCache); + this.storage.set(configMC.localStorageKeyApplicationState, JSON.stringify(state)).then(() => { return resolve(); }); }); } + + showToast(toastCtrl: ToastController, message: string, position: any = 'top'): Promise { + return toastCtrl.create({ + message, + duration: 3000, + position + }).then((toast: HTMLIonToastElement) => toast.present()); + } } diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 44ae67f73983532cb55e282857739af12c0f925f..2e6c3de077121a7308a3c1cac13652ac5a6df897 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -10,10 +10,10 @@
- - - + + + {{lang.name}}
@@ -46,7 +46,7 @@ {{'EXERCISE_PARAMETERS' | translate }}

- {{ 'CONTINUE' | translate }} + {{ 'CONTINUE' | translate }}

@@ -62,7 +62,7 @@ {{'EXERCISE_TYPE_MATCHING' | translate }}

- {{ 'CONTINUE' | translate }} + {{ 'CONTINUE' | translate }}

@@ -79,7 +79,7 @@ {{'UNIT_EVALUATION_TITLE' | translate }}

- {{ 'CONTINUE' | translate }} + {{ 'CONTINUE' | translate }}

@@ -96,7 +96,7 @@ {{'DOC_VOC_UNIT' | translate }}

- {{ 'CONTINUE' | translate }} + {{ 'CONTINUE' | translate }}

@@ -113,7 +113,7 @@ - + {{ 'IMPRINT' | translate }} diff --git a/src/app/home/home.page.spec.ts b/src/app/home/home.page.spec.ts index a71e53920b4c1786100b842bb42ae0aae299571b..8d35cc90b298fbc0ed8349f63c13823d051be7a4 100644 --- a/src/app/home/home.page.spec.ts +++ b/src/app/home/home.page.spec.ts @@ -7,9 +7,12 @@ import {IonicStorageModule} from '@ionic/storage'; import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {APP_BASE_HREF} from '@angular/common'; +import Spy = jasmine.Spy; +import {ToastController} from '@ionic/angular'; +import MockMC from '../models/mockMC'; describe('HomePage', () => { - let component: HomePage; + let homePage: HomePage; let fixture: ComponentFixture; beforeEach(async(() => { @@ -23,19 +26,57 @@ describe('HomePage', () => { ], providers: [ {provide: APP_BASE_HREF, useValue: '/'}, + {provide: ToastController, useValue: MockMC.toastController} ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - .compileComponents(); + .compileComponents().then(); })); beforeEach(() => { fixture = TestBed.createComponent(HomePage); - component = fixture.componentInstance; + homePage = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(homePage).toBeTruthy(); + }); + + it('should change the language', (done) => { + const translateSpy: Spy = spyOn(homePage.corpusService, 'adjustTranslations').and.returnValue(Promise.resolve()); + spyOn(homePage.corpusService, 'processAnnisResponse'); + homePage.changeLanguage('').then(() => { + expect(translateSpy).toHaveBeenCalledTimes(1); + homePage.changeLanguage('').then(() => { + expect(translateSpy).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); + + it('should be initialized', () => { + const translateSpy: Spy = spyOn(homePage.helperService, 'loadTranslations'); + homePage.ionViewDidEnter(); + expect(translateSpy).toHaveBeenCalledTimes(1); + homePage.helperService.isIE11 = true; + homePage.ngOnInit(); + const tabs: HTMLElement = document.querySelector('#tabs') as HTMLElement; + expect(tabs.style.maxWidth).toBe('65%'); + }); + + it('should refresh the corpora', (done) => { + homePage.isCorpusUpdateInProgress = true; + const getCorporaSpy: Spy = spyOn(homePage.corpusService, 'getCorpora').and.returnValue(Promise.resolve()); + homePage.refreshCorpora().then(() => { + expect(homePage.isCorpusUpdateInProgress).toBe(false); + homePage.isCorpusUpdateInProgress = true; + getCorporaSpy.and.callFake(() => Promise.reject()); + homePage.refreshCorpora().then(() => { + }, () => { + expect(homePage.isCorpusUpdateInProgress).toBe(false); + done(); + }); + }); }); }); diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 6227decb0ba0caca425ad227e122932b2325fb4a..ccf958f4f6ea975c2740354a6aa5ba8d89f8e449 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -1,11 +1,12 @@ /* tslint:disable:no-string-literal */ -import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {HelperService} from 'src/app/helper.service'; import {NavController, ToastController} from '@ionic/angular'; import {HttpClient, HttpErrorResponse} from '@angular/common/http'; import {TranslateService} from '@ngx-translate/core'; import {ExerciseService} from 'src/app/exercise.service'; import {CorpusService} from 'src/app/corpus.service'; +import {take} from 'rxjs/operators'; @Component({ selector: 'app-home', @@ -13,7 +14,6 @@ import {CorpusService} from 'src/app/corpus.service'; styleUrls: ['home.page.scss'], }) export class HomePage implements OnInit { - HelperService = HelperService; public isCorpusUpdateInProgress = false; constructor(public navCtrl: NavController, @@ -22,49 +22,51 @@ export class HomePage implements OnInit { public translate: TranslateService, public corpusService: CorpusService, public toastCtrl: ToastController, + public helperService: HelperService, ) { } - changeLanguage(newLanguage: string) { - if (this.translate.currentLang !== newLanguage) { - this.translate.use(newLanguage).subscribe(() => { - HelperService.loadTranslations(this.translate); - this.corpusService.initPhenomenonMap(); - this.corpusService.processAnnisResponse(this.corpusService.annisResponse); - this.corpusService.adjustTranslations(); - }); - } + changeLanguage(newLanguage: string): Promise { + return new Promise(resolve => { + if (this.translate.currentLang !== newLanguage) { + this.translate.use(newLanguage).pipe(take(1)).subscribe(() => { + this.helperService.loadTranslations(this.translate); + this.corpusService.initPhenomenonMap(); + this.corpusService.processAnnisResponse(this.corpusService.annisResponse); + this.corpusService.adjustTranslations().then(); + return resolve(); + }); + } else { + return resolve(); + } + }); } - ionViewDidEnter() { - HelperService.loadTranslations(this.translate); + ionViewDidEnter(): void { + this.helperService.loadTranslations(this.translate); } - ngOnInit() { + ngOnInit(): void { // fix footer layout on IE11 - if (HelperService.isIE11) { - const tabs: HTMLElement = document.querySelector('#tabs') as HTMLElement; + if (this.helperService.isIE11) { + const tabs: HTMLIonTabsElement = document.querySelector('#tabs') as HTMLIonTabsElement; if (tabs) { tabs.style.maxWidth = '65%'; } } } - refreshCorpora() { - this.isCorpusUpdateInProgress = true; - this.corpusService.getCorpora(0).then(async () => { - this.isCorpusUpdateInProgress = false; - const toast = await this.toastCtrl.create({ - message: HelperService.corpusUpdateCompletedString, - duration: 3000, - position: 'top' + refreshCorpora(): Promise { + return new Promise((resolve, reject) => { + this.isCorpusUpdateInProgress = true; + this.corpusService.getCorpora(0).then(() => { + this.isCorpusUpdateInProgress = false; + this.helperService.showToast(this.toastCtrl, this.helperService.corpusUpdateCompletedString).then(); + return resolve(); + }, async (error: HttpErrorResponse) => { + this.isCorpusUpdateInProgress = false; + return reject(); }); - toast.present().then(); - }, async (error: HttpErrorResponse) => { - this.isCorpusUpdateInProgress = false; }); } - - test() { - } } diff --git a/src/app/imprint/imprint.page.html b/src/app/imprint/imprint.page.html index 7bd4ef0b8a999057ab661b4104aafe53b5fddd84..f036ec570cc47dbc16c9aefaef741f8a57010508 100644 --- a/src/app/imprint/imprint.page.html +++ b/src/app/imprint/imprint.page.html @@ -2,12 +2,12 @@
- + {{ 'IMPRINT' | translate }}
@@ -114,19 +114,19 @@ - + {{ 'ABOUT' | translate }} - + {{ 'DOC_SOFTWARE' | translate}} - + {{'DOC_EXERCISES' | translate}} - + {{'DOC_VOC_UNIT' | translate}} diff --git a/src/app/imprint/imprint.page.ts b/src/app/imprint/imprint.page.ts index 262fb03627311cf7a34a64bc0c29d7702e94965f..25a7257fa5d64676d082158159989d84a6d5f64b 100644 --- a/src/app/imprint/imprint.page.ts +++ b/src/app/imprint/imprint.page.ts @@ -5,24 +5,16 @@ import {HttpClient} from '@angular/common/http'; import {TranslateService} from '@ngx-translate/core'; @Component({ - selector: 'app-imprint', - templateUrl: './imprint.page.html', - styleUrls: ['./imprint.page.scss'], + selector: 'app-imprint', + templateUrl: './imprint.page.html', + styleUrls: ['./imprint.page.scss'], }) export class ImprintPage { - HelperService = HelperService; - - constructor(public navCtrl: NavController, - public http: HttpClient, - public translate: TranslateService) { } - - goToAuthorPage() { - this.navCtrl.navigateForward('/author').then(); - } - - goToTestPage() { - this.navCtrl.navigateForward('/test').then(); - } + constructor(public navCtrl: NavController, + public http: HttpClient, + public translate: TranslateService, + public helperService: HelperService) { + } } diff --git a/src/app/info/info.page.html b/src/app/info/info.page.html index 6badd47d8cde5455112b0356ae60932a56d2cbb2..fb4bfae77664fd0e0ff2b7a70d9d654f544bfc77 100644 --- a/src/app/info/info.page.html +++ b/src/app/info/info.page.html @@ -2,12 +2,12 @@
- + {{ 'ABOUT' | translate }}
@@ -77,19 +77,19 @@ - + {{ 'DOC_SOFTWARE' | translate}} - + {{'DOC_EXERCISES' | translate}} - + {{'DOC_VOC_UNIT' | translate}} - + {{ 'IMPRINT' | translate }} diff --git a/src/app/info/info.page.ts b/src/app/info/info.page.ts index 252316cadcf39037310843d87a0c4915f03ebef0..b65ef50eb0b0494aa07046a11360fd0a82181230 100644 --- a/src/app/info/info.page.ts +++ b/src/app/info/info.page.ts @@ -1,4 +1,4 @@ -import {Component, OnInit} from '@angular/core'; +import {Component} from '@angular/core'; import {HelperService} from 'src/app/helper.service'; import {NavController} from '@ionic/angular'; import {HttpClient} from '@angular/common/http'; @@ -11,11 +11,11 @@ import {TranslateService} from '@ngx-translate/core'; }) export class InfoPage { - HelperService = HelperService; studiesIndices: number[] = [...Array(4).keys()]; constructor(public navCtrl: NavController, public http: HttpClient, - public translate: TranslateService) { + public translate: TranslateService, + public helperService: HelperService) { } } diff --git a/src/app/kwic/kwic.page.html b/src/app/kwic/kwic.page.html index 6f5bf335926c769261e2128cc0bcb6b4703f5cc0..edf32b0e8aad98d02df06d6f9e51a91b9561615b 100644 --- a/src/app/kwic/kwic.page.html +++ b/src/app/kwic/kwic.page.html @@ -2,12 +2,12 @@
- + {{ 'KWIC' | translate }}
@@ -21,11 +21,10 @@ - -
+
diff --git a/src/app/kwic/kwic.page.ts b/src/app/kwic/kwic.page.ts index 8302914d2dccf9ae9be64cc88c3ff23085d9904c..08994dfa64fc778a963f3d3442a7240fe01ab0b8 100644 --- a/src/app/kwic/kwic.page.ts +++ b/src/app/kwic/kwic.page.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {NavController} from '@ionic/angular'; import {ExerciseService} from 'src/app/exercise.service'; import {HelperService} from '../helper.service'; @@ -8,16 +8,20 @@ import {HelperService} from '../helper.service'; templateUrl: './kwic.page.html', styleUrls: ['./kwic.page.scss'], }) -export class KwicPage { +export class KwicPage implements OnInit { - HelperService = HelperService; + public svgElementSelector = '#svg'; constructor(public navCtrl: NavController, - public exerciseService: ExerciseService) { - setTimeout(this.initVisualization.bind(this), 250); + public exerciseService: ExerciseService, + public helperService: HelperService) { } public initVisualization() { - document.getElementById('svg').innerHTML = this.exerciseService.kwicGraphs; + document.querySelector(this.svgElementSelector).innerHTML = this.exerciseService.kwicGraphs; + } + + ngOnInit(): void { + setTimeout(this.initVisualization.bind(this), 250); } } diff --git a/src/app/models/h5pEventDispatcherMock.ts b/src/app/models/h5pEventDispatcherMock.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed6ed50435727afb6d666422a51fa4636cb219c5 --- /dev/null +++ b/src/app/models/h5pEventDispatcherMock.ts @@ -0,0 +1,30 @@ +import {XAPIevent} from './xAPIevent'; +import Result from './xAPI/Result'; +import StatementBase from './xAPI/StatementBase'; +import Verb from './xAPI/Verb'; + +export default class H5PeventDispatcherMock { + listeners: { [eventName: string]: any[] } = {}; + + public on(eventName: string, callback: any) { + if (!this.listeners[eventName]) { + this.listeners[eventName] = []; + } + this.listeners[eventName].push(callback); + } + + public trigger(eventName: string, event: any) { + this.listeners[eventName].forEach(value => value(event)); + } + + public triggerXAPI(verb: string, result: Result) { + this.trigger('xAPI', new XAPIevent({ + data: { + statement: new StatementBase({ + result, + verb: new Verb({id: verb}) + }) + } + })); + }; +} diff --git a/src/app/models/mock.ts b/src/app/models/mock.ts deleted file mode 100644 index 630a9f9b6206b73d4355ecdc0df9c89f78de798c..0000000000000000000000000000000000000000 --- a/src/app/models/mock.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {CorpusMC} from './corpusMC'; -import {ExerciseMC} from './exerciseMC'; -import {MoodleExerciseType} from './enum'; - -export default class MockMC { - static apiResponseCorporaGet: object = { - corpora: [new CorpusMC({ - author: 'author', - source_urn: 'urn', - title: 'title', - })] - }; - static apiResponseExerciseListGet: ExerciseMC[] = [new ExerciseMC({ - eid: 'eid', - exercise_type: MoodleExerciseType.cloze.toString(), - exercise_type_translation: 'exercise_type_translation', - work_author: 'work_author', - work_title: 'work_title', - })]; -} diff --git a/src/app/models/mockMC.ts b/src/app/models/mockMC.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6596214b7379045b9c71aece4f1e3b0010147d6 --- /dev/null +++ b/src/app/models/mockMC.ts @@ -0,0 +1,61 @@ +import {CorpusMC} from './corpusMC'; +import {ExerciseMC} from './exerciseMC'; +import {MoodleExerciseType, PartOfSpeechValue, Phenomenon} from './enum'; +import {FrequencyItem} from './frequencyItem'; +import {ApplicationState} from './applicationState'; +import {TextData} from './textData'; +import {AnnisResponse} from './annisResponse'; +import {NodeMC} from './nodeMC'; +import {TestResultMC} from './testResultMC'; +import StatementBase from './xAPI/StatementBase'; +import Result from './xAPI/Result'; +import Score from './xAPI/Score'; + +export default class MockMC { + static apiResponseCorporaGet: object = { + corpora: [new CorpusMC({ + author: 'author', + source_urn: 'urn', + title: 'title', + })] + }; + static apiResponseExerciseListGet: ExerciseMC[] = [new ExerciseMC({ + eid: 'eid', + exercise_type: MoodleExerciseType.cloze.toString(), + exercise_type_translation: 'exercise_type_translation', + work_author: 'work_author', + work_title: 'work_title', + })]; + static apiResponseFrequencyAnalysisGet: FrequencyItem[] = [new FrequencyItem({ + phenomena: [Phenomenon.partOfSpeech.toString()], + values: [PartOfSpeechValue.adjective.toString()] + })]; + static apiResponseTextGet: AnnisResponse = new AnnisResponse({ + nodes: [new NodeMC({udep_lemma: 'lemma', annis_tok: 'tok'})], + links: [] + }); + static applicationState: ApplicationState = new ApplicationState({ + currentSetup: new TextData({currentCorpus: new CorpusMC()}), + mostRecentSetup: new TextData({annisResponse: new AnnisResponse({nodes: [new NodeMC()], links: []})}), + exerciseList: [new ExerciseMC()] + }); + static popoverController: any = {create: () => Promise.resolve({present: () => Promise.resolve()})}; + static testResults: { [exerciseIndex: number]: TestResultMC } = { + 20: new TestResultMC({ + statement: new StatementBase({result: new Result({score: new Score({scaled: 0, raw: 0})})}) + }) + }; + static toastController: any = {create: () => Promise.resolve({present: () => Promise.resolve()})}; + + static addIframe(h5pIframeString: string, buttonClass: string = null): HTMLIFrameElement { + const iframe: HTMLIFrameElement = document.createElement('iframe'); + iframe.setAttribute('id', h5pIframeString.slice(1)); + document.body.appendChild(iframe); + if (buttonClass) { + const button: HTMLButtonElement = iframe.contentWindow.document.createElement('button'); + button.classList.add(buttonClass.slice(1)); + iframe.contentWindow.document.body.appendChild(button); + } + return document.querySelector(h5pIframeString); + } +} diff --git a/src/app/models/xAPI/Activity.ts b/src/app/models/xAPI/Activity.ts index afe4be474dd7c3970b6ee1e9a33b39e304a0703e..27800ef6f62c094bea71ffd4891d6c65fbf9b60f 100644 --- a/src/app/models/xAPI/Activity.ts +++ b/src/app/models/xAPI/Activity.ts @@ -1,9 +1,13 @@ import Definition from './Definition'; -interface Activity { - objectType: 'Activity'; - id: string; - definition?: Definition; +class Activity { + objectType: 'Activity'; + id: string; + definition?: Definition; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default Activity; diff --git a/src/app/models/xAPI/Context.ts b/src/app/models/xAPI/Context.ts index 49dfff368e1c4ecb9017c0f80a97f107b7bbae4c..8b647288bb8d579567dfb0de6930fa189a03e054 100644 --- a/src/app/models/xAPI/Context.ts +++ b/src/app/models/xAPI/Context.ts @@ -3,12 +3,16 @@ import Extensions from './Extensions'; import Group from './Group'; import Agent from './Agent'; -interface Context { - contextActivities?: ContextActivities; - team?: Group; - instructor?: Agent; - registration?: string; - extensions?: Extensions; +class Context { + contextActivities?: ContextActivities; + team?: Group; + instructor?: Agent; + registration?: string; + extensions?: Extensions; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default Context; diff --git a/src/app/models/xAPI/ContextActivities.ts b/src/app/models/xAPI/ContextActivities.ts index a5a26ebf30a5cce64e1d3154fc52bd2cee17d918..020992d8f51df9a9a82f575e08f8f192d5f01998 100644 --- a/src/app/models/xAPI/ContextActivities.ts +++ b/src/app/models/xAPI/ContextActivities.ts @@ -1,10 +1,14 @@ import Activity from './Activity'; -interface ContextActivities { +class ContextActivities { parent?: Activity[]; grouping?: Activity[]; category?: Activity[]; other?: Activity[]; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default ContextActivities; diff --git a/src/app/models/xAPI/Definition.ts b/src/app/models/xAPI/Definition.ts index 67ea87774e6d6279be19c61bc5c3590625b976ac..f59abc2dbdb49e40a5a5042171c8ba578c75e79d 100644 --- a/src/app/models/xAPI/Definition.ts +++ b/src/app/models/xAPI/Definition.ts @@ -1,7 +1,7 @@ import Extensions from './Extensions'; import LanguageMap from './LanguageMap'; -interface Definition { +class Definition { readonly name?: LanguageMap; readonly description?: LanguageMap; readonly extensions?: Extensions; @@ -10,6 +10,10 @@ interface Definition { readonly choices?: { description: LanguageMap, id: string }[]; readonly correctResponsesPattern?: string[]; readonly interactionType?: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default Definition; diff --git a/src/app/models/xAPI/Result.ts b/src/app/models/xAPI/Result.ts index 2d4524ff79beef6b3b4e5331f2a24c6d03113d00..242663b2a3e7bfe45bfa5bd6902e0b3d53f6781a 100644 --- a/src/app/models/xAPI/Result.ts +++ b/src/app/models/xAPI/Result.ts @@ -1,11 +1,15 @@ import Extensions from './Extensions'; -import Score from "src/app/models/xAPI/Score"; +import Score from 'src/app/models/xAPI/Score'; -interface Result { - duration?: string; - extensions?: Extensions; - response?: string; - score?: Score; +class Result { + duration?: string; + extensions?: Extensions; + response?: string; + score?: Score; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default Result; diff --git a/src/app/models/xAPI/Score.ts b/src/app/models/xAPI/Score.ts index 8b0c45c7690bc9515eb863a63d9b1a5237ea71c9..820363b5c9eb1cc008ea700b6d0809c0f494fa11 100644 --- a/src/app/models/xAPI/Score.ts +++ b/src/app/models/xAPI/Score.ts @@ -1,8 +1,12 @@ -interface Score { +class Score { max: number; min: number; raw: number; scaled: number; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default Score; diff --git a/src/app/models/xAPI/StatementBase.ts b/src/app/models/xAPI/StatementBase.ts index 5fb8a2e3cd876bbf69db1df67a5a08ef11c2076a..94f2dd1a77bd86d6c8bdc6eea729a0f72aaf7425 100644 --- a/src/app/models/xAPI/StatementBase.ts +++ b/src/app/models/xAPI/StatementBase.ts @@ -5,13 +5,17 @@ import Context from './Context'; import Result from './Result'; import StatementObject from './StatementObject'; -interface StatementBase { - actor: Actor; - object: StatementObject; - verb: Verb; - context?: Context; - result?: Result; - attachments?: Attachment[]; +class StatementBase { + actor: Actor; + object: StatementObject; + verb: Verb; + context?: Context; + result?: Result; + attachments?: Attachment[]; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default StatementBase; diff --git a/src/app/models/xAPI/Verb.ts b/src/app/models/xAPI/Verb.ts index bdfa15712db051f02a39f846deadf224110de3a7..28225fe3358f6080a090dffd597da7608dd5983c 100644 --- a/src/app/models/xAPI/Verb.ts +++ b/src/app/models/xAPI/Verb.ts @@ -1,8 +1,12 @@ import LanguageMap from './LanguageMap'; -interface Verb { - id: string; - display?: LanguageMap; +class Verb { + id: string; + display?: LanguageMap; + + constructor(init?: Partial) { + Object.assign(this, init); + } } export default Verb; diff --git a/src/app/preview/preview.page.html b/src/app/preview/preview.page.html index 46a7d42f4fe041724c3c6392179158e6b994147f..57bc606246fbe30f1fc33e34854d17e9ea607e1e 100644 --- a/src/app/preview/preview.page.html +++ b/src/app/preview/preview.page.html @@ -2,12 +2,12 @@
- + {{ 'PREVIEW' | translate }}
@@ -24,7 +24,7 @@
- + - + {{ "CHANGE_TEXT_RANGE" | translate}} - + {{ "SHARE" | translate}} @@ -80,7 +80,7 @@ beginning that it is going to be a download (instead of an ordinary link or clic
diff --git a/src/app/preview/preview.page.spec.ts b/src/app/preview/preview.page.spec.ts index 62f6aa50736dc7c4e1620746c21e764a0eeec9b4..97c27078daa8b565e4c01f8d47d66a89e76ec8d0 100644 --- a/src/app/preview/preview.page.spec.ts +++ b/src/app/preview/preview.page.spec.ts @@ -8,10 +8,27 @@ import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {FormsModule} from '@angular/forms'; import {APP_BASE_HREF} from '@angular/common'; +import {CorpusService} from '../corpus.service'; +import {ToastController} from '@ionic/angular'; +import MockMC from '../models/mockMC'; +import {AnnisResponse} from '../models/annisResponse'; +import {Solution} from '../models/solution'; +import {ExerciseType} from '../models/enum'; +import {SolutionElement} from '../models/solutionElement'; +import Spy = jasmine.Spy; +import {NodeMC} from '../models/nodeMC'; +import {TestResultMC} from '../models/testResultMC'; +import H5PeventDispatcherMock from '../models/h5pEventDispatcherMock'; +import Result from '../models/xAPI/Result'; +import configMC from '../../configMC'; + +declare var H5P: any; describe('PreviewPage', () => { - let component: PreviewPage; + let previewPage: PreviewPage; let fixture: ComponentFixture; + let corpusService: CorpusService; + let checkAnnisResponseSpy: Spy; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -25,19 +42,136 @@ describe('PreviewPage', () => { ], providers: [ {provide: APP_BASE_HREF, useValue: '/'}, + {provide: ToastController, useValue: MockMC.toastController} ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - .compileComponents(); + .compileComponents().then(); + corpusService = TestBed.inject(CorpusService); + fixture = TestBed.createComponent(PreviewPage); + previewPage = fixture.componentInstance; + checkAnnisResponseSpy = spyOn(corpusService, 'checkAnnisResponse').and.callFake(() => Promise.reject()); + fixture.detectChanges(); })); beforeEach(() => { - fixture = TestBed.createComponent(PreviewPage); - component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(previewPage).toBeTruthy(); + }); + + it('should copy the link', () => { + previewPage.helperService.isVocabularyCheck = true; + previewPage.corpusService.annisResponse = new AnnisResponse({solutions: []}); + fixture.detectChanges(); + const button: HTMLIonButtonElement = document.querySelector('#showShareLinkButton'); + button.click(); + fixture.detectChanges(); + previewPage.copyLink(); + const input: HTMLInputElement = document.querySelector(previewPage.inputSelector); + expect(input.selectionStart).toBe(0); + expect(input.selectionEnd).toBe(0); + }); + + it('should initialize H5P', () => { + spyOn(previewPage.exerciseService, 'initH5P').and.returnValue(Promise.resolve()); + previewPage.corpusService.annisResponse = new AnnisResponse({exercise_id: '', solutions: [new Solution()]}); + previewPage.currentSolutions = previewPage.corpusService.annisResponse.solutions; + previewPage.initH5P(); + expect(previewPage.solutionIndicesString.length).toBe(0); + previewPage.exerciseService.excludeOOV = true; + previewPage.corpusService.exercise.type = ExerciseType.markWords; + previewPage.initH5P(); + expect(previewPage.solutionIndicesString.length).toBe(21); + }); + + it('should be initialized', (done) => { + const body: HTMLBodyElement = document.querySelector('body'); + let iframe: HTMLIFrameElement = document.createElement('iframe'); + iframe.setAttribute('id', previewPage.exerciseService.h5pIframeString.slice(1)); + body.appendChild(iframe); + spyOn(previewPage, 'sendData').and.returnValue(Promise.resolve()); + previewPage.ngOnDestroy(); + const oldDispatcher: any = previewPage.helperService.deepCopy(H5P.externalDispatcher); + const newDispatcher: H5PeventDispatcherMock = new H5PeventDispatcherMock(); + H5P.externalDispatcher = newDispatcher; + previewPage.ngOnInit().then(() => { + newDispatcher.triggerXAPI(configMC.xAPIverbIDanswered, new Result()); + checkAnnisResponseSpy.and.returnValue(Promise.resolve()); + spyOn(previewPage, 'initH5P'); + spyOn(previewPage, 'processAnnisResponse'); + previewPage.currentSolutions = [new Solution()]; + previewPage.ngOnInit().then(() => { + expect(previewPage.currentSolutions.length).toBe(0); + iframe = document.querySelector(previewPage.exerciseService.h5pIframeString); + iframe.parentNode.removeChild(iframe); + H5P.externalDispatcher = oldDispatcher; + done(); + }); + }); + }); + + it('should process an ANNIS response', () => { + previewPage.corpusService.annisResponse = new AnnisResponse({}); + const ar: AnnisResponse = new AnnisResponse({ + solutions: [new Solution({target: new SolutionElement({content: 'content'})})] + }); + previewPage.processAnnisResponse(ar); + expect(previewPage.corpusService.annisResponse.solutions.length).toBe(1); + previewPage.corpusService.currentUrn = 'urn:'; + previewPage.processAnnisResponse(ar); + expect(previewPage.corpusService.annisResponse.nodes).toEqual(ar.nodes); + }); + + it('should process solutions', () => { + const solutions: Solution[] = [ + new Solution({ + target: new SolutionElement({content: 'content2', salt_id: 'id'}), + value: new SolutionElement({salt_id: 'id'}) + }), + new Solution({ + target: new SolutionElement({content: 'content1', salt_id: 'id'}), + value: new SolutionElement({salt_id: 'id'}) + }), + new Solution({ + target: new SolutionElement({content: 'content1', salt_id: 'id'}), + value: new SolutionElement({salt_id: 'id'}) + }), + new Solution({ + target: new SolutionElement({content: 'content3', salt_id: 'id'}), + value: new SolutionElement({salt_id: 'id'}) + })]; + previewPage.corpusService.exercise.type = ExerciseType.markWords; + previewPage.exerciseService.excludeOOV = true; + previewPage.corpusService.annisResponse = new AnnisResponse({ + nodes: [new NodeMC({is_oov: false, id: 'id'})], + solutions + }); + previewPage.processSolutions(solutions); + expect(previewPage.currentSolutions[2]).toBe(solutions[0]); + }); + + it('should send data', (done) => { + const requestSpy: Spy = spyOn(previewPage.helperService, 'makePostRequest').and.returnValue(Promise.resolve()); + const consoleSpy: Spy = spyOn(console, 'log'); + previewPage.sendData(new TestResultMC()).then(() => { + expect(consoleSpy).toHaveBeenCalledTimes(0); + requestSpy.and.callFake(() => Promise.reject()); + previewPage.sendData(new TestResultMC()).then(() => { + }, () => { + expect(consoleSpy).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); + + it('should switch OOV', () => { + previewPage.currentSolutions = [new Solution()]; + previewPage.corpusService.annisResponse = new AnnisResponse(); + spyOn(previewPage, 'processSolutions'); + spyOn(previewPage, 'initH5P'); + previewPage.switchOOV(); + expect(previewPage.currentSolutions.length).toBe(0); }); }); diff --git a/src/app/preview/preview.page.ts b/src/app/preview/preview.page.ts index d0beb9acfa2bd420b3662f4622ce9315bc907208..940bc438d9166ee2e5aa08728d2abea41f37cbd4 100644 --- a/src/app/preview/preview.page.ts +++ b/src/app/preview/preview.page.ts @@ -8,7 +8,7 @@ import {CorpusService} from 'src/app/corpus.service'; import {Component, OnDestroy, OnInit} from '@angular/core'; import {TranslateService} from '@ngx-translate/core'; import {Solution} from 'src/app/models/solution'; -import {HttpClient, HttpErrorResponse} from '@angular/common/http'; +import {HttpClient} from '@angular/common/http'; import {XAPIevent} from 'src/app/models/xAPIevent'; import {TestResultMC} from 'src/app/models/testResultMC'; import configMC from '../../configMC'; @@ -26,7 +26,6 @@ export class PreviewPage implements OnDestroy, OnInit { public ExerciseType = ExerciseType; public FileType = FileType; public currentSolutions: Solution[]; - public HelperService = HelperService; public inputSelector = 'input[type="text"]'; public maxGapLength = 0; public showShareLink = false; @@ -43,15 +42,6 @@ export class PreviewPage implements OnDestroy, OnInit { public helperService: HelperService, public toastCtrl: ToastController, public storage: Storage) { - this.currentSolutions = []; - if (!HelperService.isVocabularyCheck) { - this.exerciseService.excludeOOV = false; - } - this.corpusService.checkAnnisResponse().then(() => { - this.processAnnisResponse(this.corpusService.annisResponse); - this.initH5P(); - }, () => { - }); } async copyLink(): Promise { @@ -59,12 +49,7 @@ export class PreviewPage implements OnDestroy, OnInit { input.select(); document.execCommand('copy'); input.setSelectionRange(0, 0); - const toast = await this.toastCtrl.create({ - message: this.corpusService.shareLinkCopiedString, - duration: 3000, - position: 'middle' - }); - toast.present().then(); + this.helperService.showToast(this.toastCtrl, this.corpusService.shareLinkCopiedString, 'middle').then(); } initH5P(): void { @@ -76,7 +61,7 @@ export class PreviewPage implements OnDestroy, OnInit { // this has to be LocalStorage because the H5P javascript cannot easily access the Ionic Storage window.localStorage.setItem(configMC.localStorageKeyH5P, url); const exerciseTypePath: string = this.corpusService.exercise.type === ExerciseType.markWords ? 'mark_words' : 'drag_text'; - this.exerciseService.initH5P(exerciseTypePath); + this.exerciseService.initH5P(exerciseTypePath).then(); this.updateFileUrl(); } @@ -84,21 +69,34 @@ export class PreviewPage implements OnDestroy, OnInit { H5P.externalDispatcher.off('xAPI'); } - ngOnInit(): void { - H5P.externalDispatcher.on('xAPI', (event: XAPIevent) => { - // results are only available when a task has been completed/answered, not in the "attempted" or "interacted" stages - if (event.data.statement.verb.id === configMC.xAPIverbIDanswered && event.data.statement.result) { - const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); - if (iframe) { - const iframeDoc: Document = iframe.contentWindow.document; - const inner: string = iframeDoc.documentElement.innerHTML; - const result: TestResultMC = new TestResultMC({ - statement: event.data.statement, - innerHTML: inner - }); - this.sendData(result); - } + ngOnInit(): Promise { + return new Promise((resolve) => { + this.currentSolutions = []; + if (!this.helperService.isVocabularyCheck) { + this.exerciseService.excludeOOV = false; } + H5P.externalDispatcher.on('xAPI', (event: XAPIevent) => { + // results are only available when a task has been completed/answered, not in the "attempted" or "interacted" stages + if (event.data.statement.verb.id === configMC.xAPIverbIDanswered && event.data.statement.result) { + const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); + if (iframe) { + const iframeDoc: Document = iframe.contentWindow.document; + const inner: string = iframeDoc.documentElement.innerHTML; + const result: TestResultMC = new TestResultMC({ + statement: event.data.statement, + innerHTML: inner + }); + this.sendData(result).then(); + } + } + }); + this.corpusService.checkAnnisResponse().then(() => { + this.processAnnisResponse(this.corpusService.annisResponse); + this.initH5P(); + return resolve(); + }, () => { + return resolve(); + }); }); } @@ -137,18 +135,17 @@ export class PreviewPage implements OnDestroy, OnInit { ta.select(); } - sendData(result: TestResultMC): void { - const fileUrl: string = configMC.backendBaseUrl + configMC.backendApiFilePath; - HelperService.currentError = null; - HelperService.isLoading = true; - const formData = new FormData(); - formData.append('learning_result', JSON.stringify(result.statement)); - this.http.post(fileUrl, formData).subscribe(async () => { - HelperService.isLoading = false; - }, async (error: HttpErrorResponse) => { - HelperService.isLoading = false; - HelperService.currentError = error; - console.log('ERROR: COULD NOT SEND EXERCISE RESULTS TO SERVER.'); + sendData(result: TestResultMC): Promise { + return new Promise((resolve, reject) => { + const fileUrl: string = configMC.backendBaseUrl + configMC.backendApiFilePath; + const formData = new FormData(); + formData.append('learning_result', JSON.stringify(result.statement)); + this.helperService.makePostRequest(this.http, this.toastCtrl, fileUrl, formData, '').then(() => { + return resolve(); + }, () => { + console.log('ERROR: COULD NOT SEND EXERCISE RESULTS TO SERVER.'); + return reject(); + }); }); } diff --git a/src/app/ranking/ranking.page.html b/src/app/ranking/ranking.page.html index f84e8cd61b1cb8c21e6067e9719101317c6b6614..e6b6de79d8e1c2f096d396a6cbf4eb3f6be0b448 100644 --- a/src/app/ranking/ranking.page.html +++ b/src/app/ranking/ranking.page.html @@ -2,12 +2,12 @@
- + {{ 'VOCABULARY_RANKING' | translate }}
diff --git a/src/app/ranking/ranking.page.spec.ts b/src/app/ranking/ranking.page.spec.ts index dac8fbe8ff40547d60cbe4dae57ea0a896b004e5..d13b82bc5a6abb5da7ce461d396fb1ff52b30c65 100644 --- a/src/app/ranking/ranking.page.spec.ts +++ b/src/app/ranking/ranking.page.spec.ts @@ -7,10 +7,16 @@ import {IonicStorageModule} from '@ionic/storage'; import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {APP_BASE_HREF} from '@angular/common'; +import {CorpusService} from '../corpus.service'; +import {Sentence} from '../models/sentence'; +import Spy = jasmine.Spy; +import {AnnisResponse} from '../models/annisResponse'; +import {NodeMC} from '../models/nodeMC'; describe('RankingPage', () => { - let component: RankingPage; + let rankingPage: RankingPage; let fixture: ComponentFixture; + let corpusService: CorpusService; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -26,16 +32,35 @@ describe('RankingPage', () => { ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - .compileComponents(); + .compileComponents().then(); + corpusService = TestBed.inject(CorpusService); })); beforeEach(() => { fixture = TestBed.createComponent(RankingPage); - component = fixture.componentInstance; + rankingPage = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(rankingPage).toBeTruthy(); + }); + + it('should show the text', (done) => { + rankingPage.helperService.isVocabularyCheck = false; + const vocCheckSpy: Spy = spyOn(rankingPage.vocService, 'getVocabularyCheck').and.returnValue(Promise.resolve( + new AnnisResponse({nodes: [new NodeMC({id: 'id/id:1-2/id'})]}))); + spyOn(rankingPage.corpusService, 'processAnnisResponse'); + spyOn(rankingPage.helperService, 'goToShowTextPage').and.returnValue(Promise.resolve(true)); + rankingPage.showText([new Sentence({id: 1})]).then(() => { + expect(rankingPage.helperService.isVocabularyCheck).toBe(true); + vocCheckSpy.and.callFake(() => Promise.reject()); + rankingPage.helperService.isVocabularyCheck = false; + rankingPage.showText([new Sentence({id: 1})]).then(() => { + }, () => { + expect(rankingPage.helperService.isVocabularyCheck).toBe(false); + done(); + }); + }); }); }); diff --git a/src/app/ranking/ranking.page.ts b/src/app/ranking/ranking.page.ts index fcd065286ef09108c88fbec097517b14382eb182..331a3e5afcf99a5b1c29e3d6b4d2094e686e95b3 100644 --- a/src/app/ranking/ranking.page.ts +++ b/src/app/ranking/ranking.page.ts @@ -14,28 +14,32 @@ import {Sentence} from 'src/app/models/sentence'; styleUrls: ['./ranking.page.scss'], }) export class RankingPage { - HelperService = HelperService; Math = Math; constructor(public navCtrl: NavController, public corpusService: CorpusService, public vocService: VocabularyService, public exerciseService: ExerciseService, - public toastCtrl: ToastController) { + public toastCtrl: ToastController, + public helperService: HelperService) { // remove old sentence boundaries this.corpusService.baseUrn = this.corpusService.currentUrn.split('@')[0]; } - showText(rank: Sentence[]) { - this.corpusService.currentUrn = this.corpusService.baseUrn + `@${rank[0].id}-${rank[rank.length - 1].id}`; - this.vocService.getVocabularyCheck(this.corpusService.currentUrn, true).then((ar: AnnisResponse) => { - const urnStart: string = ar.nodes[0].id.split('/')[1]; - const urnEnd: string = ar.nodes.slice(-1)[0].id.split('/')[1]; - this.corpusService.currentUrn = urnStart.concat('-', urnEnd.split(':').slice(-1)[0]); - this.corpusService.processAnnisResponse(ar); - HelperService.isVocabularyCheck = true; - HelperService.goToShowTextPage(this.navCtrl, true).then(); - }, async (error: HttpErrorResponse) => { + showText(rank: Sentence[]): Promise { + return new Promise((resolve, reject) => { + this.corpusService.currentUrn = this.corpusService.baseUrn + `@${rank[0].id}-${rank[rank.length - 1].id}`; + this.vocService.getVocabularyCheck(this.corpusService.currentUrn, true).then((ar: AnnisResponse) => { + const urnStart: string = ar.nodes[0].id.split('/')[1]; + const urnEnd: string = ar.nodes.slice(-1)[0].id.split('/')[1]; + this.corpusService.currentUrn = urnStart.concat('-', urnEnd.split(':').slice(-1)[0]); + this.corpusService.processAnnisResponse(ar); + this.helperService.isVocabularyCheck = true; + this.helperService.goToShowTextPage(this.navCtrl, true).then(); + return resolve(); + }, async (error: HttpErrorResponse) => { + return reject(); + }); }); } diff --git a/src/app/semantics/semantics-routing.module.ts b/src/app/semantics/semantics-routing.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..d575b0020671fa65ae24cab1df01d0e9e3beb8e8 --- /dev/null +++ b/src/app/semantics/semantics-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { SemanticsPage } from './semantics.page'; + +const routes: Routes = [ + { + path: '', + component: SemanticsPage + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class SemanticsPageRoutingModule {} diff --git a/src/app/semantics/semantics.module.ts b/src/app/semantics/semantics.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a22a273e47d0f0930b08c7e217866094712bdb1 --- /dev/null +++ b/src/app/semantics/semantics.module.ts @@ -0,0 +1,23 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; + +import {IonicModule} from '@ionic/angular'; + +import {SemanticsPageRoutingModule} from './semantics-routing.module'; + +import {SemanticsPage} from './semantics.page'; +import {TranslateModule} from '@ngx-translate/core'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + SemanticsPageRoutingModule, + TranslateModule.forChild(), + ], + declarations: [SemanticsPage] +}) +export class SemanticsPageModule { +} diff --git a/src/app/semantics/semantics.page.html b/src/app/semantics/semantics.page.html new file mode 100644 index 0000000000000000000000000000000000000000..c5bdebfa70b263d04738c50787ccee561e81cfd0 --- /dev/null +++ b/src/app/semantics/semantics.page.html @@ -0,0 +1,74 @@ + + + + +
+ {{ 'SEMANTICS' | translate }} + +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + {{ 'APPLY' | translate }} + + + + + +
+
+
+ + +

{{part}}

+
+
+ + + {{ 'BACK' | translate }} + + +
+
diff --git a/src/app/semantics/semantics.page.scss b/src/app/semantics/semantics.page.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/semantics/semantics.page.spec.ts b/src/app/semantics/semantics.page.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f5cb4a663babbfccf8e031083fac48cae798094 --- /dev/null +++ b/src/app/semantics/semantics.page.spec.ts @@ -0,0 +1,74 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {IonicModule} from '@ionic/angular'; + +import {SemanticsPage} from './semantics.page'; +import {RouterModule} from '@angular/router'; +import {HttpClientModule, HttpErrorResponse} from '@angular/common/http'; +import {IonicStorageModule} from '@ionic/storage'; +import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; +import {FormsModule} from '@angular/forms'; +import {of} from 'rxjs'; +import {APP_BASE_HREF} from '@angular/common'; +import Spy = jasmine.Spy; + +describe('SemanticsPage', () => { + let semanticsPage: SemanticsPage; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SemanticsPage], + imports: [ + FormsModule, + HttpClientModule, + IonicStorageModule.forRoot(), + RouterModule.forRoot([]), + TranslateTestingModule, + ], + providers: [ + {provide: APP_BASE_HREF, useValue: '/'}, + ], + }).compileComponents().then(); + fixture = TestBed.createComponent(SemanticsPage); + semanticsPage = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(semanticsPage).toBeTruthy(); + }); + + it('should be initialized', (done) => { + spyOn(semanticsPage, 'updateVectorNetwork'); + semanticsPage.activatedRoute.queryParams = of({minCount: '0'}); + semanticsPage.ngOnInit().then(() => { + expect(semanticsPage.minCount).toBe(0); + semanticsPage.activatedRoute.queryParams = of({ + searchRegex: 'a', + highlightRegex: 'b', + nearestNeighborCount: '0' + }); + semanticsPage.ngOnInit().then(() => { + expect(semanticsPage.nearestNeighborCount).toBe(0); + done(); + }); + }); + }); + + it('should update the vector network', (done) => { + const requestSpy: Spy = spyOn(semanticsPage.helperService, 'makeGetRequest').and.returnValue(Promise.resolve('a')); + const toastSpy: Spy = spyOn(semanticsPage.helperService, 'showToast').and.returnValue(Promise.resolve()); + semanticsPage.updateVectorNetwork().then(() => { + expect(toastSpy).toHaveBeenCalledTimes(1); + semanticsPage.searchRegex = 'a'; + semanticsPage.updateVectorNetwork().then(() => { + expect(semanticsPage.kwicGraphs).toBeTruthy(); + requestSpy.and.callFake(() => Promise.reject(new HttpErrorResponse({status: 422}))); + semanticsPage.updateVectorNetwork().then(() => { + expect(toastSpy).toHaveBeenCalledTimes(2); + done(); + }); + }); + }); + }); +}); diff --git a/src/app/semantics/semantics.page.ts b/src/app/semantics/semantics.page.ts new file mode 100644 index 0000000000000000000000000000000000000000..1463c8d998d15277f134501e83dc18bf44d9bcf5 --- /dev/null +++ b/src/app/semantics/semantics.page.ts @@ -0,0 +1,85 @@ +import {Component, OnInit} from '@angular/core'; +import {NavController, ToastController} from '@ionic/angular'; +import {HelperService} from '../helper.service'; +import configMC from '../../configMC'; +import {HttpClient, HttpErrorResponse, HttpParams} from '@angular/common/http'; +import {ActivatedRoute} from '@angular/router'; +import {CorpusService} from '../corpus.service'; + +@Component({ + selector: 'app-semantics', + templateUrl: './semantics.page.html', + styleUrls: ['./semantics.page.scss'], +}) +export class SemanticsPage implements OnInit { + public highlightRegex = ''; + public kwicGraphs: string; + public metadata: string[] = ('XII panegyrici Latini\n' + + 'Baehrens, Emil\n' + + 'Lipsiae | 1874 | Teubner\n' + + 'Augsburg, Staats- und Stadtbibliothek -- LR 759\n' + + 'URL: https://reader.digitale-sammlungen.de/de/fs1/object/display/bsb11265534_00133.html\n' + + 'Permalink: http://mdz-nbn-resolving.de/urn:nbn:de:bvb:12-bsb11265534-1').split('\n'); + public minCount = 1; + public nearestNeighborCount = 1; + public searchRegex = ''; + public svgElementSelector = '#svg'; + + constructor(public navCtrl: NavController, + public helperService: HelperService, + public http: HttpClient, + public toastCtrl: ToastController, + public activatedRoute: ActivatedRoute, + public corpusService: CorpusService) { + } + + ngOnInit(): Promise { + return new Promise(resolve => { + this.activatedRoute.queryParams.subscribe((params: any) => { + if (Object.keys(params).length) { + const paramSearchRegex: string = params.searchRegex; + this.searchRegex = paramSearchRegex ? paramSearchRegex : this.searchRegex; + const paramMinCount: string = params.minCount; + this.minCount = paramMinCount ? +paramMinCount : this.minCount; + const paramNearestNeighborCount: string = params.nearestNeighborCount; + this.nearestNeighborCount = paramNearestNeighborCount ? +paramNearestNeighborCount : this.nearestNeighborCount; + const paramHighlightRegex: string = params.highlightRegex; + this.highlightRegex = paramHighlightRegex ? paramHighlightRegex : this.highlightRegex; + // dirty hack to get the loading spinner displayed correctly + setTimeout(this.updateVectorNetwork.bind(this), 500); + } + return resolve(); + }); + }); + } + + updateVectorNetwork(): Promise { + const retVal: Promise = new Promise((resolve, reject) => { + if (!this.searchRegex) { + this.helperService.showToast(this.toastCtrl, this.corpusService.searchRegexMissingString).then(); + return reject(); + } + let params: HttpParams = new HttpParams().set('search_regex', this.searchRegex); + params = params.set('min_count', (Math.max(Math.round(this.minCount), 1)).toString()); + params = params.set('highlight_regex', this.highlightRegex); + params = params.set('nearest_neighbor_count', (Math.max(Math.round(this.nearestNeighborCount), 1)).toString()); + const kwicUrl: string = configMC.backendBaseUrl + configMC.backendApiVectorNetworkPath; + const svgElement: SVGElement = document.querySelector(this.svgElementSelector); + svgElement.innerHTML = ''; + this.helperService.makeGetRequest(this.http, this.toastCtrl, kwicUrl, params).then((svgString: string) => { + this.kwicGraphs = svgString; + svgElement.innerHTML = this.kwicGraphs; + return resolve(); + }, (error: HttpErrorResponse) => { + if (error.status === 422) { + this.helperService.showToast(this.toastCtrl, this.corpusService.tooManyHitsString).then(); + } + return reject(); + }); + }); + // dirty hack to prevent unhandled promise rejection in click events + return retVal.catch(() => { + }); + } + +} diff --git a/src/app/show-text/show-text.page.html b/src/app/show-text/show-text.page.html index c27b91059db3e0120f673746ea2678486b9cee74..a439707ee16c7693b37a76969628606d018ef356 100644 --- a/src/app/show-text/show-text.page.html +++ b/src/app/show-text/show-text.page.html @@ -2,12 +2,12 @@ - + {{cc.title}} {{corpusService.currentUrn?.split(":")[corpusService.currentUrn?.split(":").length - 1]}} @@ -26,7 +26,7 @@ {{'SHOW_TEXT_TITLE' | translate}} - + - + {{ 'VOCABULARY_CHECK' | translate }} - + {{ "EXERCISE_SET_PARAMETERS" | translate}} diff --git a/src/app/show-text/show-text.page.spec.ts b/src/app/show-text/show-text.page.spec.ts index a4001b6472183ddb00acaa5b24efdd6c876d216e..590cbcf68331333222e1d9a38fd206c71532310c 100644 --- a/src/app/show-text/show-text.page.spec.ts +++ b/src/app/show-text/show-text.page.spec.ts @@ -8,9 +8,13 @@ import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; import {FormsModule} from '@angular/forms'; import {APP_BASE_HREF} from '@angular/common'; +import {AnnisResponse} from '../models/annisResponse'; +import {NodeMC} from '../models/nodeMC'; +import {VocabularyCorpus} from '../models/enum'; +import Spy = jasmine.Spy; describe('ShowTextPage', () => { - let component: ShowTextPage; + let showTextPage: ShowTextPage; let fixture: ComponentFixture; beforeEach(async(() => { @@ -28,16 +32,55 @@ describe('ShowTextPage', () => { ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - .compileComponents(); + .compileComponents().then(); })); beforeEach(() => { fixture = TestBed.createComponent(ShowTextPage); - component = fixture.componentInstance; + showTextPage = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(showTextPage).toBeTruthy(); + }); + + it('should generate a download link', (done) => { + showTextPage.corpusService.currentText = 'text'; + fixture.detectChanges(); + const requestSpy: Spy = spyOn(showTextPage.helperService, 'makePostRequest').and.returnValue(Promise.resolve('a.b.c')); + showTextPage.corpusService.initCurrentCorpus().then(() => { + showTextPage.generateDownloadLink('').then(() => { + let link: HTMLLinkElement = document.querySelector(showTextPage.downloadLinkSelector); + expect(link.href.length).toBe(61); + requestSpy.and.callFake(() => Promise.reject()); + link.style.display = 'none'; + fixture.detectChanges(); + showTextPage.generateDownloadLink('').then(() => { + }, () => { + link = document.querySelector(showTextPage.downloadLinkSelector); + expect(link.style.display).toBe('none'); + done(); + }); + }); + }); + }); + + it('should get whitespace', () => { + showTextPage.corpusService.annisResponse = new AnnisResponse({nodes: []}); + let result: string = showTextPage.getWhiteSpace(0); + expect(result.length).toBe(0); + showTextPage.corpusService.annisResponse.nodes = [new NodeMC(), new NodeMC()]; + result = showTextPage.getWhiteSpace(0); + expect(result.length).toBe(1); + showTextPage.corpusService.annisResponse.nodes[1].annis_tok = '.'; + result = showTextPage.getWhiteSpace(0); + expect(result.length).toBe(0); + }); + + it('should be initialized', () => { + showTextPage.vocService.currentReferenceVocabulary = null; + showTextPage.ngOnInit(); + expect(showTextPage.vocService.currentReferenceVocabulary).toBe(VocabularyCorpus.bws); }); }); diff --git a/src/app/show-text/show-text.page.ts b/src/app/show-text/show-text.page.ts index 935bb700040c43ccfc8c0d60a2d9cdbce3dc2dfc..dc87fee98a7480299c00e710937af79deea276eb 100644 --- a/src/app/show-text/show-text.page.ts +++ b/src/app/show-text/show-text.page.ts @@ -19,14 +19,13 @@ import configMC from '../../configMC'; }) export class ShowTextPage implements OnInit { FileType = FileType; - HelperService = HelperService; ObjectKeys = Object.keys; + public downloadLinkSelector = '#download'; public highlightOOV = false; - public text: string; - public urlBase: string; public isDownloading = false; public showTextComplexity = false; public showTextComplexityDoc = false; + public text: string; public textComplexityMap = { all: 'TEXT_COMPLEXITY_ALL', n_w: 'TEXT_COMPLEXITY_WORD_COUNT', @@ -44,6 +43,7 @@ export class ShowTextPage implements OnInit { n_gerund: 'TEXT_COMPLEXITY_GERUND_COUNT', n_abl_abs: 'TEXT_COMPLEXITY_ABLATIVI_ABSOLUTI_COUNT' }; + public urlBase: string; constructor(public navCtrl: NavController, public corpusService: CorpusService, @@ -51,45 +51,42 @@ export class ShowTextPage implements OnInit { public toastCtrl: ToastController, public translateService: TranslateService, public vocService: VocabularyService, - public http: HttpClient) { - this.urlBase = configMC.backendBaseUrl + configMC.backendApiFilePath; + public http: HttpClient, + public helperService: HelperService) { } - generateDownloadLink(fileType: string) { - const formData = new FormData(); - let content: string = document.querySelector('.text').outerHTML; - // add underline elements so we do not need to specify CSS options in the backend's PDF generator - content = content.replace(/(oov">)(.+?)(<\/span>)/g, '$1$2$3'); - this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { - const authorTitle: string = cc.author + ', ' + cc.title; - content = `

${authorTitle} ${this.corpusService.currentUrn.split(':').slice(-1)[0]}

` + content; - formData.append('html_content', content); - formData.append('file_type', fileType); - formData.append('urn', this.corpusService.currentUrn); - this.isDownloading = true; - this.http.post(this.urlBase, formData).subscribe((response: string) => { - this.isDownloading = false; - const responseParts: string[] = response.split('.'); - const link: HTMLLinkElement = document.querySelector('#download'); - link.href = configMC.backendBaseUrl + configMC.backendApiFilePath + '?id=' + responseParts[0] - + '&type=' + responseParts[1]; - link.style.display = 'block'; - }, async (error: any) => { - this.isDownloading = false; - HelperService.currentError = error; - const toast = await this.toastCtrl.create({ - message: HelperService.generalErrorAlertMessage, - duration: 3000, - position: 'top' + generateDownloadLink(fileType: string): Promise { + return new Promise((resolve, reject) => { + const formData = new FormData(); + let content: string = document.querySelector('.text').outerHTML; + // add underline elements so we do not need to specify CSS options in the backend's PDF generator + content = content.replace(/(oov">)(.+?)(<\/span>)/g, '$1$2$3'); + this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { + const authorTitle: string = cc.author + ', ' + cc.title; + content = `

${authorTitle} ${this.corpusService.currentUrn.split(':').slice(-1)[0]}

` + content; + formData.append('html_content', content); + formData.append('file_type', fileType); + formData.append('urn', this.corpusService.currentUrn); + this.isDownloading = true; + this.helperService.makePostRequest(this.http, this.toastCtrl, this.urlBase, formData).then((response: string) => { + this.isDownloading = false; + const responseParts: string[] = response.split('.'); + const link: HTMLLinkElement = document.querySelector(this.downloadLinkSelector); + link.href = configMC.backendBaseUrl + configMC.backendApiFilePath + '?id=' + responseParts[0] + + '&type=' + responseParts[1]; + link.style.display = 'block'; + return resolve(); + }, () => { + return reject(); }); - toast.present().then(); }); }); } - getWhiteSpace(index: number) { + getWhiteSpace(index: number): string { if (this.corpusService.annisResponse.nodes[index + 1]) { - if ('.,\\/#!$%\\^&\\*;:{}=\\-_`~()'.indexOf(this.corpusService.annisResponse.nodes[index + 1].annis_tok) > -1) { + if (this.corpusService.annisResponse.nodes[index + 1].annis_tok && + this.corpusService.annisResponse.nodes[index + 1].annis_tok.search(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g) >= 0) { return ''; } return ' '; @@ -98,6 +95,7 @@ export class ShowTextPage implements OnInit { } ngOnInit(): void { + this.urlBase = configMC.backendBaseUrl + configMC.backendApiFilePath; this.vocService.currentReferenceVocabulary = this.vocService.currentReferenceVocabulary || VocabularyCorpus.bws; } } diff --git a/src/app/sources/sources.page.html b/src/app/sources/sources.page.html index 1411c6a617ec8faa25a44944a2a3003dbe4e2442..11d0798858aa8b99f130ca792133c4792e076fbd 100644 --- a/src/app/sources/sources.page.html +++ b/src/app/sources/sources.page.html @@ -2,12 +2,12 @@
- + {{ 'SOURCES' | translate }}
@@ -136,11 +136,11 @@ - + Übung erstellen - + Test beginnen diff --git a/src/app/sources/sources.page.ts b/src/app/sources/sources.page.ts index 7c2be121f292810c733abbaf93a5940b2c404054..bdae6e7ccf586b810e5d08ab0d5717b3bece0fd9 100644 --- a/src/app/sources/sources.page.ts +++ b/src/app/sources/sources.page.ts @@ -1,28 +1,20 @@ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; import {HelperService} from 'src/app/helper.service'; import {NavController} from '@ionic/angular'; import {HttpClient} from '@angular/common/http'; import {TranslateService} from '@ngx-translate/core'; @Component({ - selector: 'app-sources', - templateUrl: './sources.page.html', - styleUrls: ['./sources.page.scss'], + selector: 'app-sources', + templateUrl: './sources.page.html', + styleUrls: ['./sources.page.scss'], }) export class SourcesPage { - HelperService = HelperService; - - constructor(public navCtrl: NavController, - public http: HttpClient, - public translate: TranslateService) { } - - goToAuthorPage() { - this.navCtrl.navigateForward('/author').then(); - } - - goToTestPage() { - this.navCtrl.navigateForward('/test').then(); - } + constructor(public navCtrl: NavController, + public http: HttpClient, + public translate: TranslateService, + public helperService: HelperService) { + } } diff --git a/src/app/test/test.page.html b/src/app/test/test.page.html index cd93b2e88b0ccbcdde476c2da3e9ed42ff9f0051..6306358905c8026ba75c7856d3bd99c24feb83bc 100644 --- a/src/app/test/test.page.html +++ b/src/app/test/test.page.html @@ -4,9 +4,9 @@ {{ (currentExerciseIndex == 0 ? 'TEST' : (isTestMode ? 'START_TEST' : 'START_LEARNING')) | translate }}
- + -
@@ -16,7 +16,7 @@ - + Show exercise: - + {{ 'TEST_MODULE_GO_TO_EXERCISE' | translate}}: - {{ 'START_LEARNING' | translate}} + {{ 'START_LEARNING' | translate}} - {{'START_TEST' | translate }} + {{'START_TEST' | translate }}
@@ -102,7 +102,7 @@
- + {{ 'BUTTON_CONTINUE' | translate}} @@ -170,15 +170,15 @@ - + {{ 'TEST_MODULE_SEND_DATA' | translate }} - + {{ 'EXERCISE_GENERATE' | translate }} - + {{ 'TEST_REPEAT' | translate }} diff --git a/src/app/test/test.page.spec.ts b/src/app/test/test.page.spec.ts index f867601193c327c1a1221f5486e4f3f8bd115733..7a1d5e6d7158a310b2f7ae6249f59ccfb532c3b7 100644 --- a/src/app/test/test.page.spec.ts +++ b/src/app/test/test.page.spec.ts @@ -1,21 +1,37 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {async, ComponentFixture, inject, TestBed, TestBedStatic} from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {TestPage} from './test.page'; import {IonicStorageModule} from '@ionic/storage'; import {RouterModule} from '@angular/router'; import {TranslateTestingModule} from '../translate-testing/translate-testing.module'; -import {PopoverController, ToastController} from '@ionic/angular'; +import {PopoverController} from '@ionic/angular'; import {APP_BASE_HREF} from '@angular/common'; import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TestResultMC} from '../models/testResultMC'; +import StatementBase from '../models/xAPI/StatementBase'; +import Result from '../models/xAPI/Result'; +import Score from '../models/xAPI/Score'; +import {TestModuleState} from '../models/enum'; +import MockMC from '../models/mockMC'; +import {XAPIevent} from '../models/xAPIevent'; +import Spy = jasmine.Spy; +import H5PeventDispatcherMock from '../models/h5pEventDispatcherMock'; +import Verb from '../models/xAPI/Verb'; +import configMC from '../../configMC'; +import Context from '../models/xAPI/Context'; +import ContextActivities from '../models/xAPI/ContextActivities'; +import Activity from '../models/xAPI/Activity'; +import Definition from '../models/xAPI/Definition'; + +declare var H5P: any; describe('TestPage', () => { - let component: TestPage; + let testPage: TestPage; let fixture: ComponentFixture; - let tbs: TestBedStatic; beforeEach(async(() => { - tbs = TestBed.configureTestingModule({ + TestBed.configureTestingModule({ declarations: [TestPage], imports: [ HttpClientTestingModule, @@ -25,8 +41,7 @@ describe('TestPage', () => { ], providers: [ {provide: APP_BASE_HREF, useValue: '/'}, - {provide: ToastController}, - {provide: PopoverController, useValue: {}}, + {provide: PopoverController, useValue: MockMC.popoverController}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }); @@ -34,11 +49,242 @@ describe('TestPage', () => { beforeEach(() => { fixture = TestBed.createComponent(TestPage); - component = fixture.componentInstance; + testPage = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(testPage).toBeTruthy(); + }); + + it('should adjust the timer', () => { + testPage.isTestMode = false; + const timerElement: HTMLSpanElement = document.querySelector(testPage.timerIDstring); + timerElement.innerHTML = '1'; + testPage.adjustTimer(1, false); + expect(document.querySelector(testPage.timerIDstring).innerHTML).toBe('1'); + testPage.isTestMode = true; + testPage.adjustTimer(testPage.currentExerciseParts[testPage.currentExerciseParts.length - 1].startIndex, true); + expect(document.querySelector(testPage.timerIDstring).innerHTML).toBe('1'); + testPage.adjustTimer(1, false); + expect(document.querySelector(testPage.timerIDstring).innerHTML).toBe(testPage.timerValueZero); + }); + + it('should analyze results', () => { + testPage.isTestMode = false; + testPage.currentState = TestModuleState.inProgress; + testPage.vocService.currentTestResults = testPage.helperService.deepCopy(MockMC.testResults); + testPage.analyzeResults(); + expect(testPage.results.length).toBe(3); + testPage.vocService.currentTestResults[21] = new TestResultMC({ + statement: new StatementBase({result: new Result({score: new Score({scaled: 0, raw: 0})})}) + }); + testPage.vocService.currentTestResults[5] = testPage.vocService.currentTestResults[21]; + testPage.isTestMode = true; + testPage.analyzeResults(); + expect(testPage.results.length).toBe(4); + }); + + it('should continue to the next exercise', () => { + spyOn(testPage, 'showNextExercise'); + testPage.currentExerciseIndex = 0; + testPage.continueToNextExercise(false); + expect(testPage.currentExerciseIndex).toBe(1); + }); + + it('should attempt to exit', (done) => { + testPage.helperService.currentPopover = null; + testPage.attemptExit().then(() => { + expect(testPage.helperService.currentPopover).toBeTruthy(); + done(); + }); + }); + + it('should finish the current exercise', (done) => { + testPage.isTestMode = false; + spyOn(testPage, 'saveCurrentExerciseResult'); + const continueSpy: Spy = spyOn(testPage, 'continueToNextExercise'); + testPage.finishCurrentExercise(new XAPIevent()).then(() => { + expect(continueSpy).toHaveBeenCalledTimes(0); + testPage.isTestMode = true; + testPage.finishCurrentExercise(new XAPIevent()).then(() => { + expect(continueSpy).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); + + it('should get the current exercise name', () => { + testPage.currentExerciseIndex = 10; + const name: string = testPage.getCurrentExerciseName(); + expect(name.length).toBe(15); + }); + + it('should hide the retry button', () => { + const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString, testPage.h5pRetryClassString); + const retryButton: HTMLButtonElement = iframe.contentWindow.document.querySelector(testPage.h5pRetryClassString); + retryButton.style.display = 'block'; + testPage.hideRetryButton(); + expect(retryButton.style.display).toBe('none'); + iframe.parentNode.removeChild(iframe); + }); + + it('should initialize the timer', () => { + spyOn(testPage, 'updateTimer'); + testPage.timer = null; + testPage.initTimer(5); + expect(testPage.timer).toBeTruthy(); + clearInterval(testPage.timer); + }); + + it('should reset the test environment', () => { + testPage.currentExerciseIndex = 1; + testPage.resetTestEnvironment(); + expect(testPage.currentExerciseIndex).toBe(0); + }); + + it('should save the current result', () => { + const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString, testPage.h5pShowSolutionClassString); + const input: HTMLInputElement = iframe.contentWindow.document.createElement('input'); + input.setAttribute('id', testPage.h5pKnownIDstring.slice(1)); + iframe.contentWindow.document.body.appendChild(input); + testPage.currentExerciseIndex = 5; + testPage.knownCount = [0, 0]; + testPage.saveCurrentExerciseResult(true, new XAPIevent({data: {statement: new StatementBase()}})); + expect(testPage.knownCount[0]).toBe(0); + input.checked = true; + testPage.saveCurrentExerciseResult(true, new XAPIevent({data: {statement: new StatementBase()}})); + expect(testPage.knownCount[0]).toBe(1); + iframe.parentNode.removeChild(iframe); + }); + + it('should send data', (done) => { + testPage.wasDataSent = false; + testPage.vocService.currentTestResults[0] = new TestResultMC(); + const requestSpy: Spy = spyOn(testPage.helperService, 'makePostRequest').and.callFake(() => Promise.reject()); + testPage.sendData().then(() => { + }, () => { + expect(requestSpy).toHaveBeenCalledTimes(1); + requestSpy.and.returnValue(Promise.resolve()); + testPage.sendData().then(() => { + expect(testPage.wasDataSent).toBe(true); + expect(requestSpy).toHaveBeenCalledTimes(2); + testPage.sendData().then(() => { + expect(requestSpy).toHaveBeenCalledTimes(2); + done(); + }); + }); + }); + }); + + it('should set H5P event handlers', () => { + const finishSpy: Spy = spyOn(testPage, 'finishCurrentExercise').and.returnValue(Promise.resolve()); + const newDispatcher: H5PeventDispatcherMock = new H5PeventDispatcherMock(); + spyOn(H5P.externalDispatcher, 'on').and.callFake(newDispatcher.on.bind(newDispatcher)); + testPage.setH5PeventHandlers(); + testPage.currentState = TestModuleState.showResults; + const xapiEvent: XAPIevent = new XAPIevent({ + data: { + statement: new StatementBase({result: new Result(), verb: new Verb({id: configMC.xAPIverbIDanswered})}) + } + }); + newDispatcher.trigger('xAPI', xapiEvent); + expect(finishSpy).toHaveBeenCalledTimes(0); + testPage.currentState = TestModuleState.inProgress; + newDispatcher.trigger('xAPI', xapiEvent); + expect(finishSpy).toHaveBeenCalledTimes(1); + const inputEventSpy: Spy = spyOn(testPage, 'triggerInputEventHandler'); + const solutionsEventSpy: Spy = spyOn(testPage, 'triggerSolutionsEventHandler'); + testPage.currentState = TestModuleState.inProgress; + const domChangedEvent: any = {data: {library: testPage.h5pBlanksString}}; + testPage.areEventHandlersSet = false; + newDispatcher.trigger('domChanged', domChangedEvent); + expect(inputEventSpy).toHaveBeenCalledTimes(1); + testPage.currentState = TestModuleState.showSolutions; + testPage.areEventHandlersSet = false; + newDispatcher.trigger('domChanged', domChangedEvent); + expect(solutionsEventSpy).toHaveBeenCalledTimes(1); + }); + + it('should show the next exercise', () => { + const h5pSpy: Spy = spyOn(testPage.exerciseService, 'initH5P').and.returnValue(Promise.resolve()); + const exerciseNameSpy: Spy = spyOn(testPage, 'getCurrentExerciseName').and.returnValue(testPage.exerciseService.vocListString); + const resultsSpy: Spy = spyOn(testPage, 'analyzeResults'); + const hideButtonSpy: Spy = spyOn(testPage, 'hideRetryButton'); + const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString); + testPage.showNextExercise(1); + expect(h5pSpy).toHaveBeenCalledTimes(1); + testPage.vocService.currentTestResults[2] = new TestResultMC({ + statement: new StatementBase({ + context: + new Context({contextActivities: new ContextActivities({category: [new Activity({id: testPage.h5pDragTextString})]})}) + }) + }); + testPage.currentExerciseIndex = 2; + testPage.showNextExercise(2, true); + expect(hideButtonSpy).toHaveBeenCalledTimes(1); + exerciseNameSpy.and.returnValue(testPage.nonH5Pstring); + testPage.showNextExercise(testPage.currentExerciseParts[testPage.currentExerciseParts.length - 1].startIndex); + expect(resultsSpy).toHaveBeenCalledTimes(1); + iframe.parentNode.removeChild(iframe); + }); + + it('should trigger the input event handler', () => { + const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString, testPage.h5pCheckButtonClassString); + const checkButton: HTMLButtonElement = iframe.contentWindow.document.querySelector(testPage.h5pCheckButtonClassString); + const clickSpy: Spy = spyOn(checkButton, 'click'); + const input: HTMLInputElement = iframe.contentWindow.document.createElement('input'); + input.classList.add(testPage.h5pTextInputClassString.slice(1)); + iframe.contentWindow.document.body.appendChild(input); + testPage.triggerInputEventHandler(); + const inputs: NodeListOf = iframe.contentWindow.document.querySelectorAll(testPage.h5pTextInputClassString); + const kbe: KeyboardEvent = new KeyboardEvent('keydown', {key: 'Enter'}); + inputs[0].dispatchEvent(kbe); + expect(clickSpy).toHaveBeenCalledTimes(1); + iframe.parentNode.removeChild(iframe); + }); + + it('should trigger the solutions event handler', () => { + const hideButtonSpy: Spy = spyOn(testPage, 'hideRetryButton'); + const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString, testPage.h5pCheckButtonClassString); + testPage.currentExerciseIndex = 0; + const description = 'description'; + testPage.vocService.currentTestResults[0] = new TestResultMC({ + statement: new StatementBase({ + context: new Context({ + contextActivities: new ContextActivities({category: [new Activity({id: testPage.h5pMultiChoiceString})]}) + }), + object: new Activity({ + definition: new Definition({choices: [{description: {'en-US': description}, id: 'id'}]}) + }), + result: new Result({response: 'id'}) + }) + }); + const ul: HTMLUListElement = iframe.contentWindow.document.createElement('ul'); + ul.innerText = description + 's'; + ul.classList.add(testPage.h5pAnswerClassString.slice(1)); + iframe.contentWindow.document.body.appendChild(ul); + const clickSpy: Spy = spyOn(ul, 'click'); + testPage.triggerSolutionsEventHandler(); + expect(clickSpy).toHaveBeenCalledTimes(1); + testPage.vocService.currentTestResults[0].statement.context.contextActivities.category[0].id = testPage.h5pBlanksString; + const input: HTMLInputElement = iframe.contentWindow.document.createElement('input'); + input.classList.add(testPage.h5pTextInputClassString.slice(1)); + iframe.contentWindow.document.body.appendChild(input); + testPage.triggerSolutionsEventHandler(); + expect(input.value).toBe(testPage.vocService.currentTestResults[0].statement.result.response); + testPage.vocService.currentTestResults[0].statement.context.contextActivities.category[0].id = testPage.h5pDragTextString; + testPage.triggerSolutionsEventHandler(); + expect(hideButtonSpy).toHaveBeenCalledTimes(3); + iframe.parentNode.removeChild(iframe); + }); + + it('should update the timer', () => { + testPage.countDownDateTime = new Date().getTime() - 1; + const iframe: HTMLIFrameElement = MockMC.addIframe(testPage.exerciseService.h5pIframeString, testPage.h5pCheckButtonClassString); + const showNextSpy: Spy = spyOn(testPage, 'showNextExercise'); + testPage.updateTimer(); + expect(showNextSpy).toHaveBeenCalledTimes(1); + iframe.parentNode.removeChild(iframe); }); }); diff --git a/src/app/test/test.page.ts b/src/app/test/test.page.ts index 5f53a395ffa060308d428cb463bd0f1e1043742e..907f34622a746e2dd2243f2fd1c8ac8a942ed743 100644 --- a/src/app/test/test.page.ts +++ b/src/app/test/test.page.ts @@ -10,12 +10,13 @@ import {ConfirmCancelPage} from 'src/app/confirm-cancel/confirm-cancel.page'; import {ExercisePart} from 'src/app/models/exercisePart'; import Activity from 'src/app/models/xAPI/Activity'; import LanguageMap from 'src/app/models/xAPI/LanguageMap'; -import {HttpClient, HttpErrorResponse} from '@angular/common/http'; +import {HttpClient} from '@angular/common/http'; import Context from 'src/app/models/xAPI/Context'; import {TestResultMC} from 'src/app/models/testResultMC'; import {ExerciseService} from 'src/app/exercise.service'; import configMC from '../../configMC'; import {Storage} from '@ionic/storage'; +import {CorpusService} from '../corpus.service'; declare var H5P: any; @@ -27,7 +28,6 @@ declare var H5P: any; export class TestPage implements OnDestroy, OnInit { Array = Array; - HelperService = HelperService; Object = Object; TestModuleState = TestModuleState; TestType = TestType; @@ -76,17 +76,18 @@ export class TestPage implements OnDestroy, OnInit { 'fill_blanks_4', 'multi_choice_18', 'multi_choice_9'] }), new ExercisePart({exercises: ['nonH5P_2'], startIndex: 0})]; public configMC = configMC; + public countDownDateTime: number; public currentExerciseIndex: number; public currentExerciseParts: ExercisePart[]; public currentState: TestModuleState = TestModuleState.inProgress; - public dataAlreadySentMessage: string; - public dataSentSuccessMessage: string; public didTimeRunOut = false; public exerciseIndices: number[]; public finishExerciseTimeout = 200; + public h5pAnswerClassString = '.h5p-answer'; public h5pBlanksString = 'H5P.Blanks'; public h5pCheckButtonClassString = '.h5p-question-check-answer'; public h5pDragTextString = 'H5P.DragText'; + public h5pKnownIDstring = '#known'; public h5pMultiChoiceString = 'H5P.MultiChoice'; public h5pRetryClassString = '.h5p-question-try-again'; public h5pRowIDstring = '#h5p-row'; @@ -102,6 +103,7 @@ export class TestPage implements OnDestroy, OnInit { public testType: TestType; public timer: any; public timerIDstring = '#timer'; + public timerValueZero = '00m00s'; public wasDataSent: boolean; constructor(public navCtrl: NavController, @@ -111,12 +113,12 @@ export class TestPage implements OnDestroy, OnInit { public http: HttpClient, public toastCtrl: ToastController, public exerciseService: ExerciseService, - public storage: Storage) { - this.translate.get('DATA_SENT').subscribe(value => this.dataSentSuccessMessage = value); - this.translate.get('DATA_ALREADY_SENT').subscribe(value => this.dataAlreadySentMessage = value); + public storage: Storage, + public helperService: HelperService, + public corpusService: CorpusService) { } - addScore(allTestIndices: number[], exercisePartIndex: number) { + addScore(allTestIndices: number[], exercisePartIndex: number): void { const relevantTestIndices = allTestIndices.filter( x => this.currentExerciseParts[exercisePartIndex].startIndex <= x && (!this .currentExerciseParts[exercisePartIndex + 1] || x < this.currentExerciseParts[exercisePartIndex + 1].startIndex)); @@ -126,7 +128,7 @@ export class TestPage implements OnDestroy, OnInit { this.results.push([correctlySolved.length, relevantTestIndices.length]); } - adjustStartIndices() { + adjustStartIndices(): void { this.currentExerciseParts[0].startIndex = 0; [...Array(this.currentExerciseParts.length).keys()].forEach((index: number) => { if (index === 0) { @@ -137,7 +139,7 @@ export class TestPage implements OnDestroy, OnInit { }); } - adjustTimer(newIndex: number, review: boolean) { + adjustTimer(newIndex: number, review: boolean): void { if (!this.isTestMode) { return; } @@ -153,7 +155,7 @@ export class TestPage implements OnDestroy, OnInit { } } - analyzeResults() { + analyzeResults(): void { this.results = []; this.resultsBaseIndex = this.isTestMode ? 2 : 1; const allTestIndices = Object.keys(this.vocService.currentTestResults).map(x => +x); @@ -182,7 +184,19 @@ export class TestPage implements OnDestroy, OnInit { this.currentState = TestModuleState.showResults; } - continue(isTestMode: boolean = true) { + attemptExit(ev: any = null): Promise { + return new Promise(async (resolve) => { + this.helperService.currentPopover = await this.popoverController.create({ + component: ConfirmCancelPage, + event: ev, + translucent: true + }); + this.helperService.currentPopover.present().then(); + return resolve(); + }); + } + + continueToNextExercise(isTestMode: boolean = true): void { if (this.isTestMode && !isTestMode) { // no pretest in learning mode this.deleteExercisePart(1); @@ -192,7 +206,7 @@ export class TestPage implements OnDestroy, OnInit { this.showNextExercise(this.currentExerciseIndex, this.currentState === TestModuleState.showSolutions); } - deleteExercisePart(index: number) { + deleteExercisePart(index: number): void { this.exerciseIndices = []; this.currentExerciseParts.splice(index, 1); this.adjustStartIndices(); @@ -203,32 +217,26 @@ export class TestPage implements OnDestroy, OnInit { }, 50); } - async exit(ev: any = null) { - HelperService.currentPopover = await this.popoverController.create({ - component: ConfirmCancelPage, - event: ev, - translucent: true - }); - return await HelperService.currentPopover.present(); - } - - finishExercise(event: XAPIevent) { - if (!this.isTestMode) { - this.saveResult(false, event); - return; - } - // hide H5P immediately so the solutions are not visible to the user - document.querySelector(this.h5pRowIDstring).classList.add(this.hideClassString); - // dirty hack to wait for the solutions being processed by H5P - setTimeout(() => { - this.saveResult(true, event); - if (!this.didTimeRunOut) { - this.continue(); + finishCurrentExercise(event: XAPIevent): Promise { + return new Promise(resolve => { + if (!this.isTestMode) { + this.saveCurrentExerciseResult(false, event); + return resolve(); } - }, this.finishExerciseTimeout); + // hide H5P immediately so the solutions are not visible to the user + document.querySelector(this.h5pRowIDstring).classList.add(this.hideClassString); + // dirty hack to wait for the solutions being processed by H5P + setTimeout(() => { + this.saveCurrentExerciseResult(true, event); + if (!this.didTimeRunOut) { + this.continueToNextExercise(); + } + return resolve(); + }, this.finishExerciseTimeout); + }); } - getCurrentExerciseName() { + getCurrentExerciseName(): string { const targetPartIndex: number = this.getCurrentExercisePartIndex(); if (!targetPartIndex) { return ''; @@ -237,13 +245,13 @@ export class TestPage implements OnDestroy, OnInit { .exercises[this.currentExerciseIndex - this.currentExerciseParts[targetPartIndex].startIndex]; } - getCurrentExercisePartIndex() { + getCurrentExercisePartIndex(): number { return [...Array(this.currentExerciseParts.length).keys()].find( i => this.currentExerciseParts[i].startIndex <= this.currentExerciseIndex && (!this.currentExerciseParts[i + 1] || this.currentExerciseParts[i + 1].startIndex > this.currentExerciseIndex)); } - hideRetryButton() { + hideRetryButton(): void { const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); const iframeDoc: Document = iframe.contentWindow.document; // hide the retry button during review @@ -253,47 +261,14 @@ export class TestPage implements OnDestroy, OnInit { } } - initTimer(durationSeconds: number) { + initTimer(durationSeconds: number): void { // add the new duration to countdown - const countDownDate = new Date(new Date().getTime() + durationSeconds * 1000).getTime(); // 15 1000 + this.countDownDateTime = new Date(new Date().getTime() + durationSeconds * 1000).getTime(); // Update the countdown every 1 second - this.timer = setInterval(() => { - // Get today's date and time - const now = new Date().getTime(); - // Find the distance between now and the countdown date - const distance = countDownDate - now; - // Time calculations for minutes and seconds - const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((distance % (1000 * 60)) / 1000); - // Output the result in an element with the corresponding ID - const timerElement = document.querySelector(this.timerIDstring); - if (timerElement) { - timerElement.innerHTML = minutes + 'm' + seconds + 's '; - } - // If the count down is over, write some text - if (distance < 0) { - this.removeTimer(false); - const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); - if (iframe) { - const checkButton: HTMLButtonElement = iframe.contentWindow.document.body.querySelector(this.h5pCheckButtonClassString); - if (checkButton) { - // prevent the check button from jumping to the next exercise - this.didTimeRunOut = true; - checkButton.click(); - // dirty hack to wait for the XAPI handlers - setTimeout(() => { - this.didTimeRunOut = false; - }, this.finishExerciseTimeout); - } - } - const newIndex: number = this.currentExerciseParts[this.getCurrentExercisePartIndex() + 1].startIndex; - this.currentExerciseIndex = newIndex; - this.showNextExercise(newIndex); - } - }, 1000); + this.timer = setInterval(this.updateTimer, 1000); } - ngOnDestroy() { + ngOnDestroy(): void { this.removeTimer(false); H5P.externalDispatcher.off('xAPI'); H5P.externalDispatcher.off('domChanged'); @@ -315,7 +290,7 @@ export class TestPage implements OnDestroy, OnInit { } } - randomizeTestType() { + randomizeTestType(): void { this.currentExerciseParts = this.availableExerciseParts.slice(); // remove either the second last or third last exercise const index: number = Math.random() < 0.5 ? 3 : 4; @@ -323,20 +298,20 @@ export class TestPage implements OnDestroy, OnInit { this.deleteExercisePart(this.currentExerciseParts.length - index); } - removeTimer(freeze: boolean) { - const timerElement = document.querySelector(this.timerIDstring); + removeTimer(freeze: boolean): void { + const timerElement: HTMLSpanElement = document.querySelector(this.timerIDstring); clearInterval(this.timer); if (timerElement && !freeze) { - timerElement.innerHTML = '00m00s'; + timerElement.innerHTML = this.timerValueZero; } } - reset() { + resetTestEnvironment(): void { this.ngOnDestroy(); this.ngOnInit(); } - saveResult(showSolutions: boolean, event: XAPIevent) { + saveCurrentExerciseResult(showSolutions: boolean, event: XAPIevent): void { const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); if (iframe) { const iframeDoc: Document = iframe.contentWindow.document; @@ -345,7 +320,7 @@ export class TestPage implements OnDestroy, OnInit { statement: event.data.statement, innerHTML: inner }); - const knownCheckbox: HTMLInputElement = iframeDoc.querySelector('#known'); + const knownCheckbox: HTMLInputElement = iframeDoc.querySelector(this.h5pKnownIDstring); if (knownCheckbox) { this.knownCount = [this.knownCount[0] + (knownCheckbox.checked ? 1 : 0), this.knownCount[1] + 1]; } @@ -358,54 +333,37 @@ export class TestPage implements OnDestroy, OnInit { } } - async sendData() { - if (this.wasDataSent) { - const toast = await this.toastCtrl.create({ - message: this.dataAlreadySentMessage, - duration: 3000, - position: 'top' - }); - toast.present().then(); - return; - } - const fileUrl: string = configMC.backendBaseUrl + configMC.backendApiFilePath; - HelperService.currentError = null; - HelperService.isLoading = true; - const formData = new FormData(); - // tslint:disable-next-line:prefer-const - let learningResult: object = {}; - Object.keys(this.vocService.currentTestResults) - .forEach(i => learningResult[i] = this.vocService.currentTestResults[i].statement); - formData.append('learning_result', JSON.stringify(learningResult)); - this.http.post(fileUrl, formData).subscribe(async () => { - HelperService.isLoading = false; - this.wasDataSent = true; - const toast = await this.toastCtrl.create({ - message: this.dataSentSuccessMessage, - duration: 3000, - position: 'top' - }); - toast.present().then(); - }, async (error: HttpErrorResponse) => { - HelperService.isLoading = false; - HelperService.currentError = error; - const toast = await this.toastCtrl.create({ - message: HelperService.generalErrorAlertMessage, - duration: 3000, - position: 'top' + sendData(): Promise { + return new Promise((resolve, reject) => { + if (this.wasDataSent) { + this.helperService.showToast(this.toastCtrl, this.corpusService.dataAlreadySentMessage).then(); + return resolve(); + } + const fileUrl: string = configMC.backendBaseUrl + configMC.backendApiFilePath; + const formData = new FormData(); + // tslint:disable-next-line:prefer-const + let learningResult: object = {}; + Object.keys(this.vocService.currentTestResults) + .forEach(i => learningResult[i] = this.vocService.currentTestResults[i].statement); + formData.append('learning_result', JSON.stringify(learningResult)); + this.helperService.makePostRequest(this.http, this.toastCtrl, fileUrl, formData).then(async () => { + this.wasDataSent = true; + this.helperService.showToast(this.toastCtrl, this.corpusService.dataSentSuccessMessage).then(); + return resolve(); + }, () => { + return reject(); }); - toast.present().then(); }); } - setH5PeventHandlers() { + setH5PeventHandlers(): void { H5P.externalDispatcher.on('xAPI', (event: XAPIevent) => { if (this.currentState !== TestModuleState.inProgress) { return; } // results are only available when a task has been completed/answered, not in the "attempted" or "interacted" stages if (event.data.statement.verb.id === configMC.xAPIverbIDanswered && event.data.statement.result) { - this.finishExercise(event); + this.finishCurrentExercise(event).then(); } }); H5P.externalDispatcher.on('domChanged', (event: any) => { @@ -421,7 +379,7 @@ export class TestPage implements OnDestroy, OnInit { }); } - showNextExercise(newIndex: number, review: boolean = false) { + showNextExercise(newIndex: number, review: boolean = false): void { this.adjustTimer(newIndex, review); const currentExercisePart: ExercisePart = this.currentExerciseParts[this.getCurrentExercisePartIndex()]; const maxProgress: number = currentExercisePart.exercises.length; @@ -452,15 +410,15 @@ export class TestPage implements OnDestroy, OnInit { } const fileName: string = currentExerciseName.split('_').slice(-1) + '_' + this.translate.currentLang + '.json'; let exerciseType = currentExerciseName.split('_').slice(0, 2).join('_'); - this.storage.set(configMC.localStorageKeyH5P, HelperService.baseUrl + '/assets/h5p/' + this.storage.set(configMC.localStorageKeyH5P, this.helperService.baseUrl + '/assets/h5p/' + exerciseType + '/content/' + fileName).then(); if (exerciseType.startsWith(this.exerciseService.vocListString)) { exerciseType = this.exerciseService.fillBlanksString; } - this.exerciseService.initH5P(exerciseType); + this.exerciseService.initH5P(exerciseType).then(); } - triggerInputEventHandler() { + triggerInputEventHandler(): void { const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); if (iframe) { const inputs: NodeList = iframe.contentWindow.document.querySelectorAll(this.h5pTextInputClassString); @@ -476,12 +434,12 @@ export class TestPage implements OnDestroy, OnInit { checkButton.click(); } } - }, {passive: true}); + }, {passive: false}); }); } } - triggerSolutionsEventHandler() { + triggerSolutionsEventHandler(): void { const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); if (iframe) { if (this.vocService.currentTestResults[this.currentExerciseIndex]) { @@ -493,7 +451,8 @@ export class TestPage implements OnDestroy, OnInit { const oldChosen: { description: LanguageMap, id: string }[] = oldActivity.definition.choices.filter( x => singleResponses.indexOf(x.id) > -1); const oldCheckedStrings: string[] = oldChosen.map(x => x.description[Object.keys(x.description)[0]]); - const newOptions: NodeList = iframe.contentWindow.document.querySelectorAll('.h5p-answer'); + const newOptions: NodeListOf = iframe.contentWindow.document.querySelectorAll( + this.h5pAnswerClassString); newOptions.forEach((newOption: HTMLUListElement) => { if (oldCheckedStrings.indexOf(newOption.innerText.slice(0, -1)) > -1) { newOption.click(); @@ -517,7 +476,42 @@ export class TestPage implements OnDestroy, OnInit { } } - updateUI() { + public updateTimer(): void { + // Get today's date and time + const now = new Date().getTime(); + // Find the distance between now and the countdown date + const distance = this.countDownDateTime - now; + // Time calculations for minutes and seconds + const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((distance % (1000 * 60)) / 1000); + // Output the result in an element with the corresponding ID + const timerElement: HTMLSpanElement = document.querySelector(this.timerIDstring); + if (timerElement) { + timerElement.innerHTML = minutes + 'm' + seconds + 's '; + } + // If the count down is over, write some text + if (distance < 0) { + this.removeTimer(false); + const iframe: HTMLIFrameElement = document.querySelector(this.exerciseService.h5pIframeString); + if (iframe) { + const checkButton: HTMLButtonElement = iframe.contentWindow.document.body.querySelector(this.h5pCheckButtonClassString); + if (checkButton) { + // prevent the check button from jumping to the next exercise + this.didTimeRunOut = true; + checkButton.click(); + // dirty hack to wait for the XAPI handlers + setTimeout(() => { + this.didTimeRunOut = false; + }, this.finishExerciseTimeout); + } + } + const newIndex: number = this.currentExerciseParts[this.getCurrentExercisePartIndex() + 1].startIndex; + this.currentExerciseIndex = newIndex; + this.showNextExercise(newIndex); + } + } + + updateUI(): void { // dirty hack to trigger ngIf evaluation & data bindings (document.querySelector('#refreshUI') as HTMLLinkElement).click(); } diff --git a/src/app/text-range/text-range.page.html b/src/app/text-range/text-range.page.html index fee726de55b37c6c82277100e6c42284bd6976ac..6c320ddd1742417065e3d5c0073bf1dbecc54b79 100644 --- a/src/app/text-range/text-range.page.html +++ b/src/app/text-range/text-range.page.html @@ -2,12 +2,12 @@ - + {{cc.title}} @@ -163,7 +163,7 @@ - {{ (HelperService.isVocabularyCheck ? "VOCABULARY_CHECK" : "SHOW_TEXT") | translate }} + {{ (helperService.isVocabularyCheck ? "VOCABULARY_CHECK" : "SHOW_TEXT") | translate }} diff --git a/src/app/text-range/text-range.page.spec.ts b/src/app/text-range/text-range.page.spec.ts index be7cf3eb053ce09781a2cbd35f7e7c3df3b6cc2b..59c17594f08b8720c42a0fcb0b5d1bfa4adb90ee 100644 --- a/src/app/text-range/text-range.page.spec.ts +++ b/src/app/text-range/text-range.page.spec.ts @@ -13,7 +13,7 @@ import {ReplaySubject} from 'rxjs'; import {CorpusMC} from '../models/corpusMC'; describe('TextRangePage', () => { - let component: TextRangePage; + let textRangePage: TextRangePage; let fixture: ComponentFixture; beforeEach(async(() => { @@ -32,16 +32,16 @@ describe('TextRangePage', () => { ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - .compileComponents(); + .compileComponents().then(); })); beforeEach(() => { fixture = TestBed.createComponent(TextRangePage); - component = fixture.componentInstance; + textRangePage = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(textRangePage).toBeTruthy(); }); }); diff --git a/src/app/text-range/text-range.page.ts b/src/app/text-range/text-range.page.ts index bfbabef4bdbce875ab8a3a90f6b553f2667f048e..a71a8499d6385538b99998d7d9132f2abbc9b634 100644 --- a/src/app/text-range/text-range.page.ts +++ b/src/app/text-range/text-range.page.ts @@ -31,7 +31,6 @@ export class TextRangePage implements OnInit { 1: new BehaviorSubject(true) }; public isTextRangeCheckRunning = false; - HelperService = HelperService; constructor(public navCtrl: NavController, public corpusService: CorpusService, @@ -229,12 +228,7 @@ export class TextRangePage implements OnInit { this.checkTextRange(citationLabelsStart, citationLabelsEnd).then(async (isTextRangeCorrect: boolean) => { this.isTextRangeCheckRunning = false; if (!isTextRangeCorrect) { - const toast = await this.toastCtrl.create({ - message: this.corpusService.invalidTextRangeString, - duration: 3000, - position: 'top' - }); - toast.present().then(); + this.helperService.showToast(this.toastCtrl, this.corpusService.invalidTextRangeString).then(); return; } this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { @@ -246,16 +240,16 @@ export class TextRangePage implements OnInit { this.corpusService.currentUrn = newUrnBase + this.citationValuesStart.join('.') + '-' + this.citationValuesEnd.join('.'); } - HelperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => { + this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => { state.currentSetup.currentTextRange = tr; this.corpusService.isTextRangeCorrect = true; this.corpusService.getText().then(() => { if (skipText) { - HelperService.goToExerciseParametersPage(this.navCtrl).then(); - } else if (HelperService.isVocabularyCheck) { - HelperService.goToVocabularyCheckPage(this.navCtrl).then(); + this.helperService.goToExerciseParametersPage(this.navCtrl).then(); + } else if (this.helperService.isVocabularyCheck) { + this.helperService.goToVocabularyCheckPage(this.navCtrl).then(); } else { - HelperService.goToShowTextPage(this.navCtrl).then(); + this.helperService.goToShowTextPage(this.navCtrl).then(); } }, () => { }); diff --git a/src/app/translate-testing/translate-testing.module.ts b/src/app/translate-testing/translate-testing.module.ts index 6903b30829b4aa7b0d58d35f425dc3a76fe38ca3..9b6e26c5c7b23ccd3c8be2762326c79db08252c4 100644 --- a/src/app/translate-testing/translate-testing.module.ts +++ b/src/app/translate-testing/translate-testing.module.ts @@ -1,6 +1,6 @@ import {EventEmitter, Injectable, NgModule, Pipe, PipeTransform} from '@angular/core'; import {TranslateLoader, TranslateModule, TranslatePipe, TranslateService} from '@ngx-translate/core'; -import {Observable, of} from 'rxjs'; +import {Observable, of, Subscriber} from 'rxjs'; import { DefaultLangChangeEvent, LangChangeEvent, @@ -44,11 +44,17 @@ export class TranslateServiceStub { public setDefaultLang(lang: string) { } + public getDefaultLang() { + return 'en'; + } + public getBrowserLang(): string { return 'en'; } - public use(lang: string) { + public use(lang: string): Observable { + this.currentLang = lang; + return of(true); } } diff --git a/src/app/vocabulary-check/vocabulary-check.page.html b/src/app/vocabulary-check/vocabulary-check.page.html index 6f9b2ee8df7f011bed045c7866e6525df01c6749..231fadf2e216a6d9e4846a1b86ebf107c86fc6e2 100644 --- a/src/app/vocabulary-check/vocabulary-check.page.html +++ b/src/app/vocabulary-check/vocabulary-check.page.html @@ -2,12 +2,12 @@ - + {{ 'VOCABULARY_CHECK' | translate }} diff --git a/src/app/vocabulary-check/vocabulary-check.page.ts b/src/app/vocabulary-check/vocabulary-check.page.ts index e269521daaf6873e1b2ae372a0da20adba16213a..4462abcdbbe0bba521f615b00020b80afb716fcf 100644 --- a/src/app/vocabulary-check/vocabulary-check.page.ts +++ b/src/app/vocabulary-check/vocabulary-check.page.ts @@ -18,7 +18,6 @@ import {TextRange} from '../models/textRange'; styleUrls: ['./vocabulary-check.page.scss'], }) export class VocabularyCheckPage { - HelperService = HelperService; invalidSentenceCountString: string; ObjectKeys = Object.keys; VocabularyCorpus = VocabularyCorpus; @@ -33,7 +32,8 @@ export class VocabularyCheckPage { public translate: TranslateService, public corpusService: CorpusService, public http: HttpClient, - public exerciseService: ExerciseService) { + public exerciseService: ExerciseService, + public helperService: HelperService) { this.translate.get('INVALID_SENTENCE_COUNT').subscribe(value => this.invalidSentenceCountString = value); this.translate.get('INVALID_QUERY_CORPUS').subscribe(value => this.invalidQueryCorpusString = value); } @@ -42,20 +42,10 @@ export class VocabularyCheckPage { this.corpusService.currentCorpus.pipe(take(1)).subscribe(async (cc: CorpusMC) => { this.corpusService.currentTextRange.pipe(take(1)).subscribe(async (tr: TextRange) => { if (this.vocService.desiredSentenceCount < 0 || this.vocService.frequencyUpperBound < 0) { - const toast = await this.toastCtrl.create({ - message: this.invalidSentenceCountString, - duration: 3000, - position: 'top' - }); - toast.present().then(); + this.helperService.showToast(this.toastCtrl, this.invalidSentenceCountString).then(); return; } else if (!cc || tr.start.length === 0 || tr.end.length === 0 || !this.corpusService.isTextRangeCorrect) { - const toast = await this.toastCtrl.create({ - message: this.invalidQueryCorpusString, - duration: 3000, - position: 'top' - }); - toast.present().then(); + this.helperService.showToast(this.toastCtrl, this.invalidQueryCorpusString).then(); return; } this.vocService.currentSentences = []; diff --git a/src/app/vocabulary.service.spec.ts b/src/app/vocabulary.service.spec.ts index 80bbdef8d5f9c73062587160f3f6fdf045f9f466..1b71b29fe8b037b8aba3d955db241a8b6f102892 100644 --- a/src/app/vocabulary.service.spec.ts +++ b/src/app/vocabulary.service.spec.ts @@ -2,16 +2,24 @@ import {TestBed} from '@angular/core/testing'; import {VocabularyService} from './vocabulary.service'; import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {HelperService} from './helper.service'; describe('VocabularyService', () => { - beforeEach(() => TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - })); + let vocabularyService: VocabularyService; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + {provide: HelperService, useValue: {}}, + ], + }); + vocabularyService = TestBed.inject(VocabularyService); + } + ); it('should be created', () => { - const service: VocabularyService = TestBed.get(VocabularyService); - expect(service).toBeTruthy(); + expect(vocabularyService).toBeTruthy(); }); }); diff --git a/src/app/vocabulary.service.ts b/src/app/vocabulary.service.ts index 7e72f7e1d05c4ba60eebc128c630ff3565ddc654..6275868d285c0d2ec222b4674fd6f5c62e495213 100644 --- a/src/app/vocabulary.service.ts +++ b/src/app/vocabulary.service.ts @@ -22,7 +22,9 @@ export class VocabularyService { ranking: Sentence[][] = []; refVocMap: { [refVoc: string]: Vocabulary } = {}; - constructor(public http: HttpClient, public toastCtrl: ToastController) { + constructor(public http: HttpClient, + public toastCtrl: ToastController, + public helperService: HelperService) { this.refVocMap[VocabularyCorpus.agldt] = new Vocabulary({ hasFrequencyOrder: true, totalCount: 7182, @@ -65,7 +67,7 @@ export class VocabularyService { .set('frequency_upper_bound', this.frequencyUpperBound.toString()) .set('query_urn', queryUrn) .set('show_oov', showOOV ? '1' : '0'); - HelperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((ar: AnnisResponse) => { + this.helperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((ar: AnnisResponse) => { return resolve(ar); }, (error: HttpErrorResponse) => { return reject(error); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 0f6e154456214a8ca9369de6bcafd95f922f2573..08e975eadafcb3e5cb3cd021c5eba7b4452819ca 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -6,6 +6,7 @@ "AUTHOR_SELECT": "Autor auswählen", "AUTHOR_SHOW_ONLY_TREEBANKS": "Nur aufbereitete Texte (Haken entfernen für alle Autoren)", "BACK": "zurück", + "BROWSE": "Durchsuchen...", "BUTTON_CONTINUE": "Nächste Aufgabe", "CALLIDUS_PROJECT": "CALLIDUS-Projekt", "CANCEL": "Abbrechen", @@ -206,6 +207,7 @@ "GIVEN": "Gegeben", "HEAD_WORD": "Basiswort", "HELP": "Hilfe", + "HIGHLIGHT": "Markieren", "HOME": "Startseite", "HOME_INTRO": "Hier dreht sich alles um Wortschatzübungen zu Originaltexten von Cicero, Ovid und Co. Unsere Devise ist: Keine Übung ohne einen Bezug zum Kontext des Wortes, wie schon der englische Linguist John Rupert Firth 1957 schrieb:", "HOME_TITLE": "Context matters: Smart mit lateinischen Wörtern umgehen lernen!", @@ -229,7 +231,9 @@ "MACHINA_CALLIDA_BACKEND": "Machina Callida Backend", "MACHINA_CALLIDA_FRONTEND": "Machina Callida Frontend", "MACHINA_CALLIDA_INTRO": "Die entwickelte Software (Open Source-Projekt auf GitLab) unterstützt eine korpusbasierte Wortschatzarbeit in der Lektürephase des Lateinunterrichts. Sie bietet Zugriff auf zahlreiche bekannte und weniger bekannte lateinische Korpora, um für ausgewählte Textstellen Übungen zu generieren. Im Folgenden werden einige wesentliche Entwicklungsschritte nachgezeichnet.", + "MINIMUM_WORD_FREQUENCY_COUNT": "Minimale Wortfrequenz", "MOST_RECENT_SETUP": "Zuletzt genutzte Einstellungen", + "NEAREST_NEIGHBORS_COUNT": "Streuung", "NO_ENTRY_FOUND": "Kein Eintrag verfügbar", "NO_EXERCISES_FOUND": "Keine Übungen gefunden", "OF": "von", @@ -274,7 +278,9 @@ "RESEARCH_STUDIES_3": "im Lateinunterricht der älteren Fortgeschrittenen (Oberstufe) ebenfalls die Studien zu Ovid und Cicero", "RESEARCH_STUDIES_4": "eine Testung der computergestützten Übungsformate (MC) durch Studierende der Klassischen Philologie (Dez. 2018)", "RESULT": "Ergebnis", - "SEARCH": "Durchsuchen...", + "SEARCH": "Suche", + "SEARCH_REGEX_MISSING": "Bitte Suchanfrage eingeben...", + "SEMANTICS": "Semantik", "SHARE": "Teilen", "SHOW_TEXT": "Text anzeigen", "SHOW_TEXT_TITLE": "Ausgewählte Textpassage", @@ -324,6 +330,7 @@ "TEXT_SHOW_OOV": "Unbekannte Vokabeln markieren", "TEXT_TOO_LONG": "Text zu lang, max. Wortzahl: ", "TEXT_WORK": "Textarbeit", + "TOO_MANY_SEARCH_RESULTS": "Zu viele Treffer. Bitte Auswahl einschränken...", "TYPE": "Typ", "UNIT_APPLICATION_TITLE": "Wortschatzarbeit am Text", "UNIT_DATA_SECURITY": "Datenschutz: Es werden keine persönlichen Daten erhoben. Die Ergebnisse können auch nicht bis zu einzelnen Teilnehmern zurückverfolgt werden.", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b542828ca3d7e1a7e5f003a426ce4970d698582a..b3b11371a0ab57f868ac20a4094657107e447935 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -6,6 +6,7 @@ "AUTHOR_SELECT": "Select author", "AUTHOR_SHOW_ONLY_TREEBANKS": "High-quality texts only (uncheck for all authors)", "BACK": "back", + "BROWSE": "Search...", "BUTTON_CONTINUE": "Next exercise", "CALLIDUS_PROJECT": "CALLIDUS Project", "CANCEL": "Cancel", @@ -206,6 +207,7 @@ "GIVEN": "Given", "HEAD_WORD": "Head word", "HELP": "Help", + "HIGHLIGHT": "Highlight", "HOME": "Home", "HOME_INTRO": "Here everything has to do with vocabulary exercises to original texts by Cicero, Ovid and Co. Our motto is: No exercise without a reference to the context of the word, as the English linguist John Rupert Firth wrote in 1957:", "HOME_TITLE": "Context matters: Learn to use Latin words smartly!", @@ -229,7 +231,9 @@ "MACHINA_CALLIDA_BACKEND": "Machina Callida Backend", "MACHINA_CALLIDA_FRONTEND": "Machina Callida Frontend", "MACHINA_CALLIDA_INTRO": "The software (open source project, GitLab) is able to create corpus-based exercises which can be used by beginners and intermediate learners as well as by teachers of Latin. Thus, it provides access to numerous known and lesser known Latin corpora. Some essential steps of development are given below.", + "MINIMUM_WORD_FREQUENCY_COUNT": "Minimum Word Frequency", "MOST_RECENT_SETUP": "Most recent settings", + "NEAREST_NEIGHBORS_COUNT": "Dispersion", "NO_ENTRY_FOUND": "No entry available", "NO_EXERCISES_FOUND": "No exercises found", "OF": "of", @@ -274,7 +278,9 @@ "RESEARCH_STUDIES_3": "in the Latin classes of the older advanced students (upper level) also the studies to Ovid and Cicero", "RESEARCH_STUDIES_4": "a test of the computer-aided exercise formats (MC) by students of classical philology (Dec. 2018)", "RESULT": "Result", - "SEARCH": "Search...", + "SEARCH": "Search", + "SEARCH_REGEX_MISSING": "Please provide search query...", + "SEMANTICS": "Semantics", "SHARE": "Share", "SHOW_TEXT": "Show text", "SHOW_TEXT_TITLE": "Selected Text", @@ -324,6 +330,7 @@ "TEXT_SHOW_OOV": "Highlight unknown vocabulary", "TEXT_TOO_LONG": "Text too long, max. word count: ", "TEXT_WORK": "Text work", + "TOO_MANY_SEARCH_RESULTS": "Too many hits. Please refine your query...", "TYPE": "Type", "UNIT_APPLICATION_TITLE": "Vocabulary work on text", "UNIT_DATA_SECURITY": "Privacy protection: No personal data will be collected. The results can also not be traced up to individual participants.", diff --git a/src/configMC.ts b/src/configMC.ts index 2b86e6a63cb7e735415a51261320ecf5ed8f1964..6c13b2872bcfe824faba4a82960fb0609d47f26a 100644 --- a/src/configMC.ts +++ b/src/configMC.ts @@ -9,13 +9,16 @@ export default { backendApiKwicPath: 'kwic', backendApiRawtextPath: 'rawtext', backendApiValidReffPath: 'validReff', + backendApiVectorNetworkPath: 'vectorNetwork', backendApiVocabularyPath: 'vocabulary', backendBaseApiPath: '/mc-service/mc/api/v1.0', backendBaseUrl: '', bambergCoreVocabularyUrl: 'https://www.ccbuchner.de/reihe-0-0/adeo-53/', callidusProjectUrl: 'https://www.projekte.hu-berlin.de/de/callidus', developerMailTo: 'mailto:sulzkons@hu-berlin.de', + excerciseTypePathMarkWords: 'mark_words', frontendExercisePage: 'exercise', + h5pAssetFilePath: 'assets/dist/js/h5p-standalone-main.min.js', intervalCorporaUpdate: 1209600000, localStorageKeyApplicationState: 'mc/applicationState', localStorageKeyCorpora: 'mc/corpora', @@ -26,11 +29,29 @@ export default { machinaCallidaFrontendUrl: 'https://scm.cms.hu-berlin.de/callidus/mc_frontend', maxTextLength: 0, menuId: 'mc-menu', + pageUrlAuthor: '/author', + pageUrlAuthorDetail: '/author-detail', + pageUrlDocExercises: '/doc-exercises', + pageUrlDocSoftware: '/doc-software', + pageUrlDocVocUnit: '/doc-voc-unit', + pageUrlExerciseList: '/exercise-list', + pageUrlExerciseParameters: '/exercise-parameters', + pageUrlHome: '/home', + pageUrlImprint: '/imprint', + pageUrlInfo: '/info', + pageUrlKwic: '/kwic', + pageUrlPreview: '/preview', + pageUrlShowText: '/show-text', + pageUrlSemantics: '/semantics', + pageUrlSources: '/sources', + pageUrlTest: '/test', + pageUrlTextRange: '/text-range', + pageUrlVocabularyCheck: '/vocabulary-check', perseidsCTSapiBaseUrl: 'https://cts.perseids.org/api/cts?request=', perseidsCTSapiGetCapabilities: 'GetCapabilities', perseidsCTSapiGetValidReff: 'GetValidReff', perseidsCTSapiUrnSnippet: '&urn=', proielProjectUrl: 'https://proiel.github.io/', vivaURN: 'urn:custom:latinLit:viva.lat', - xAPIverbIDanswered: 'http://adlnet.gov/expapi/verbs/answered' + xAPIverbIDanswered: 'http://adlnet.gov/expapi/verbs/answered', }; diff --git a/src/karma.conf.js b/src/karma.conf.js index ba28f2a1b36240e7b33679f758dddae52446e1d9..760b1dba28afdf391fa23e75339074766da5a46e 100644 --- a/src/karma.conf.js +++ b/src/karma.conf.js @@ -13,7 +13,7 @@ module.exports = function (config) { require('@angular-devkit/build-angular/plugins/karma') ], client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + clearContext: false, // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { dir: require('path').join(__dirname, '../coverage'), @@ -21,17 +21,16 @@ module.exports = function (config) { fixWebpackSourcePaths: true }, files: [ + // "./assets/dist/js/h5p-standalone-main.js" "./assets/dist/js/h5p-standalone-main.min.js" ], + mime: {'text/x-typescript': ['ts', 'tsx']}, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['ChromeHeadlessNoSandbox'], - // browserDisconnectTimeout: 10000, - // browserDisconnectTolerance: 3, - // browserNoActivityTimeout: 60000, customLaunchers: { ChromeHeadlessNoSandbox: { base: 'ChromeHeadless', diff --git a/src/polyfills.ts b/src/polyfills.ts index 11ed22fc4ff16ef9cd12ba7b9fe0eea5bef70b87..d6ca8037b54aadc2380adf7d135e134f510d2dec 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -19,11 +19,11 @@ */ // polyfills for IE 11; DON'T MOVE THIS ANYWHERE ELSE... they need to be imported before everything else -import 'core-js/es6/object'; -import 'core-js/es6/set'; -import 'core-js/es6/array'; -import 'core-js/es6/symbol'; -import 'core-js/es6/string'; +import 'core-js/es/array'; +import 'core-js/es/object'; +import 'core-js/es/set'; +import 'core-js/es/string'; +import 'core-js/es/symbol'; /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json index 2672bf5985b0339e816dd61686ccca697b063cd5..9e1c00fcec76e4e5bfce7601a58c1619fb014231 100644 --- a/src/tsconfig.app.json +++ b/src/tsconfig.app.json @@ -9,7 +9,8 @@ "test.ts", "**/*.spec.ts", "app/translate-testing/translate-testing.module.ts", - "app/models/mock.ts", + "app/models/h5pEventDispatcherMock.ts", + "app/models/mockMC.ts", "environments/environment.prod.ts", "app/models/xAPI/IdFormattedSubStatement.ts", "app/models/xAPI/ClientModel.ts", @@ -34,13 +35,13 @@ "app/models/xAPI/UpRef.ts", "app/models/xAPI/UnstoredStatementModel.ts" ] -// "files": [ -// "main.ts", -// "zone-flags.ts", -// "polyfills.ts" -// ], -// "include": [ -// "app/models/**/*.ts", -// "app/**/*.module.ts" -// ] + // "files": [ + // "main.ts", + // "zone-flags.ts", + // "polyfills.ts" + // ], + // "include": [ + // "app/models/**/*.ts", + // "app/**/*.module.ts" + // ] } diff --git a/tsconfig.json b/tsconfig.json index 6ec43b170f8734bc0d2dafb350f50191f7342a20..3acb028930a349800eb247924aa8d6e210807bcc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,12 +6,14 @@ "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, - "module": "es2015", // es2015 + "module": "esnext", + // es2015 "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, - "target": "es5", // es5 + "target": "es5", + // es5 "typeRoots": [ "node_modules/@types" ],