From 0fe343f7d3fb53306b24792d1857a805bc728337 Mon Sep 17 00:00:00 2001 From: Konstantin Schulz Date: Wed, 25 Mar 2020 12:04:10 +0100 Subject: [PATCH] added semantics page to display vector networks for the Panegyrici Latini corpus --- .gitlab-ci.yml | 1 - angular.json | 2 +- package-lock.json | 388 +++++++++++------- package.json | 15 +- src/app/app-routing.module.ts | 4 + src/app/app.component.html | 25 +- src/app/app.component.spec.ts | 35 +- src/app/app.component.ts | 1 - src/app/author-detail/author-detail.page.html | 8 +- .../author-detail/author-detail.page.spec.ts | 16 +- src/app/author-detail/author-detail.page.ts | 8 +- src/app/author/author.page.html | 6 +- src/app/author/author.page.spec.ts | 65 ++- src/app/author/author.page.ts | 58 +-- .../confirm-cancel.page.spec.ts | 15 +- src/app/confirm-cancel/confirm-cancel.page.ts | 22 +- src/app/corpus.service.spec.ts | 379 ++++++++++++++++- src/app/corpus.service.ts | 208 +++++----- src/app/doc-exercises/doc-exercises.page.html | 12 +- src/app/doc-exercises/doc-exercises.page.ts | 4 +- src/app/doc-software/doc-software.page.html | 12 +- src/app/doc-software/doc-software.page.ts | 4 +- src/app/doc-voc-unit/doc-voc-unit.page.html | 12 +- src/app/doc-voc-unit/doc-voc-unit.page.ts | 4 +- src/app/exercise-list/exercise-list.page.html | 10 +- .../exercise-list/exercise-list.page.spec.ts | 102 ++++- src/app/exercise-list/exercise-list.page.ts | 91 ++-- .../exercise-parameters.page.html | 4 +- .../exercise-parameters.page.spec.ts | 124 +++++- .../exercise-parameters.page.ts | 123 +++--- src/app/exercise.service.spec.ts | 51 ++- src/app/exercise.service.ts | 27 +- src/app/exercise/exercise.page.html | 4 +- src/app/exercise/exercise.page.spec.ts | 69 +++- src/app/exercise/exercise.page.ts | 88 ++-- src/app/helper.service.spec.ts | 190 ++++++++- src/app/helper.service.ts | 308 ++++++++------ src/app/home/home.page.html | 18 +- src/app/home/home.page.spec.ts | 49 ++- src/app/home/home.page.ts | 62 +-- src/app/imprint/imprint.page.html | 12 +- src/app/imprint/imprint.page.ts | 24 +- src/app/info/info.page.html | 12 +- src/app/info/info.page.ts | 6 +- src/app/kwic/kwic.page.html | 7 +- src/app/kwic/kwic.page.ts | 16 +- src/app/models/h5pEventDispatcherMock.ts | 30 ++ src/app/models/mock.ts | 20 - src/app/models/mockMC.ts | 61 +++ src/app/models/xAPI/Activity.ts | 12 +- src/app/models/xAPI/Context.ts | 16 +- src/app/models/xAPI/ContextActivities.ts | 6 +- src/app/models/xAPI/Definition.ts | 6 +- src/app/models/xAPI/Result.ts | 16 +- src/app/models/xAPI/Score.ts | 6 +- src/app/models/xAPI/StatementBase.ts | 18 +- src/app/models/xAPI/Verb.ts | 10 +- src/app/preview/preview.page.html | 12 +- src/app/preview/preview.page.spec.ts | 146 ++++++- src/app/preview/preview.page.ts | 85 ++-- src/app/ranking/ranking.page.html | 4 +- src/app/ranking/ranking.page.spec.ts | 33 +- src/app/ranking/ranking.page.ts | 28 +- src/app/semantics/semantics-routing.module.ts | 17 + src/app/semantics/semantics.module.ts | 23 ++ src/app/semantics/semantics.page.html | 74 ++++ src/app/semantics/semantics.page.scss | 0 src/app/semantics/semantics.page.spec.ts | 74 ++++ src/app/semantics/semantics.page.ts | 85 ++++ src/app/show-text/show-text.page.html | 10 +- src/app/show-text/show-text.page.spec.ts | 51 ++- src/app/show-text/show-text.page.ts | 66 ++- src/app/sources/sources.page.html | 8 +- src/app/sources/sources.page.ts | 26 +- src/app/test/test.page.html | 20 +- src/app/test/test.page.spec.ts | 264 +++++++++++- src/app/test/test.page.ts | 250 ++++++----- src/app/text-range/text-range.page.html | 6 +- src/app/text-range/text-range.page.spec.ts | 8 +- src/app/text-range/text-range.page.ts | 18 +- .../translate-testing.module.ts | 10 +- .../vocabulary-check.page.html | 4 +- .../vocabulary-check/vocabulary-check.page.ts | 18 +- src/app/vocabulary.service.spec.ts | 22 +- src/app/vocabulary.service.ts | 6 +- src/assets/i18n/de.json | 9 +- src/assets/i18n/en.json | 9 +- src/configMC.ts | 23 +- src/karma.conf.js | 7 +- src/polyfills.ts | 10 +- src/tsconfig.app.json | 21 +- tsconfig.json | 6 +- 92 files changed, 3166 insertions(+), 1189 deletions(-) create mode 100644 src/app/models/h5pEventDispatcherMock.ts delete mode 100644 src/app/models/mock.ts create mode 100644 src/app/models/mockMC.ts create mode 100644 src/app/semantics/semantics-routing.module.ts create mode 100644 src/app/semantics/semantics.module.ts create mode 100644 src/app/semantics/semantics.page.html create mode 100644 src/app/semantics/semantics.page.scss create mode 100644 src/app/semantics/semantics.page.spec.ts create mode 100644 src/app/semantics/semantics.page.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5a9c61d..d0efc9e 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 493ea8b..dbbca11 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 66ea230..fe6181b 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 48c5b88..7b0dac3 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 acaeaf9..1458846 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 bb13d3a..cdc2819 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 0de89b1..48459e8 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 86696ff..8a667b4 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 2d35e19..e0abc7a 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 4f82b71..b429901 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 d3273cd..141d4dd 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 ef3bc23..dfd162b 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 f4db2c3..43c4e57 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 44ae67f..2e6c3de 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 a71e539..8d35cc9 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 6227dec..ccf958f 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 7bd4ef0..f036ec5 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 262fb03..25a7257 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 6badd47..fb4bfae 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 252316c..b65ef50 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 6f5bf33..edf32b0 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 8302914..08994df 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 0000000..ed6ed50 --- /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 630a9f9..0000000 --- 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 0000000..b659621 --- /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 afe4be4..27800ef 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 49dfff3..8b64728 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 a5a26eb..020992d 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 67ea877..f59abc2 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 2d4524f..242663b 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 8b0c45c..820363b 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 5fb8a2e..94f2dd1 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 bdfa157..28225fe 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 46a7d42..57bc606 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 62f6aa5..97c2707 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 d0beb9a..940bc43 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 f84e8cd..e6b6de7 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 dac8fbe..d13b82b 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 fcd0652..331a3e5 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 0000000..d575b00 --- /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 0000000..2a22a27 --- /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 0000000..c5bdebf --- /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 0000000..e69de29 diff --git a/src/app/semantics/semantics.page.spec.ts b/src/app/semantics/semantics.page.spec.ts new file mode 100644 index 0000000..4f5cb4a --- /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 0000000..1463c8d --- /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 c27b910..a439707 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 a4001b6..590cbcf 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 935bb70..dc87fee 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 1411c6a..11d0798 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 7c2be12..bdae6e7 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 cd93b2e..6306358 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 f867601..7a1d5e6 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 5f53a39..907f346 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 fee726d..6c320dd 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 be7cf3e..59c1759 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 bfbabef..a71a849 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 6903b30..9b6e26c 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 6f9b2ee..231fadf 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 e269521..4462abc 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 80bbdef..1b71b29 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 7e72f7e..6275868 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 0f6e154..08e975e 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 b542828..b3b1137 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 2b86e6a..6c13b28 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 ba28f2a..760b1db 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 11ed22f..d6ca803 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 2672bf5..9e1c00f 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 6ec43b1..3acb028 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" ], -- GitLab