From e204997895bcecf419516bb89e62578401c6803b Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Wed, 3 May 2023 09:53:20 +0200 Subject: [PATCH 01/22] Update h5p/h5p-core and h5p/h5p-editor Composer packages, use patch version in library folder name --- .../apis/contentauthor/app/H5PContent.php | 9 +-- .../app/H5PLibrariesHubCache.php | 16 +++-- .../apis/contentauthor/app/H5PLibrary.php | 33 ++++++++++- .../app/Libraries/H5P/Framework.php | 59 +++++++++++++++++-- .../H5P/Interfaces/CerpusStorageInterface.php | 4 -- .../H5P/Storage/H5PCerpusStorage.php | 14 +++-- .../app/Libraries/H5P/ViewConfig.php | 2 +- sourcecode/apis/contentauthor/composer.json | 4 +- sourcecode/apis/contentauthor/composer.lock | 26 ++++---- ..._in_folder_name_to_h5p_libraries_table.php | 32 ++++++++++ .../js/h5p/core-override/h5p-content-type.js | 41 +++++++++++++ .../h5p/{ => core-override}/request-queue.js | 0 .../assets/entrypoints/h5p-core-bundle.js | 5 +- .../assets/entrypoints/h5p-core.scss | 1 + .../resources/views/h5p/show.blade.php | 4 +- .../tests/Unit/H5PContentTest.php | 52 ++++++++++++++++ .../tests/Unit/H5PLibraryTest.php | 46 +++++++++++++++ 17 files changed, 304 insertions(+), 44 deletions(-) create mode 100644 sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php create mode 100755 sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js rename sourcecode/apis/contentauthor/public/js/h5p/{ => core-override}/request-queue.js (100%) mode change 100644 => 100755 create mode 100644 sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php diff --git a/sourcecode/apis/contentauthor/app/H5PContent.php b/sourcecode/apis/contentauthor/app/H5PContent.php index bbc422fc92..d2a9dcc775 100644 --- a/sourcecode/apis/contentauthor/app/H5PContent.php +++ b/sourcecode/apis/contentauthor/app/H5PContent.php @@ -326,12 +326,13 @@ public static function getContentTypeInfo(string $contentType): ?ContentTypeData if ($library->has_icon) { $h5pFramework = app(H5PFrameworkInterface::class); - $library_folder = H5PCore::libraryToString([ + $library_folder = H5PCore::libraryToFolderName([ 'machineName' => $library->machine_name, 'majorVersion' => $library->major_version, - 'minorVersion' => $library->minor_version - ], true); - + 'minorVersion' => $library->minor_version, + 'patchVersion' => $library->patch_version, + 'patchVersionInFolderName' => $library->patch_version_in_folder_name, + ]); $icon_path = $h5pFramework->getLibraryFileUrl($library_folder, 'icon.svg'); diff --git a/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php b/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php index e24b1dd307..74ce455a38 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php @@ -41,10 +41,16 @@ public function libraries() public function getLibraryString($folderName = false) { - return \H5PCore::libraryToString([ - 'machineName' => $this->name, - 'majorVersion' => $this->major_version, - 'minorVersion' => $this->minor_version, - ], $folderName); + return $folderName ? + \H5PCore::libraryToFolderName([ + 'machineName' => $this->name, + 'majorVersion' => $this->major_version, + 'minorVersion' => $this->minor_version, + ]) : + \H5PCore::libraryToString([ + 'machineName' => $this->name, + 'majorVersion' => $this->major_version, + 'minorVersion' => $this->minor_version, + ]); } } diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index a5d070008a..7c7aaa9a5b 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -36,6 +36,10 @@ class H5PLibrary extends Model protected $guarded = ['id']; + protected $casts = [ + 'patch_version_in_folder_name' => 'bool', + ]; + protected static function boot() { parent::boot(); @@ -119,7 +123,33 @@ public function getVersions($asModels = false) public function getLibraryString($folderName = false) { - return \H5PCore::libraryToString($this->getLibraryH5PFriendly(), $folderName); + $lib = $this->getLibraryH5PFriendly(); + $lib['patchVersionInFolderName'] = $this->patch_version_in_folder_name; + return $folderName ? + \H5PCore::libraryToFolderName($lib) : + \H5PCore::libraryToString($lib); + } + + /** + * Get the library name, supports patch version naming + */ + public function libraryToString(): string + { + $includePatchVersion = $this->patch_version_in_folder_name ?? false; + if ($includePatchVersion) { + return sprintf('%s %d.%d.%d', + $this->name, + $this->major_version, + $this->minor_version, + $this->patch_version + ); + } + + return sprintf('%s %d.%d', + $this->name, + $this->major_version, + $this->minor_version, + ); } public function getLibraryH5PFriendly($machineName = 'name') @@ -128,6 +158,7 @@ public function getLibraryH5PFriendly($machineName = 'name') 'machineName' => $this->$machineName, 'majorVersion' => $this->major_version, 'minorVersion' => $this->minor_version, + 'patchVersion' => $this->patch_version, ]; } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php index cccde54f45..4d6401f01b 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php @@ -21,6 +21,7 @@ use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Request; +use Illuminate\Support\Stringable; use InvalidArgumentException; use PDO; use Psr\Http\Message\ResponseInterface; @@ -132,19 +133,35 @@ public function getPlatformInfo() ]; } - public function fetchExternalData($url, $data = null, $blocking = true, $stream = null): string|null - { + public function fetchExternalData( + $url, + $data = null, + $blocking = true, + $stream = null, + $fullData = false, + $headers = [], + $files = [], + $method = 'POST' + ): string|array|null { $method = $data ? 'POST' : 'GET'; $options = [RequestOptions::FORM_PARAMS => $data]; if ($stream !== null) { $options[RequestOptions::SINK] = $stream; } + $options[RequestOptions::HEADERS] = $headers; return $this->httpClient->requestAsync($method, $url, $options) - ->then(static function (ResponseInterface $response) use ($blocking) { + ->then(static function (ResponseInterface $response) use ($blocking, $fullData) { if (!$blocking) { return null; } + if ($fullData) { + return [ + 'status' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + 'data' => $response->getBody()->getContents(), + ]; + } return $response->getBody()->getContents(); }) @@ -500,7 +517,8 @@ public function saveLibraryData(&$libraryData, $new = true) 'metadata_settings' => $libraryData['metadataSettings'], 'add_to' => $libraryData['addTo'], 'has_icon' => $libraryData['hasIcon'] ?? 0, - 'tutorial_url' => '' + 'tutorial_url' => '', + 'patch_version_in_folder_name' => true, ]); $libraryData['libraryId'] = $h5pLibrary->id; @@ -819,6 +837,7 @@ public function loadLibrary($machineName, $majorVersion, $minorVersion): array|f 'preloadedCss' => $h5pLibrary->preloaded_css, 'dropLibraryCss' => $h5pLibrary->drop_library_css, 'semantics' => $h5pLibrary->semantics, + 'patchVersionInFolderName' => $h5pLibrary->patch_version_in_folder_name, ]; foreach ($h5pLibrary->libraries as $dependency) { @@ -826,6 +845,8 @@ public function loadLibrary($machineName, $majorVersion, $minorVersion): array|f 'machineName' => $dependency->requiredLibrary->name, 'majorVersion' => $dependency->requiredLibrary->major_version, 'minorVersion' => $dependency->requiredLibrary->minor_version, + 'patchVersion' => $dependency->requiredLibrary->patch_version, + 'patchVersionInFolderName' => $dependency->requiredLibrary->patch_version_in_folder_name, ]; } @@ -910,7 +931,7 @@ public function deleteLibrary($library): void $libraryModel = H5PLibrary::findOrFail($library->id); $libraryModel->deleteOrFail(); - app(CerpusStorageInterface::class)->deleteLibrary($libraryModel); + app(\H5PFileStorage::class)->deleteLibrary($libraryModel->getLibraryH5PFriendly()); } /** @@ -953,6 +974,8 @@ public function loadContent($id) 'libraryName' => $h5pcontent->library->name, 'libraryMajorVersion' => $h5pcontent->library->major_version, 'libraryMinorVersion' => $h5pcontent->library->minor_version, + 'libraryPatchVersion' => $h5pcontent->library->patch_version, + 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(), 'libraryEmbedTypes' => $h5pcontent->library->embed_types, 'libraryFullscreen' => $h5pcontent->library->fullscreen, 'language' => $h5pcontent->metadata->default_language ?? null, @@ -986,6 +1009,8 @@ public function loadContent($id) * - preloadedJs(optional): comma separated string with js file paths * - preloadedCss(optional): comma separated sting with css file paths * - dropCss(optional): csv of machine names + * - dependencyType: editor or preloaded + * - patchVersionInFolderName: Is patch version a part of the folder name */ public function loadContentDependencies($id, $type = null) { @@ -1003,6 +1028,7 @@ public function loadContentDependencies($id, $type = null) , hl.preloaded_js AS preloadedJs , hcl.drop_css AS dropCss , hcl.dependency_type AS dependencyType + , hl.patch_version_in_folder_name AS patchVersionInFolderName FROM h5p_contents_libraries hcl JOIN h5p_libraries hl ON hcl.library_id = hl.id WHERE hcl.content_id = ?"; @@ -1334,4 +1360,27 @@ public function libraryHasUpgrade($library) $h5pLibrary = H5PLibrary::fromLibrary($library)->first(); return !is_null($h5pLibrary) && $h5pLibrary->isUpgradable(); } + + public function replaceContentHubMetadataCache($metadata, $lang) + { + // H5P Content Hub is not in use + } + + public function getContentHubMetadataCache($lang = 'en') + { + // H5P Content Hub is not in use + return new Stringable(); + } + + public function getContentHubMetadataChecked($lang = 'en') + { + // H5P Content Hub is not in use + return now()->toRfc7231String(); + } + + public function setContentHubMetadataChecked($time, $lang = 'en') + { + // H5P Content Hub is not in use + return true; + } } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php index 546efea1f4..643768c6fd 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php @@ -2,8 +2,6 @@ namespace App\Libraries\H5P\Interfaces; -use App\H5PLibrary; - interface CerpusStorageInterface { public function getDisplayPath(bool $fullUrl = true); @@ -18,8 +16,6 @@ public function getAjaxPath(); public function alterLibraryFiles($files); - public function deleteLibrary(H5PLibrary $library); - public function getFileUrl(string $path); /** diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php index 42d93478b0..5051f9fa54 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php @@ -106,7 +106,8 @@ public function cloneContentFile($file, $fromId, $toId) */ public function saveLibrary($library) { - $path = sprintf(ContentStorageSettings::LIBRARY_PATH, \H5PCore::libraryToString($library, true)); + $library['patchVersionInFolderName'] = true; + $path = sprintf(ContentStorageSettings::LIBRARY_PATH, \H5PCore::libraryToFolderName($library)); $libraryPath = Str::after($library['uploadDirectory'], $this->uploadDisk->path("")); $this->deleteLibraryFromPath($path); @@ -235,7 +236,7 @@ public function exportContent($id, $target) */ public function exportLibrary($library, $target) { - $folder = \H5PCore::libraryToString($library, true); + $folder = \H5PCore::libraryToFolderName($library); $srcPath = sprintf(ContentStorageSettings::LIBRARY_PATH, $folder); $finalTarget = Str::after($target, $this->uploadDisk->path("")) . "/$folder"; if ($this->hasLibraryVersion($folder, sprintf(ContentStorageSettings::LIBRARY_VERSION_PREFIX, $library['majorVersion'], $library['minorVersion'], $library['patchVersion']))) { @@ -493,11 +494,11 @@ public function hasPresave($libraryName, $developmentPath = null) */ public function getUpgradeScript($machineName, $majorVersion, $minorVersion) { - $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, \H5PCore::libraryToString([ + $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, \H5PCore::libraryToFolderName([ 'machineName' => $machineName, 'majorVersion' => $majorVersion, 'minorVersion' => $minorVersion, - ], true)); + ])); return $this->filesystem->exists($path) ? "/$path" : null; } @@ -605,11 +606,12 @@ private function hasLibraryVersion($path, $versionString): bool ->isNotEmpty(); } - public function deleteLibrary(H5PLibrary $library) + public function deleteLibrary($library) { - $libraryPath = sprintf(ContentStorageSettings::LIBRARY_PATH, $library->getLibraryString(true)); + $libraryPath = sprintf(ContentStorageSettings::LIBRARY_PATH, \H5PCore::libraryToFolderName($library)); $deleteRemote = $this->deleteLibraryFromPath($libraryPath); $deleteLocal = $this->uploadDisk->exists($libraryPath) ? $this->uploadDisk->deleteDirectory($libraryPath) : true; + return $deleteRemote && $deleteLocal; } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/ViewConfig.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/ViewConfig.php index 87b2638dce..0bbf8319d2 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/ViewConfig.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/ViewConfig.php @@ -162,7 +162,7 @@ private function initContentConfig() $parameters = $this->behaviorSettings($content['library']['name'], $this->adapter->alterParameters($core->filterParameters($content), $this->alterParametersSettings)); $contentConfig = new \stdClass(); - $contentConfig->library = $core::libraryToString($content['library']); + $contentConfig->library = $content['libraryFullVersionName']; $contentConfig->jsonContent = $parameters; $contentConfig->fullScreen = $content['library']['fullscreen']; $contentConfig->exportUrl = route('content-download', ['h5p' => $content['id']]); diff --git a/sourcecode/apis/contentauthor/composer.json b/sourcecode/apis/contentauthor/composer.json index dfe20eb05a..ea57326e10 100644 --- a/sourcecode/apis/contentauthor/composer.json +++ b/sourcecode/apis/contentauthor/composer.json @@ -28,8 +28,8 @@ "embed/embed": "^3.2", "firebase/php-jwt": "^6.3", "guzzlehttp/guzzle": "^7.4", - "h5p/h5p-core": "^1.24", - "h5p/h5p-editor": "dev-master#1ae19fdb80839b32dad3846d6b0a5c745f8f6187", + "h5p/h5p-core": "dev-master#06dab4041b7ae59a4f579d14a12297507145b7f8", + "h5p/h5p-editor": "dev-master#0365b081efa8b55ab9fd58594aa599f9630268f6", "laravel/framework": "^9.17", "laravel/horizon": "^5.7", "laravel/tinker": "^2.6", diff --git a/sourcecode/apis/contentauthor/composer.lock b/sourcecode/apis/contentauthor/composer.lock index 222adf016f..ca19fa350e 100644 --- a/sourcecode/apis/contentauthor/composer.lock +++ b/sourcecode/apis/contentauthor/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "211f9a624c6cce708322275ff71cb579", + "content-hash": "acf1f3d79f06b46bb188c2b4431594f2", "packages": [ { "name": "auth0/auth0-php", @@ -2504,21 +2504,22 @@ }, { "name": "h5p/h5p-core", - "version": "1.24.3", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/h5p/h5p-php-library.git", - "reference": "db3da7a1441ae6c9ffacbb8110f41758c85beaa0" + "reference": "06dab4041b7ae59a4f579d14a12297507145b7f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/h5p/h5p-php-library/zipball/db3da7a1441ae6c9ffacbb8110f41758c85beaa0", - "reference": "db3da7a1441ae6c9ffacbb8110f41758c85beaa0", + "url": "https://api.github.com/repos/h5p/h5p-php-library/zipball/06dab4041b7ae59a4f579d14a12297507145b7f8", + "reference": "06dab4041b7ae59a4f579d14a12297507145b7f8", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.0.0" }, + "default-branch": true, "type": "library", "autoload": { "files": [ @@ -2559,9 +2560,9 @@ ], "support": { "issues": "https://github.com/h5p/h5p-php-library/issues", - "source": "https://github.com/h5p/h5p-php-library/tree/wp-1.15.3" + "source": "https://github.com/h5p/h5p-php-library/tree/master" }, - "time": "2021-04-22T09:30:45+00:00" + "time": "2023-03-09T14:48:41+00:00" }, { "name": "h5p/h5p-editor", @@ -2569,12 +2570,12 @@ "source": { "type": "git", "url": "https://github.com/h5p/h5p-editor-php-library.git", - "reference": "1ae19fdb80839b32dad3846d6b0a5c745f8f6187" + "reference": "0365b081efa8b55ab9fd58594aa599f9630268f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/h5p/h5p-editor-php-library/zipball/1ae19fdb80839b32dad3846d6b0a5c745f8f6187", - "reference": "1ae19fdb80839b32dad3846d6b0a5c745f8f6187", + "url": "https://api.github.com/repos/h5p/h5p-editor-php-library/zipball/0365b081efa8b55ab9fd58594aa599f9630268f6", + "reference": "0365b081efa8b55ab9fd58594aa599f9630268f6", "shasum": "" }, "require": { @@ -2623,7 +2624,7 @@ "issues": "https://github.com/h5p/h5p-editor-php-library/issues", "source": "https://github.com/h5p/h5p-editor-php-library/tree/master" }, - "time": "2022-06-02T06:11:10+00:00" + "time": "2023-03-09T14:54:57+00:00" }, { "name": "kamermans/guzzle-oauth2-subscriber", @@ -11540,6 +11541,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { + "h5p/h5p-core": 20, "h5p/h5p-editor": 20 }, "prefer-stable": false, diff --git a/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php b/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php new file mode 100644 index 0000000000..12e3a9e032 --- /dev/null +++ b/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php @@ -0,0 +1,32 @@ +boolean('patch_version_in_folder_name')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('h5p_libraries', function (Blueprint $table) { + $table->dropColumn('patch_version_in_folder_name'); + }); + } +}; diff --git a/sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js b/sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js new file mode 100755 index 0000000000..1ce6e1dbc5 --- /dev/null +++ b/sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js @@ -0,0 +1,41 @@ +/** + * H5P.ContentType is a base class for all content types. Used by newRunnable() + * + * Functions here may be overridable by the libraries. In special cases, + * it is also possible to override H5P.ContentType on a global level. + * + * NOTE that this doesn't actually 'extend' the event dispatcher but instead + * it creates a single instance which all content types shares as their base + * prototype. (in some cases this may be the root of strange event behavior) + * + * @class + * @augments H5P.EventDispatcher + */ +H5P.ContentType = function (isRootLibrary) { + + function ContentType() {} + + // Inherit from EventDispatcher. + ContentType.prototype = new H5P.EventDispatcher(); + + /** + * Is library standalone or not? Not beeing standalone, means it is + * included in another library + * + * @return {Boolean} + */ + ContentType.prototype.isRoot = function () { + return isRootLibrary; + }; + + /** + * Returns the file path of a file in the current library + * @param {string} filePath The path to the file relative to the library folder + * @return {string} The full path to the file + */ + ContentType.prototype.getLibraryFilePath = function (filePath) { + return H5P.getLibraryPath(this.libraryInfo.versionedName.replace(' ', '-')) + '/' + filePath; + }; + + return ContentType; +}; diff --git a/sourcecode/apis/contentauthor/public/js/h5p/request-queue.js b/sourcecode/apis/contentauthor/public/js/h5p/core-override/request-queue.js old mode 100644 new mode 100755 similarity index 100% rename from sourcecode/apis/contentauthor/public/js/h5p/request-queue.js rename to sourcecode/apis/contentauthor/public/js/h5p/core-override/request-queue.js diff --git a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js index 04daeb7a66..00b4d45b95 100644 --- a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js +++ b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js @@ -3,7 +3,8 @@ import '../../../vendor/h5p/h5p-core/js/h5p.js'; import '../../../vendor/h5p/h5p-core/js/h5p-event-dispatcher.js'; import '../../../vendor/h5p/h5p-core/js/h5p-x-api-event.js'; import '../../../vendor/h5p/h5p-core/js/h5p-x-api.js'; -import '../../../vendor/h5p/h5p-core/js/h5p-content-type.js'; +import '../../../public/js/h5p/core-override/h5p-content-type.js'; //TODO Replaced to support patch-version in library folder name. Used by libraries to loads assets that is not js or css import '../../../vendor/h5p/h5p-core/js/h5p-confirmation-dialog.js'; -import '../../../public/js/h5p/request-queue.js'; //TODO Change to vanilla H5P when they fix import '../../../vendor/h5p/h5p-core/js/h5p-action-bar.js'; +import '../../../public/js/h5p/core-override/request-queue.js'; //TODO Change to vanilla H5P when they fix https://github.com/h5p/h5p-php-library/pull/66 +import '../../../vendor/h5p/h5p-core/js/h5p-tooltip.js'; diff --git a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss index f05a95dcfa..10a43c61bf 100644 --- a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss +++ b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss @@ -1,3 +1,4 @@ @import '../../../vendor/h5p/h5p-core/styles/h5p.css'; @import '../../../vendor/h5p/h5p-core/styles/h5p-confirmation-dialog.css'; @import '../../../vendor/h5p/h5p-core/styles/h5p-core-button.css'; +@import '../../../vendor/h5p/h5p-core/styles/h5p-tooltip.css'; diff --git a/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php b/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php index dc0b633c83..cfe237717a 100644 --- a/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php +++ b/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php @@ -14,7 +14,7 @@ @foreach( $styles as $css) {!! HTML::style($css) !!} @endforeach - {!! HTML::script('https://code.jquery.com/jquery-1.11.3.min.js') !!} + {!! HTML::script('https://code.jquery.com/jquery-1.12.4.min.js') !!} @if($preview && $inDraftState ?? false) @@ -39,7 +39,7 @@ diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php new file mode 100644 index 0000000000..e9694b21d0 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php @@ -0,0 +1,52 @@ +create(['has_icon' => 1]); + + $frameWork = $this->createMock(H5PFrameworkInterface::class); + $this->instance(H5PFrameworkInterface::class, $frameWork); + + $frameWork + ->expects($this->once()) + ->method('getLibraryFileUrl') + ->with('H5P.Foobar-1.2', 'icon.svg') + ->willReturn('assets/H5P.Foobar-1.2/icon.svg'); + + $result = H5PContent::getContentTypeInfo('H5P.Foobar'); + $this->assertInstanceOf(ContentTypeDataObject::class, $result); + $this->assertEquals('H5P.Foobar', $result->contentType); + $this->assertEquals($library->title, $result->title); + $this->assertEquals('assets/H5P.Foobar-1.2/icon.svg', $result->icon); + } + + public function test_getContentTypeInfo_NoIcon(): void + { + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create(); + + $frameWork = $this->createMock(H5PFrameworkInterface::class); + $this->instance(H5PFrameworkInterface::class, $frameWork); + + $frameWork + ->expects($this->never()) + ->method('getLibraryFileUrl'); + + $result = H5PContent::getContentTypeInfo('H5P.Foobar'); + $this->assertInstanceOf(ContentTypeDataObject::class, $result); + $this->assertEquals('H5P.Foobar', $result->contentType); + $this->assertEquals($library->title, $result->title); + $this->assertEquals('http://localhost/graphical/h5p_logo.svg', $result->icon); + } +} diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php new file mode 100644 index 0000000000..0b0ebcf42e --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php @@ -0,0 +1,46 @@ +make([ + 'patch_version_in_folder_name' => $usePatch, + ]); + + $this->assertEquals($expected, $lib->getLibraryString($isFolder)); + } + + public function provider_LibraryString(): Generator + { + yield [true, false, 'H5P.Foobar-1.2']; + yield [true, true, 'H5P.Foobar-1.2.3']; + yield [false, false, 'H5P.Foobar 1.2']; + yield [false, true, 'H5P.Foobar 1.2']; + } + + /** + * @dataProvider provider_libraryToString + */ + public function test_libraryToString($usePatch, $expected): void + { + /** @var H5PLibrary $lib */ + $lib = H5PLibrary::factory()->make([ + 'patch_version_in_folder_name' => $usePatch, + ]); + $this->assertEquals($expected, $lib->libraryToString()); + } + + public function provider_libraryToString(): Generator + { + yield [false, 'H5P.Foobar 1.2']; + yield [true, 'H5P.Foobar 1.2.3']; + } +} From 0f1f38e60ac1e2f8e40bb7375296de14e08a8cd8 Mon Sep 17 00:00:00 2001 From: chrieinv Date: Wed, 3 May 2023 07:54:30 +0000 Subject: [PATCH 02/22] Run php-cs-fixer --- sourcecode/apis/contentauthor/app/H5PLibrary.php | 6 ++++-- .../app/Libraries/H5P/Storage/H5PCerpusStorage.php | 1 - ..._patch_version_in_folder_name_to_h5p_libraries_table.php | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index 7c7aaa9a5b..868a9960d6 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -137,7 +137,8 @@ public function libraryToString(): string { $includePatchVersion = $this->patch_version_in_folder_name ?? false; if ($includePatchVersion) { - return sprintf('%s %d.%d.%d', + return sprintf( + '%s %d.%d.%d', $this->name, $this->major_version, $this->minor_version, @@ -145,7 +146,8 @@ public function libraryToString(): string ); } - return sprintf('%s %d.%d', + return sprintf( + '%s %d.%d', $this->name, $this->major_version, $this->minor_version, diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php index 5051f9fa54..e512f2d590 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php @@ -2,7 +2,6 @@ namespace App\Libraries\H5P\Storage; -use App\H5PLibrary; use App\Libraries\ContentAuthorStorage; use App\Libraries\DataObjects\ContentStorageSettings; use App\H5PContentsVideo; diff --git a/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php b/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php index 12e3a9e032..51b607d866 100644 --- a/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php +++ b/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class () extends Migration { /** * Run the migrations. * From 9e8d945276a248f1f41ccd9bbad0db91d07d14c2 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Fri, 5 May 2023 11:50:29 +0200 Subject: [PATCH 03/22] Fix export of content, rewrite functions to get library name and foldername, add/update unittests --- sourcecode/apis/contentauthor/.gitignore | 2 + .../apis/contentauthor/app/H5PContent.php | 10 +- .../apis/contentauthor/app/H5PLibrary.php | 73 ++++++---- .../app/Http/Controllers/H5PController.php | 4 +- .../app/Libraries/H5P/AjaxRequest.php | 10 +- .../app/Libraries/H5P/EditorStorage.php | 4 +- .../app/Libraries/H5P/Framework.php | 2 +- .../app/Libraries/H5P/H5PExport.php | 13 +- .../H5P/Storage/H5PCerpusStorage.php | 7 +- .../Http/Controllers/H5PControllerTest.php | 5 +- .../Integration/Libraries/H5P/CRUTest.php | 5 +- .../tests/Unit/H5PContentTest.php | 46 ++++-- .../tests/Unit/H5PLibrariesHubCacheTest.php | 32 +++++ .../tests/Unit/H5PLibraryTest.php | 45 +++--- .../Unit/Libraries/H5P/FrameworkTest.php | 135 +++++++++++++++++- 15 files changed, 303 insertions(+), 90 deletions(-) create mode 100644 sourcecode/apis/contentauthor/tests/Unit/H5PLibrariesHubCacheTest.php diff --git a/sourcecode/apis/contentauthor/.gitignore b/sourcecode/apis/contentauthor/.gitignore index 61b4f18c37..e4dec7ff60 100644 --- a/sourcecode/apis/contentauthor/.gitignore +++ b/sourcecode/apis/contentauthor/.gitignore @@ -31,9 +31,11 @@ Homestead.json !/public/js/videos/brightcove.js !/public/js/videos/streamps.js !/public/js/h5p/ndlah5p-youtube.js +!public/js/h5p/core-override !public/css /public/css/* !/public/css/ndlah5p-youtube.css + mix-manifest.json app/Libraries/oauth-php/example/server/cache/ ca1.sql diff --git a/sourcecode/apis/contentauthor/app/H5PContent.php b/sourcecode/apis/contentauthor/app/H5PContent.php index d2a9dcc775..d19346a9eb 100644 --- a/sourcecode/apis/contentauthor/app/H5PContent.php +++ b/sourcecode/apis/contentauthor/app/H5PContent.php @@ -325,15 +325,7 @@ public static function getContentTypeInfo(string $contentType): ?ContentTypeData if ($library->has_icon) { $h5pFramework = app(H5PFrameworkInterface::class); - - $library_folder = H5PCore::libraryToFolderName([ - 'machineName' => $library->machine_name, - 'majorVersion' => $library->major_version, - 'minorVersion' => $library->minor_version, - 'patchVersion' => $library->patch_version, - 'patchVersionInFolderName' => $library->patch_version_in_folder_name, - ]); - + $library_folder = $library->getLibraryString(true); $icon_path = $h5pFramework->getLibraryFileUrl($library_folder, 'icon.svg'); if (!empty($icon_path)) { diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index 868a9960d6..b4dbb74404 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -121,36 +121,55 @@ public function getVersions($asModels = false) return $asModels !== true ? $versions : $this->hydrate($versions->toArray()); } - public function getLibraryString($folderName = false) + /** + * @param bool $folderName True to get folder name for the library + * @param bool|null $fullVersion Use true/false to force or null to use patch_version_in_folder_name value + * @return string + */ + public function getLibraryString(bool $folderName = false, ?bool $fullVersion = null): string { - $lib = $this->getLibraryH5PFriendly(); - $lib['patchVersionInFolderName'] = $this->patch_version_in_folder_name; - return $folderName ? - \H5PCore::libraryToFolderName($lib) : - \H5PCore::libraryToString($lib); + return static::getLibraryName([ + 'machineName' => $this->name, + 'majorVersion' => $this->major_version, + 'minorVersion' => $this->minor_version, + 'patchVersion' => $this->patch_version, + 'patchVersionInFolderName' => $this->patch_version_in_folder_name, + + ], $folderName, $fullVersion); } - /** - * Get the library name, supports patch version naming - */ - public function libraryToString(): string + public static function libraryToFolderName(array $libraryData, ?bool $fullVersion = null): string { - $includePatchVersion = $this->patch_version_in_folder_name ?? false; - if ($includePatchVersion) { - return sprintf( - '%s %d.%d.%d', - $this->name, - $this->major_version, - $this->minor_version, - $this->patch_version - ); + return static::getLibraryName($libraryData, true, $fullVersion); + } + + public static function libraryToString(array $libraryData, ?bool $fullVersion = null): string + { + return static::getLibraryName($libraryData, false, $fullVersion); + } + + private static function getLibraryName(array $libraryData, bool $asFolder, ?bool $fullVersion): string + { + if ( + isset($libraryData['patchVersion']) && ( + $fullVersion === true || ( + $fullVersion === null && + isset($libraryData['patchVersionInFolderName']) && + $libraryData['patchVersionInFolderName'] + ) + ) + ) { + $format = $asFolder ? '%s-%d.%d.%d' : '%s %d.%d.%d'; + } else { + $format = $asFolder ? '%s-%d.%d' : '%s %d.%d'; } return sprintf( - '%s %d.%d', - $this->name, - $this->major_version, - $this->minor_version, + $format, + $libraryData['machineName'] ?? $libraryData['name'], + $libraryData['majorVersion'], + $libraryData['minorVersion'], + $libraryData['patchVersion'] ?? '' ); } @@ -166,7 +185,13 @@ public function getLibraryH5PFriendly($machineName = 'name') public function getTitleAndVersionString() { - return \H5PCore::libraryToString($this->getLibraryH5PFriendly('title')); + return static::getLibraryName([ + 'machineName' => $this->title, + 'majorVersion' => $this->major_version, + 'minorVersion' => $this->minor_version, + 'patchVersion' => $this->patch_version, + ], false, true); + //return \H5PCore::libraryToString($this->getLibraryH5PFriendly('title')); } public function scopeFromLibrary($query, $value) diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php index d8eaab1ee2..aab2e946eb 100644 --- a/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php +++ b/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php @@ -175,7 +175,7 @@ public function create(Request $request, H5PCore $core, $contenttype = null): Vi ->latestVersion() ->first(); if (!empty($library)) { - $contenttype = $library->getLibraryString(false); + $contenttype = $library->getLibraryString(false, false); } else { $contenttype = false; } @@ -332,7 +332,7 @@ public function edit(Request $request, int $id): View $state = H5PStateDataObject::create($displayOptions + [ 'id' => $h5pContent->id, - 'library' => $library->getLibraryString(), + 'library' => $library->getLibraryString(false, false), 'libraryid' => $h5pContent->library_id, 'parameters' => $params, 'language_iso_639_3' => $contentLanguage, diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php index a9f0996fb8..6a33bff4fa 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php @@ -259,8 +259,8 @@ private function libraryRebuild(Request $request): array $libraries = collect(); $this->getLibraryDetails($H5PLibrary, $libraries); - if ($libraries->has($H5PLibrary->getLibraryString())) { - $libraryData = $libraries->get($H5PLibrary->getLibraryString()); + if ($libraries->has($H5PLibrary->getLibraryString(false, false))) { + $libraryData = $libraries->get($H5PLibrary->getLibraryString(false, false)); if (array_key_exists('semantics', $libraryData)) { $H5PLibrary->semantics = $libraryData['semantics']; $H5PLibrary->save(); @@ -307,15 +307,15 @@ private function getLibraryDetails(H5PLibrary $H5PLibrary, Collection $affectedL $libraryData = $validator->getLibraryData($H5PLibrary->getLibraryString(true), $tmpLibraryFolder, $tmpLibraries); $libraryData['libraryId'] = $H5PLibrary->id; - if (!$affectedLibraries->has($H5PLibrary->getLibraryString())) { - $affectedLibraries->put($H5PLibrary->getLibraryString(), $libraryData); + if (!$affectedLibraries->has($H5PLibrary->getLibraryString(false, false))) { + $affectedLibraries->put($H5PLibrary->getLibraryString(false, false), $libraryData); } foreach (['preloadedDependencies', 'dynamicDependencies', 'editorDependencies'] as $value) { if (!empty($libraryData[$value])) { foreach ($libraryData[$value] as $library) { /** @var H5PLibrary $dependentLibrary */ $dependentLibrary = H5PLibrary::fromLibrary($library)->first(); - if (!$affectedLibraries->has($dependentLibrary->getLibraryString())) { + if (!$affectedLibraries->has($dependentLibrary->getLibraryString(false, false))) { $affectedLibraries = $this->getLibraryDetails($dependentLibrary, $affectedLibraries); } } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php index e35612a558..570e4fbf95 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php @@ -65,7 +65,7 @@ public function getLibraries($libraries = null) ->map(function ($h5pLibrary) { /** @var H5PLibrary $h5pLibrary */ $library = [ - 'uberName' => $h5pLibrary->getLibraryString(), + 'uberName' => $h5pLibrary->getLibraryString(false, false), 'name' => $h5pLibrary->name, 'majorVersion' => $h5pLibrary->major_version, 'minorVersion' => $h5pLibrary->minor_version, @@ -101,7 +101,7 @@ public function getLibraries($libraries = null) $library->minorVersion = $library->minor_version; // Add new library - $library->uberName = $library->getLibraryString(); + $library->uberName = $library->getLibraryString(false, false); if ($index > 0) { $library->isOld = true; } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php index 4d6401f01b..ff5b9af800 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php @@ -975,7 +975,7 @@ public function loadContent($id) 'libraryMajorVersion' => $h5pcontent->library->major_version, 'libraryMinorVersion' => $h5pcontent->library->minor_version, 'libraryPatchVersion' => $h5pcontent->library->patch_version, - 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(), + 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(false, false), 'libraryEmbedTypes' => $h5pcontent->library->embed_types, 'libraryFullscreen' => $h5pcontent->library->fullscreen, 'language' => $h5pcontent->metadata->default_language ?? null, diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php index 288bb2ed61..004e51d127 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php @@ -6,7 +6,6 @@ use App\H5PLibrary; use App\Libraries\H5P\Interfaces\H5PAdapterInterface; use App\Libraries\H5P\Interfaces\H5PExternalProviderInterface; -use H5PCore; use H5PExport as H5PDefaultExport; use Illuminate\Support\Collection; @@ -33,9 +32,13 @@ public function generateExport($convertMediaToLocal = false) } /** @var H5PLibrary $h5PLibrary */ $h5PLibrary = $this->content->library; - $library = H5PCore::libraryFromString($h5PLibrary->getLibraryString()); - $library['libraryId'] = $h5PLibrary->id; - $library['name'] = $h5PLibrary->name; + $library = [ + 'machineName' => $h5PLibrary->name, + 'majorVersion' => $h5PLibrary->major_version, + 'minorVersion' => $h5PLibrary->minor_version, + 'libraryId' => $h5PLibrary->id, + 'name' => $h5PLibrary->name, + ]; $contents = $this->content->toArray(); $contents['params'] = $this->content->parameters; @@ -46,7 +49,7 @@ public function generateExport($convertMediaToLocal = false) /** @var \H5PContentValidator $validator */ $validator = resolve(\H5PContentValidator::class); $params = (object)[ - 'library' => $h5PLibrary->getLibraryString(), + 'library' => $h5PLibrary->getLibraryString(false, false), 'params' => json_decode($this->content->parameters) ]; if (!$params->params) { diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php index e512f2d590..234b6e46f2 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php @@ -2,6 +2,7 @@ namespace App\Libraries\H5P\Storage; +use App\H5PLibrary; use App\Libraries\ContentAuthorStorage; use App\Libraries\DataObjects\ContentStorageSettings; use App\H5PContentsVideo; @@ -235,9 +236,11 @@ public function exportContent($id, $target) */ public function exportLibrary($library, $target) { - $folder = \H5PCore::libraryToFolderName($library); + $folder = H5PLibrary::libraryToFolderName($library); + // To make the exported file backward compatible, we don't use patch in target folder name + $targetFolder = H5PLibrary::libraryToFolderName($library, false); $srcPath = sprintf(ContentStorageSettings::LIBRARY_PATH, $folder); - $finalTarget = Str::after($target, $this->uploadDisk->path("")) . "/$folder"; + $finalTarget = Str::after($target, $this->uploadDisk->path("")) . "/$targetFolder"; if ($this->hasLibraryVersion($folder, sprintf(ContentStorageSettings::LIBRARY_VERSION_PREFIX, $library['majorVersion'], $library['minorVersion'], $library['patchVersion']))) { $this->exportLocalDirectory($srcPath, $finalTarget, $folder); } else { diff --git a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php index bd5a0bed8c..0cce51e3a4 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php @@ -97,7 +97,10 @@ public function testEdit(): void $data = $result->getData(); $this->assertArrayHasKey('state', $data); $state = json_decode($data['state'], true); - $this->assertEquals(License::LICENSE_CC, $state['license']); + $this->assertSame(License::LICENSE_CC, $state['license']); + + $editorSetup = json_decode($data['editorSetup']); + $this->assertSame($libs[0]->title . ' 1.2.3', $editorSetup->contentProperties->type); } /** diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php index 35ac4e648f..b12bb73841 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php @@ -496,6 +496,7 @@ public function enabledUserPublish_NotOwner() /** @var H5PContent $newContent */ $newContent = $contents->first(); + /** @var H5PLibrary $library */ $library = $newContent->library()->first(); $this->setupH5PAdapter([ @@ -513,7 +514,7 @@ public function enabledUserPublish_NotOwner() '_token' => csrf_token(), 'title' => 'New resource', 'action' => 'create', - 'library' => $library->getLibraryString(), + 'library' => $library->getLibraryString(false, false), 'parameters' => '{"params":{"simpleTest":"SimpleTest"},"metadata":{}}', 'license' => "PRIVATE", 'lti_message_type' => $this->faker->word, @@ -536,7 +537,7 @@ public function enabledUserPublish_NotOwner() ->put(route('h5p.update', $newContent->id), [ '_token' => csrf_token(), 'title' => $newContent->title, - 'library' => $library->getLibraryString(), + 'library' => $library->getLibraryString(false, false), 'parameters' => '{"params":{"simpleTest":"SimpleTest"},"metadata":{}}', 'license' => "PRIVATE", 'lti_message_type' => $this->faker->word, diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php index e9694b21d0..6aea4592a8 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php @@ -1,5 +1,7 @@ create(['has_icon' => 1]); - $frameWork = $this->createMock(H5PFrameworkInterface::class); - $this->instance(H5PFrameworkInterface::class, $frameWork); + $frameWork = $this->createMock(\H5PFrameworkInterface::class); + $this->instance(\H5PFrameworkInterface::class, $frameWork); $frameWork ->expects($this->once()) @@ -26,9 +28,33 @@ public function test_getContentTypeInfo_WithIcon(): void $result = H5PContent::getContentTypeInfo('H5P.Foobar'); $this->assertInstanceOf(ContentTypeDataObject::class, $result); - $this->assertEquals('H5P.Foobar', $result->contentType); - $this->assertEquals($library->title, $result->title); - $this->assertEquals('assets/H5P.Foobar-1.2/icon.svg', $result->icon); + $this->assertSame('H5P.Foobar', $result->contentType); + $this->assertSame($library->title, $result->title); + $this->assertSame('assets/H5P.Foobar-1.2/icon.svg', $result->icon); + } + + public function test_getContentTypeInfo_PatchName(): void + { + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create([ + 'patch_version_in_folder_name' => true, + 'has_icon' => 1, + ]); + + $frameWork = $this->createMock(\H5PFrameworkInterface::class); + $this->instance(\H5PFrameworkInterface::class, $frameWork); + + $frameWork + ->expects($this->once()) + ->method('getLibraryFileUrl') + ->with('H5P.Foobar-1.2.3', 'icon.svg') + ->willReturn('assets/H5P.Foobar-1.2.3/icon.svg'); + + $result = H5PContent::getContentTypeInfo('H5P.Foobar'); + $this->assertInstanceOf(ContentTypeDataObject::class, $result); + $this->assertSame('H5P.Foobar', $result->contentType); + $this->assertSame($library->title, $result->title); + $this->assertSame('assets/H5P.Foobar-1.2.3/icon.svg', $result->icon); } public function test_getContentTypeInfo_NoIcon(): void @@ -36,8 +62,8 @@ public function test_getContentTypeInfo_NoIcon(): void /** @var H5PLibrary $library */ $library = H5PLibrary::factory()->create(); - $frameWork = $this->createMock(H5PFrameworkInterface::class); - $this->instance(H5PFrameworkInterface::class, $frameWork); + $frameWork = $this->createMock(\H5PFrameworkInterface::class); + $this->instance(\H5PFrameworkInterface::class, $frameWork); $frameWork ->expects($this->never()) @@ -45,8 +71,8 @@ public function test_getContentTypeInfo_NoIcon(): void $result = H5PContent::getContentTypeInfo('H5P.Foobar'); $this->assertInstanceOf(ContentTypeDataObject::class, $result); - $this->assertEquals('H5P.Foobar', $result->contentType); - $this->assertEquals($library->title, $result->title); - $this->assertEquals('http://localhost/graphical/h5p_logo.svg', $result->icon); + $this->assertSame('H5P.Foobar', $result->contentType); + $this->assertSame($library->title, $result->title); + $this->assertSame('http://localhost/graphical/h5p_logo.svg', $result->icon); } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PLibrariesHubCacheTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PLibrariesHubCacheTest.php new file mode 100644 index 0000000000..eb19dbb100 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PLibrariesHubCacheTest.php @@ -0,0 +1,32 @@ + 'H5P.Foobar', + 'major_version' => 1, + 'minor_version' => 2, + 'patch_version_in_folder_name' => $usePatch, + ]); + + $this->assertSame($expected, $lib->getLibraryString($isFolder)); + } + + public function provider_LibraryString(): \Generator + { + yield [true, false, 'H5P.Foobar-1.2']; + yield [true, true, 'H5P.Foobar-1.2']; + yield [false, false, 'H5P.Foobar 1.2']; + yield [false, true, 'H5P.Foobar 1.2']; + } +} diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php index 0b0ebcf42e..8c892086c6 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php @@ -1,46 +1,39 @@ make([ - 'patch_version_in_folder_name' => $usePatch, + 'patch_version_in_folder_name' => $hasPatch, ]); - $this->assertEquals($expected, $lib->getLibraryString($isFolder)); + $this->assertSame($expected, $lib->getLibraryString($isFolder, $usePatch)); } - public function provider_LibraryString(): Generator + public function provider_libraryString(): \Generator { - yield [true, false, 'H5P.Foobar-1.2']; - yield [true, true, 'H5P.Foobar-1.2.3']; - yield [false, false, 'H5P.Foobar 1.2']; - yield [false, true, 'H5P.Foobar 1.2']; - } + yield [true, null, false, 'H5P.Foobar-1.2']; + yield [true, null, true, 'H5P.Foobar-1.2.3']; + yield [true, true, false, 'H5P.Foobar-1.2.3']; + yield [true, true, true, 'H5P.Foobar-1.2.3']; + yield [true, false, false, 'H5P.Foobar-1.2']; + yield [true, false, true, 'H5P.Foobar-1.2']; - /** - * @dataProvider provider_libraryToString - */ - public function test_libraryToString($usePatch, $expected): void - { - /** @var H5PLibrary $lib */ - $lib = H5PLibrary::factory()->make([ - 'patch_version_in_folder_name' => $usePatch, - ]); - $this->assertEquals($expected, $lib->libraryToString()); - } - - public function provider_libraryToString(): Generator - { - yield [false, 'H5P.Foobar 1.2']; - yield [true, 'H5P.Foobar 1.2.3']; + yield [false, null, false, 'H5P.Foobar 1.2']; + yield [false, null, true, 'H5P.Foobar 1.2.3']; + yield [false, true, false, 'H5P.Foobar 1.2.3']; + yield [false, true, true, 'H5P.Foobar 1.2.3']; + yield [false, false, false, 'H5P.Foobar 1.2']; + yield [false, false, true, 'H5P.Foobar 1.2']; } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php index 6e65d103ee..4809eb5781 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php @@ -4,6 +4,8 @@ namespace Tests\Unit\Libraries\H5P; +use App\H5PLibrary; +use App\H5PLibraryLibrary; use App\Libraries\H5P\Framework; use ArrayObject; use Exception; @@ -14,14 +16,17 @@ use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Response; use Illuminate\Contracts\Filesystem\Filesystem; +use Illuminate\Foundation\Testing\RefreshDatabase; use InvalidArgumentException; use PDO; -use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Tests\TestCase; final class FrameworkTest extends TestCase { + use RefreshDatabase; + /** @var ArrayObject */ private ArrayObject $history; @@ -31,6 +36,8 @@ final class FrameworkTest extends TestCase protected function setUp(): void { + parent::setUp(); + $this->history = new ArrayObject(); $this->mockedResponses = new MockHandler(); @@ -87,6 +94,30 @@ public function testFetchExternalDataWithData(): void ); } + public function testFetchExternalDataWithFullData(): void + { + $this->mockedResponses->append(new Response(200, [], 'Some body')); + + $response = $this->framework->fetchExternalData( + 'http://www.example.com', + [ + 'foo' => 'bar', + ], + fullData: true, + ); + + $this->assertSame( + 'foo=bar', + $this->history[0]['request']->getBody()->getContents(), + ); + $this->assertIsArray($response); + $this->assertArrayHasKey('status', $response); + $this->assertArrayHasKey('headers', $response); + $this->assertArrayHasKey('data', $response); + $this->assertSame(200, $response['status']); + $this->assertSame('Some body', $response['data']); + } + public function testFetchExternalDataWithGuzzleError(): void { $this->mockedResponses->append(new TransferException()); @@ -146,4 +177,106 @@ public function testGetMessagesOfUnknownType(): void $this->framework->getMessages('unknown'); } + + public function testSaveLibrary(): void + { + $input = [ + 'machineName' => 'H5P.UnitTest', + 'title' => 'Unit Test', + 'majorVersion' => 2, + 'minorVersion' => 4, + 'patchVersion' => 6, + 'runnable' => 1, + 'metadataSettings' => 'Yupp', + 'addTo' => ['machineName' => 'Something'], + 'hasIcon' => 1, + 'embedTypes' => ['E1', 'E2'], + 'preloadedJs' => [ + ['path' => 'PJ1', 'name' => 'PJ1 name', 'machineName' => 'H5P.Pj1'], + ['path' => 'PJ2', 'name' => 'PJ2 name', 'machineName' => 'H5P.Pj2'], + ], + 'preloadedCss' => [ + ['path' => 'PC1', 'name' => 'PC1 name', 'machineName' => 'H5P.Pc1'], + ['path' => 'PC2', 'name' => 'PC2 name', 'machineName' => 'H5P.Pc1'], + ], + 'dropLibraryCss' => [ + ['path' => 'DC1', 'name' => 'DC1 name', 'machineName' => 'H5P.Dc1'], + ['path' => 'DC2', 'name' => 'DC2 name', 'machineName' => 'H5P.Dc2'], + ], + 'language' => [ + 'nb' => 'Norsk Bokmål', + 'nn' => 'Norsk Nynorsk', + ], + ]; + $this->framework->saveLibraryData($input); + + $this->assertDatabaseHas('h5p_libraries', ['id' => $input['libraryId']]); + $this->assertDatabaseHas('h5p_libraries_languages', [ + 'library_id' => $input['libraryId'], + 'language_code' => 'nb', + 'translation' => 'Norsk Bokmål', + ]); + $this->assertDatabaseHas('h5p_libraries_languages', [ + 'library_id' => $input['libraryId'], + 'language_code' => 'nn', + 'translation' => 'Norsk Nynorsk', + ]); + + /** @var H5PLibrary $library */ + $library = H5PLibrary::find($input['libraryId']); + + $this->assertSame('H5P.UnitTest', $library->name); + $this->assertSame('Unit Test', $library->title); + $this->assertSame(2, $library->major_version); + $this->assertSame(4, $library->minor_version); + $this->assertSame(6, $library->patch_version); + $this->assertSame(1, $library->runnable); + $this->assertSame(0, $library->fullscreen); + $this->assertSame('E1, E2', $library->embed_types); + $this->assertSame('PJ1, PJ2', $library->preloaded_js); + $this->assertSame('PC1, PC2', $library->preloaded_css); + $this->assertSame('H5P.Dc1, H5P.Dc2', $library->drop_library_css); + $this->assertSame('', $library->semantics); + $this->assertSame(1, $library->has_icon); + $this->assertSame(true, $library->patch_version_in_folder_name); + } + + public function testLoadLibrary(): void + { + H5PLibrary::factory()->create([ + 'major_version' => 1, + 'minor_version' => 1, + 'patch_version' => 9, + ]); + H5PLibrary::factory()->create([ + 'major_version' => 1, + 'minor_version' => 2, + 'patch_version' => 2, + ]); + /** @var H5PLibrary $editDep */ + $editDep = H5PLibrary::factory()->create([ + 'name' => 'H5PEditor.Foobar', + 'patch_version_in_folder_name' => true, + ]); + /** @var H5PLibrary $saved */ + $saved = H5PLibrary::factory()->create([ + 'patch_version_in_folder_name' => true, + ]); + H5PLibraryLibrary::create([ + 'library_id' => $saved->id, + 'required_library_id' => $editDep->id, + 'dependency_type' => 'editor', + ]); + + $library = $this->framework->loadLibrary('H5P.Foobar', 1, 2); + $this->assertSame($saved->id, $library['libraryId']); + $this->assertSame($saved->name, $library['machineName']); + $this->assertSame($saved->major_version, $library['majorVersion']); + $this->assertSame($saved->minor_version, $library['minorVersion']); + $this->assertSame($saved->patch_version, $library['patchVersion']); + $this->assertSame($saved->patch_version_in_folder_name, $library['patchVersionInFolderName']); + + $this->assertSame($editDep->name, $library['editorDependencies'][0]['machineName']); + $this->assertSame($editDep->patch_version_in_folder_name, $library['editorDependencies'][0]['patchVersionInFolderName']); + } } From ae0ecbf13902e1a42483e9aa94a910bacc51f570 Mon Sep 17 00:00:00 2001 From: chrieinv Date: Fri, 5 May 2023 09:51:38 +0000 Subject: [PATCH 04/22] Run php-cs-fixer --- sourcecode/apis/contentauthor/app/H5PLibrary.php | 1 - 1 file changed, 1 deletion(-) diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index b4dbb74404..da7d6c5146 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -124,7 +124,6 @@ public function getVersions($asModels = false) /** * @param bool $folderName True to get folder name for the library * @param bool|null $fullVersion Use true/false to force or null to use patch_version_in_folder_name value - * @return string */ public function getLibraryString(bool $folderName = false, ?bool $fullVersion = null): string { From 297ae9882762997597ca4bfb18e51dc56af0b7bb Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Fri, 5 May 2023 11:59:26 +0200 Subject: [PATCH 05/22] PHPStan fixes --- sourcecode/apis/contentauthor/app/H5PLibrary.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index da7d6c5146..3829ff4994 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -127,7 +127,7 @@ public function getVersions($asModels = false) */ public function getLibraryString(bool $folderName = false, ?bool $fullVersion = null): string { - return static::getLibraryName([ + return self::getLibraryName([ 'machineName' => $this->name, 'majorVersion' => $this->major_version, 'minorVersion' => $this->minor_version, @@ -139,12 +139,12 @@ public function getLibraryString(bool $folderName = false, ?bool $fullVersion = public static function libraryToFolderName(array $libraryData, ?bool $fullVersion = null): string { - return static::getLibraryName($libraryData, true, $fullVersion); + return self::getLibraryName($libraryData, true, $fullVersion); } public static function libraryToString(array $libraryData, ?bool $fullVersion = null): string { - return static::getLibraryName($libraryData, false, $fullVersion); + return self::getLibraryName($libraryData, false, $fullVersion); } private static function getLibraryName(array $libraryData, bool $asFolder, ?bool $fullVersion): string @@ -184,13 +184,12 @@ public function getLibraryH5PFriendly($machineName = 'name') public function getTitleAndVersionString() { - return static::getLibraryName([ + return self::getLibraryName([ 'machineName' => $this->title, 'majorVersion' => $this->major_version, 'minorVersion' => $this->minor_version, 'patchVersion' => $this->patch_version, ], false, true); - //return \H5PCore::libraryToString($this->getLibraryH5PFriendly('title')); } public function scopeFromLibrary($query, $value) From aee9b12f44679f3ed661c22c13989e90cfcecd7d Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Tue, 9 May 2023 12:25:28 +0200 Subject: [PATCH 06/22] Fix getUpgradeScript, add/fix unittests --- .../apis/contentauthor/app/H5PLibrary.php | 33 +++-- .../H5P/Storage/H5PCerpusStorage.php | 9 +- .../Integration/Article/ArticleLockTest.php | 1 + .../tests/Integration/Article/ArticleTest.php | 8 +- .../Article/ArticleVersioningTest.php | 1 + .../Libraries/H5P/AjaxRequestTest.php | 78 ++++++++++++ .../Libraries/H5P/EditorStorageTest.php | 56 +++++++++ .../H5P/Storage/H5pCerpusStorageTest.php | 106 ++++++++++++++-- .../tests/Unit/H5PLibraryTest.php | 71 +++++++++-- .../Unit/Libraries/H5P/FrameworkTest.php | 47 ++++++++ .../Unit/Libraries/H5P/ViewConfigTest.php | 113 ++++++++++++++++++ 11 files changed, 477 insertions(+), 46 deletions(-) create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index 3829ff4994..4757bbb04b 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -122,8 +122,8 @@ public function getVersions($asModels = false) } /** - * @param bool $folderName True to get folder name for the library - * @param bool|null $fullVersion Use true/false to force or null to use patch_version_in_folder_name value + * @param bool $folderName True to get name suitable for folder + * @param bool|null $fullVersion Null to use patchVersionInFolderName, true/false to override */ public function getLibraryString(bool $folderName = false, ?bool $fullVersion = null): string { @@ -137,27 +137,37 @@ public function getLibraryString(bool $folderName = false, ?bool $fullVersion = ], $folderName, $fullVersion); } + /** + * @param array $libraryData Key names: machineName/name, majorVersion, minorVersion, patchVersion, patchVersionInFolderName + * @param bool|null $fullVersion Null to use patchVersionInFolderName, true/false to override + * @throws \InvalidArgumentException If requesting full version without patchVersion present in data + */ public static function libraryToFolderName(array $libraryData, ?bool $fullVersion = null): string { return self::getLibraryName($libraryData, true, $fullVersion); } + /** + * @param array $libraryData Key names: machineName/name, majorVersion, minorVersion, patchVersion, patchVersionInFolderName + * @param bool|null $fullVersion Null to use patchVersionInFolderName, true/false to override + * @throws \InvalidArgumentException If requesting full version without patchVersion present in data + */ public static function libraryToString(array $libraryData, ?bool $fullVersion = null): string { return self::getLibraryName($libraryData, false, $fullVersion); } + /** + * @throws \InvalidArgumentException If requesting full version without patchVersion present in data + */ private static function getLibraryName(array $libraryData, bool $asFolder, ?bool $fullVersion): string { - if ( - isset($libraryData['patchVersion']) && ( - $fullVersion === true || ( - $fullVersion === null && - isset($libraryData['patchVersionInFolderName']) && - $libraryData['patchVersionInFolderName'] - ) - ) - ) { + $usePatch = $fullVersion === true || ($fullVersion === null && array_key_exists('patchVersionInFolderName', $libraryData) && $libraryData['patchVersionInFolderName']); + if ($usePatch && !isset($libraryData['patchVersion'])) { + throw new \InvalidArgumentException('Full version name requested but patch version missing'); + } + + if ($usePatch) { $format = $asFolder ? '%s-%d.%d.%d' : '%s %d.%d.%d'; } else { $format = $asFolder ? '%s-%d.%d' : '%s %d.%d'; @@ -179,6 +189,7 @@ public function getLibraryH5PFriendly($machineName = 'name') 'majorVersion' => $this->major_version, 'minorVersion' => $this->minor_version, 'patchVersion' => $this->patch_version, + 'patchVersionInFolderName' => $this->patch_version_in_folder_name, ]; } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php index 234b6e46f2..14a2ab4c1b 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php @@ -496,11 +496,10 @@ public function hasPresave($libraryName, $developmentPath = null) */ public function getUpgradeScript($machineName, $majorVersion, $minorVersion) { - $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, \H5PCore::libraryToFolderName([ - 'machineName' => $machineName, - 'majorVersion' => $majorVersion, - 'minorVersion' => $minorVersion, - ])); + /** @var H5PLibrary $library */ + $library = H5PLibrary::fromLibrary([$machineName, $majorVersion, $minorVersion])->latestVersion()->first(); + $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $library->getLibraryString(true)); + return $this->filesystem->exists($path) ? "/$path" : null; } diff --git a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php index eaf17ec16a..f2be17c291 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php @@ -33,6 +33,7 @@ public function setUp(): void $versionData = new VersionData(); $this->setupVersion([ 'createVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), + 'getVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), ]); } diff --git a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php index 33af3dcb4c..a20251e6ec 100755 --- a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php @@ -132,7 +132,7 @@ public function testCreateArticleWithMathContent() public function testCreateAndEditArticleWithIframeContent() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); Event::fake(); $authId = Str::uuid(); @@ -175,7 +175,7 @@ public function testCreateAndEditArticleWithIframeContent() public function testEditArticle() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); $this->setupAuthApi([ 'getUser' => new User("1", "this", "that", "this@that.com") ]); @@ -218,7 +218,7 @@ public function testEditArticle() public function testEditArticleWithDraftEnabled() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); $this->setupAuthApi([ 'getUser' => new User("1", "this", "that", "this@that.com") ]); @@ -285,7 +285,7 @@ public function testEditArticleWithDraftEnabled() public function testViewArticle() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); /** @var Article $article */ $article = Article::factory()->create([ diff --git a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php index d63fa64eac..f31c0c9a01 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php @@ -33,6 +33,7 @@ public function setUp(): void $versionData = new VersionData(); $this->setupVersion([ 'createVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), + 'getVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), ]); } diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php new file mode 100644 index 0000000000..57ee5f7011 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php @@ -0,0 +1,78 @@ +create(); + $preLib = H5PLibrary::factory()->create(['name' => 'player', 'major_version' => 3, 'minor_version' => 14]); + $dynLib = H5PLibrary::factory()->create(['name' => 'H5P.Dynamic', 'major_version' => 2, 'minor_version' => 42, 'patch_version' => 3, 'patch_version_in_folder_name' => true]); + $edLib = H5PLibrary::factory()->create(['name' => 'FontOk', 'major_version' => 1, 'minor_version' => 3]); + + H5PLibraryLibrary::create(['library_id' => $library->id, 'required_library_id' => $preLib->id, 'dependency_type' => 'preloaded']); + H5PLibraryLibrary::create(['library_id' => $library->id, 'required_library_id' => $dynLib->id, 'dependency_type' => 'dynamic']); + H5PLibraryLibrary::create(['library_id' => $library->id, 'required_library_id' => $edLib->id, 'dependency_type' => 'editor']); + + $storage = $this->createMock(ContentAuthorStorage::class); + $this->instance(ContentAuthorStorage::class, $storage); + $storage + ->expects($this->exactly(4)) + ->method('copyFolder'); + + $framework = $this->createMock(Framework::class); + $this->instance(Framework::class, $framework); + $framework->expects($this->exactly(4)) + ->method('deleteLibraryDependencies') + ->withConsecutive([$library->id], [$preLib->id], [$dynLib->id], [$edLib->id]); + $framework->expects($this->exactly(3)) + ->method('saveLibraryDependencies') + ->withConsecutive( + [$library->id, [['machineName' => 'player', 'majorVersion' => 3, 'minorVersion' => 14]], 'preloaded'], + [$library->id, [['machineName' => 'H5P.Dynamic', 'majorVersion' => 2, 'minorVersion' => 42]], 'dynamic'], + [$library->id, [['machineName' => 'FontOk', 'majorVersion' => 1, 'minorVersion' => 3]], 'editor'], + ); + $framework + ->expects($this->exactly(8)) + ->method('getH5pPath') + ->withConsecutive(['libraries'], ['libraries/H5P.Foobar-1.2']) + ->willReturnOnConsecutiveCalls('libraries', 'libraries/H5P.Foobar-1.2'); + + $core = $this->createMock(\H5PCore::class); + $this->instance(\H5PCore::class, $core); + $core->h5pF = $framework; + $core->expects($this->once())->method('mayUpdateLibraries')->willReturn(true); + + $validator = $this->createMock(\H5PValidator::class); + $this->instance(\H5PValidator::class, $validator); + $validator->h5pF = $framework; + $validator + ->expects($this->exactly(4)) + ->method('getLibraryData') + ->withConsecutive(['H5P.Foobar-1.2'], ['player-3.14'], ['H5P.Dynamic-2.42.3'], ['FontOk-1.3']) + ->willReturnOnConsecutiveCalls([ + 'preloadedDependencies' => [['machineName' => 'player', 'majorVersion' => 3, 'minorVersion' => 14]], + 'dynamicDependencies' => [['machineName' => 'H5P.Dynamic', 'majorVersion' => 2, 'minorVersion' => 42]], + 'editorDependencies' => [['machineName' => 'FontOk', 'majorVersion' => 1, 'minorVersion' => 3]], + ], [], [], []); + + $this + ->post('/ajax', ['action' => AjaxRequest::LIBRARY_REBUILD, 'libraryId' => $library->id]) + ->assertOk() + ->assertJson([ + 'success' => true, + 'message' => 'Library rebuild', + ]); + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php new file mode 100644 index 0000000000..0dcb9a9274 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php @@ -0,0 +1,56 @@ +create(['semantics' => 'something']); + $libraries = (object) [ + 'uberName' => $library->getLibraryString(false, false), + 'name' => 'H5P.Foobar', + 'majorVersion' => 1, + 'minorVersion' => 2, + ]; + + $core = $this->createMock(\H5PCore::class); + $this->instance(\H5PCore::class, $core); + + /** @var EditorStorage $editorStorage */ + $editorStorage = app(EditorStorage::class); + $ret = $editorStorage->getLibraries([$libraries]); + + $this->assertCount(1, $ret); + $this->assertSame($library->id, $ret[0]->id); + $this->assertSame('H5P.Foobar 1.2', $ret[0]->uberName); + } + + public function test_getLibrary_allLibraries(): void + { + $lib1 = H5PLibrary::factory()->create(['semantics' => 'something']); + $lib2 = H5PLibrary::factory()->create(['name' => 'H5P.Headphones', 'semantics' => 'something']); + + $core = $this->createMock(\H5PCore::class); + $this->instance(\H5PCore::class, $core); + + /** @var EditorStorage $editorStorage */ + $editorStorage = app(EditorStorage::class); + $ret = $editorStorage->getLibraries(); + + $this->assertCount(2, $ret); + $this->assertSame($lib1->id, $ret[0]->id); + $this->assertSame($lib2->id, $ret[1]->id); + + $this->assertSame('H5P.Foobar 1.2', $ret[0]->uberName); + $this->assertSame('H5P.Headphones 1.2', $ret[1]->uberName); + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php index 8a631e8f4e..a697f07943 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php @@ -2,40 +2,78 @@ namespace Tests\Integration\Libraries\H5P\Storage; +use App\H5PLibrary; use App\Libraries\ContentAuthorStorage; +use App\Libraries\DataObjects\ContentStorageSettings; use App\Libraries\H5P\Storage\H5PCerpusStorage; use App\Libraries\H5P\Video\NullVideoAdapter; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Psr\Log\NullLogger; use Tests\TestCase; class H5pCerpusStorageTest extends TestCase { - protected function setUp(): void + use RefreshDatabase; + + public function test_correct_url_without_cdn_prefix() { - $this->markTestIncomplete('Fix these later'); + Storage::fake('test'); + Storage::put('test.txt', 'some content'); + + $cerpusStorage = new H5pCerpusStorage( + new ContentAuthorStorage(''), + new NullLogger(), + new NullVideoAdapter(), + ); + $this->assertEquals("http://localhost/content/assets/test.txt", $cerpusStorage->getFileUrl('test.txt')); + Storage::delete('test.txt'); } - public function test_correct_url_without_cdn_prefix() + public function test_correct_url_with_cdn_prefix() { - $disk = Storage::fake('fake'); + Storage::fake('test'); + Storage::put('test.txt', 'some content'); + + $cerpusStorage = new H5pCerpusStorage( + new ContentAuthorStorage('http://not.localhost/prefix/'), + new NullLogger(), + new NullVideoAdapter(), + ); - $disk->put('test.txt', 'some content'); + $this->assertEquals("http://not.localhost/prefix/test.txt", $cerpusStorage->getFileUrl('test.txt')); + Storage::delete('test.txt'); + } + public function test_correct_url_when_file_not_found() + { + Storage::fake('test'); $cerpusStorage = new H5pCerpusStorage( new ContentAuthorStorage(''), new NullLogger(), new NullVideoAdapter(), ); - $this->assertEquals("/test.txt", $cerpusStorage->getFileUrl('test.txt')); + $this->assertEquals("", $cerpusStorage->getFileUrl('test.txt')); } - public function test_correct_url_with_cdn_prefix() + /** @dataProvider provider_getUpdateScript */ + public function test_getUpgradeScript($data): void { - $disk = Storage::fake('fake'); + H5PLibrary::factory()->create(array_merge($data, ['major_version' => $data['major_version'] - 1])); + H5PLibrary::factory()->create(array_merge($data, ['minor_version' => $data['minor_version'] - 1])); - $disk->put('test.txt', 'some content'); + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create($data); + + H5PLibrary::factory()->create(array_merge($data, ['minor_version' => $data['minor_version'] + 1])); + H5PLibrary::factory()->create(array_merge($data, ['major_version' => $data['major_version'] + 1])); + + $folder = $library->getLibraryString(true); + $expected = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $folder); + + Storage::fake('test'); + Storage::put($expected, 'Empty'); $cerpusStorage = new H5pCerpusStorage( new ContentAuthorStorage(''), @@ -43,12 +81,35 @@ public function test_correct_url_with_cdn_prefix() new NullVideoAdapter(), ); - $this->assertEquals("http://test/aaa/test.txt", $cerpusStorage->getFileUrl('test.txt')); + $path = $cerpusStorage->getUpgradeScript($data['name'], $data['major_version'], $data['minor_version']); + $this->assertSame("/$expected", $path); + Storage::delete($expected); } - public function test_correct_url_when_file_not_found() + public function provider_getUpdateScript(): \Generator { - $disk = Storage::fake('fake'); + yield [[ + 'name' => 'H5P.Foobar', + 'major_version' => 2, + 'minor_version' => 3, + 'patch_version' => 4, + ]]; + yield [[ + 'name' => 'H5P.Foobar', + 'major_version' => 2, + 'minor_version' => 3, + 'patch_version' => 4, + 'patch_version_in_folder_name' => true, + ]]; + } + + /** @dataProvider provider_deleteLibrary */ + public function test_deleteLibrary($data): void + { + $path = sprintf(ContentStorageSettings::LIBRARY_PATH, H5PLibrary::libraryToFolderName($data)); + + Storage::fake('test'); + Storage::createDirectory($path); $cerpusStorage = new H5pCerpusStorage( new ContentAuthorStorage(''), @@ -56,7 +117,26 @@ public function test_correct_url_when_file_not_found() new NullVideoAdapter(), ); + $this->assertTrue(Storage::exists($path)); + $ret = $cerpusStorage->deleteLibrary($data); + $this->assertTrue($ret); + $this->assertFalse(Storage::exists($path)); + } - $this->assertEquals("", $cerpusStorage->getFileUrl('test.txt')); + public function provider_deleteLibrary(): \Generator + { + yield [[ + 'name' => 'H5P.Foobar', + 'majorVersion' => 2, + 'minorVersion' => 3, + 'patchVersion' => 4, + ]]; + yield [[ + 'name' => 'H5P.Foobar', + 'majorVersion' => 2, + 'minorVersion' => 3, + 'patchVersion' => 4, + 'patchVersionInFolderName' => true, + ]]; } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php index 8c892086c6..5a05173b60 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php @@ -22,18 +22,63 @@ public function test_getLibraryString($isFolder, $usePatch, $hasPatch, $expected public function provider_libraryString(): \Generator { - yield [true, null, false, 'H5P.Foobar-1.2']; - yield [true, null, true, 'H5P.Foobar-1.2.3']; - yield [true, true, false, 'H5P.Foobar-1.2.3']; - yield [true, true, true, 'H5P.Foobar-1.2.3']; - yield [true, false, false, 'H5P.Foobar-1.2']; - yield [true, false, true, 'H5P.Foobar-1.2']; - - yield [false, null, false, 'H5P.Foobar 1.2']; - yield [false, null, true, 'H5P.Foobar 1.2.3']; - yield [false, true, false, 'H5P.Foobar 1.2.3']; - yield [false, true, true, 'H5P.Foobar 1.2.3']; - yield [false, false, false, 'H5P.Foobar 1.2']; - yield [false, false, true, 'H5P.Foobar 1.2']; + yield 'Folder 1' => [true, null, false, 'H5P.Foobar-1.2']; + yield 'Folder 2' => [true, null, true, 'H5P.Foobar-1.2.3']; + yield 'Folder 3' => [true, true, false, 'H5P.Foobar-1.2.3']; + yield 'Folder 4' => [true, true, true, 'H5P.Foobar-1.2.3']; + yield 'Folder 5' => [true, false, false, 'H5P.Foobar-1.2']; + yield 'Folder 6' => [true, false, true, 'H5P.Foobar-1.2']; + + yield 'Name 1' => [false, null, false, 'H5P.Foobar 1.2']; + yield 'Name 2' => [false, null, true, 'H5P.Foobar 1.2.3']; + yield 'Name 3' => [false, true, false, 'H5P.Foobar 1.2.3']; + yield 'Name 4' => [false, true, true, 'H5P.Foobar 1.2.3']; + yield 'Name 5' => [false, false, false, 'H5P.Foobar 1.2']; + yield 'Name 6' => [false, false, true, 'H5P.Foobar 1.2']; + } + + /** @dataProvider provider_libraryToFolderName */ + public function test_libraryToFolderName($data, $usePatch, $expected): void + { + $this->assertSame($expected, H5PLibrary::libraryToFolderName($data, $usePatch)); + } + + public function provider_libraryToFolderName(): \Generator + { + yield 0 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], false, 'H5P.Foobar-2.1']; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], true, 'H5P.Foobar-2.1.4']; + yield 2 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => 0], null, 'H5P.Foobar-2.1']; + yield 3 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => 1], null, 'H5P.Foobar-2.1.4']; + yield 4 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4], true, 'H5P.Foobar-2.1.4']; + yield 5 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4], null, 'H5P.Foobar-2.1']; + yield 6 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => true], false, 'H5P.Foobar-2.1']; + } + + /** @dataProvider provider_libraryToFolderNameExceptions */ + public function test_libraryToFolderNameExceptions($data, $usePatch): void + { + $this->expectException(\InvalidArgumentException::class); + H5PLibrary::libraryToFolderName($data, $usePatch); + } + + public function provider_libraryToFolderNameExceptions(): \Generator + { + yield 0 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => true], true]; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => false], true]; + yield 2 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => true], null]; + } + + /** @dataProvider provider_libraryToString */ + public function test_libraryToString($data, $usePatch, $expected): void + { + $this->assertSame($expected, H5PLibrary::libraryToString($data, $usePatch)); + } + + public function provider_libraryToString(): \Generator + { + yield 0 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], null, 'H5P.Foobar 2.1.4']; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], null, 'H5P.Foobar 2.1']; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], false, 'H5P.Foobar 2.1']; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], true, 'H5P.Foobar 2.1.4']; } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php index 4809eb5781..22a937f5af 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php @@ -7,6 +7,7 @@ use App\H5PLibrary; use App\H5PLibraryLibrary; use App\Libraries\H5P\Framework; +use App\Libraries\H5P\Storage\H5PCerpusStorage; use ArrayObject; use Exception; use GuzzleHttp\Client; @@ -279,4 +280,50 @@ public function testLoadLibrary(): void $this->assertSame($editDep->name, $library['editorDependencies'][0]['machineName']); $this->assertSame($editDep->patch_version_in_folder_name, $library['editorDependencies'][0]['patchVersionInFolderName']); } + + public function test_deleteLibrary(): void + { + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create(); + + $storage = $this->createMock(H5PCerpusStorage::class); + $this->instance(H5PCerpusStorage::class, $storage); + $storage + ->expects($this->once()) + ->method('deleteLibrary') + ->with([ + 'machineName' => $library->name, + 'majorVersion' => $library->major_version, + 'minorVersion' => $library->minor_version, + 'patchVersion' => $library->patch_version, + 'patchVersionInFolderName' => $library->patch_version_in_folder_name, + ]); + + $this->assertDatabaseHas('h5p_libraries', ['id' => $library->id]); + $this->framework->deleteLibrary($library); + $this->assertDatabaseMissing('h5p_libraries', ['id' => $library->id]); + } + + public function test_deleteLibrary_withPatch(): void + { + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => true]); + + $storage = $this->createMock(H5PCerpusStorage::class); + $this->instance(H5PCerpusStorage::class, $storage); + $storage + ->expects($this->once()) + ->method('deleteLibrary') + ->with([ + 'machineName' => $library->name, + 'majorVersion' => $library->major_version, + 'minorVersion' => $library->minor_version, + 'patchVersion' => $library->patch_version, + 'patchVersionInFolderName' => $library->patch_version_in_folder_name, + ]); + + $this->assertDatabaseHas('h5p_libraries', ['id' => $library->id]); + $this->framework->deleteLibrary($library); + $this->assertDatabaseMissing('h5p_libraries', ['id' => $library->id]); + } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php new file mode 100644 index 0000000000..5649a99e37 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php @@ -0,0 +1,113 @@ +make(); + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); + /** @var H5PContent $h5pcontent */ + $h5pcontent = H5PContent::factory()->create(['user_id' => $user->auth_id, 'library_id' => $library->id]); + + $core = $this->createMock(\H5PCore::class); + $this->instance(\H5PCore::class, $core); + $core->expects($this->once())->method('getLocalization')->willReturn([]); + $core->expects($this->exactly(2))->method('loadContentDependencies')->willReturn([]); + $core->expects($this->once())->method('getDependenciesFiles')->willReturn([]); + + $storage = $this->createMock(H5PCerpusStorage::class); + $this->instance(H5PCerpusStorage::class, $storage); + $core->fs = $storage; + $storage->expects($this->once())->method('getDisplayPath')->with(false)->willReturn('/yupp'); + + $resourceApi = $this->createMock(ResourceApiService::class); + $this->instance(ResourceApiService::class, $resourceApi); + $resourceApi + ->expects($this->exactly(2)) + ->method('getResourceFromExternalReference') + ->with('contentauthor', $h5pcontent->id) + ->willReturn( + new Resource( + 42, + 24, + '', + null, + '', + '', + 'Test resource' + ) + ); + + $content = [ + 'id' => $h5pcontent->id, + 'contentId' => $h5pcontent->id, + 'params' => $h5pcontent->parameters, + 'filtered' => $h5pcontent->filtered, + 'embedType' => $h5pcontent->embed_type, + 'title' => $h5pcontent->title, + 'disable' => $h5pcontent->disable, + 'user_id' => $h5pcontent->user_id, + 'slug' => $h5pcontent->slug, + 'libraryId' => $h5pcontent->library->id, + 'libraryName' => $h5pcontent->library->name, + 'libraryMajorVersion' => $h5pcontent->library->major_version, + 'libraryMinorVersion' => $h5pcontent->library->minor_version, + 'libraryPatchVersion' => $h5pcontent->library->patch_version, + 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(false, false), + 'libraryEmbedTypes' => $h5pcontent->library->embed_types, + 'libraryFullscreen' => $h5pcontent->library->fullscreen, + 'language' => $h5pcontent->metadata->default_language ?? null, + 'max_score' => $h5pcontent->max_score, + 'created_at' => $h5pcontent->created_at, + 'updated_at' => $h5pcontent->updated_at, + 'library' => [ + 'id' => $library->id, + 'name' => $library->name, + 'fullscreen' => $library->fullscreen, + ], + 'metadata' => [], + ]; + + $viewConfig = app(ViewConfig::class) + ->setId($h5pcontent->id) + ->setUserId($user->auth_id) + ->setUserName($user->name) + ->setEmail($user->email) + ->setPreview(false) + ->setContext('nope'); + + $viewConfig->setContent($content); + + $config = $viewConfig->getConfig(); + + $this->assertSame('/yupp', $config->url); + $this->assertSame($user->email, $config->user->mail); + $viewContents = $config->contents->{'cid-' . $h5pcontent->id}; + $this->assertSame('H5P.Foobar 1.2', $viewContents->library); + $this->assertSame($h5pcontent->title, $viewContents->title); + } + + public function provider_testConfig(): \Generator + { + yield [false]; + yield [true]; + } +} From 32b0e80ff4b4c10e3f203a3aa137f5581e4aa7bd Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Tue, 9 May 2023 12:44:14 +0200 Subject: [PATCH 07/22] PHPStan fixes and baseline update --- .../apis/contentauthor/phpstan-baseline.neon | 20 +++++++++++++++++++ .../Libraries/H5P/AjaxRequestTest.php | 4 ++++ .../Libraries/H5P/EditorStorageTest.php | 2 ++ .../Unit/Libraries/H5P/ViewConfigTest.php | 6 ++++-- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/sourcecode/apis/contentauthor/phpstan-baseline.neon b/sourcecode/apis/contentauthor/phpstan-baseline.neon index d0d0aa956d..0b95eea921 100644 --- a/sourcecode/apis/contentauthor/phpstan-baseline.neon +++ b/sourcecode/apis/contentauthor/phpstan-baseline.neon @@ -1810,3 +1810,23 @@ parameters: message: "#^Method Tests\\\\TestCase\\:\\:setUpTraits\\(\\) should return array but return statement is missing\\.$#" count: 1 path: tests/TestCase.php + + - + message: "#^Parameter \\#1 \\$library of method App\\\\Libraries\\\\H5P\\\\Framework\\:\\:deleteLibrary\\(\\) expects stdClass, App\\\\H5PLibrary given\\.$#" + count: 2 + path: tests/Unit/Libraries/H5P/FrameworkTest.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$auth_id\\.$#" + count: 2 + path: tests/Unit/Libraries/H5P/ViewConfigTest.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#" + count: 2 + path: tests/Unit/Libraries/H5P/ViewConfigTest.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$name\\.$#" + count: 1 + path: tests/Unit/Libraries/H5P/ViewConfigTest.php diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php index 57ee5f7011..572226184b 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php @@ -16,9 +16,13 @@ class AjaxRequestTest extends TestCase public function test_libraryRebuild(): void { + /** @var H5PLibrary $library */ $library = H5PLibrary::factory()->create(); + /** @var H5PLibrary $preLib */ $preLib = H5PLibrary::factory()->create(['name' => 'player', 'major_version' => 3, 'minor_version' => 14]); + /** @var H5PLibrary $dynLib */ $dynLib = H5PLibrary::factory()->create(['name' => 'H5P.Dynamic', 'major_version' => 2, 'minor_version' => 42, 'patch_version' => 3, 'patch_version_in_folder_name' => true]); + /** @var H5PLibrary $edLib */ $edLib = H5PLibrary::factory()->create(['name' => 'FontOk', 'major_version' => 1, 'minor_version' => 3]); H5PLibraryLibrary::create(['library_id' => $library->id, 'required_library_id' => $preLib->id, 'dependency_type' => 'preloaded']); diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php index 0dcb9a9274..dfce962b09 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php @@ -36,7 +36,9 @@ public function test_getLibraries_forLibrary(): void public function test_getLibrary_allLibraries(): void { + /** @var H5PLibrary $lib1 */ $lib1 = H5PLibrary::factory()->create(['semantics' => 'something']); + /** @var H5PLibrary $lib2 */ $lib2 = H5PLibrary::factory()->create(['name' => 'H5P.Headphones', 'semantics' => 'something']); $core = $this->createMock(\H5PCore::class); diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php index 5649a99e37..bc3407aa3f 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php @@ -10,11 +10,13 @@ use App\Libraries\H5P\ViewConfig; use App\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Foundation\Testing\WithFaker; use Tests\TestCase; class ViewConfigTest extends TestCase { use RefreshDatabase; + use WithFaker; /** * @dataProvider provider_testConfig @@ -46,8 +48,8 @@ public function testConfig($usePatch): void ->with('contentauthor', $h5pcontent->id) ->willReturn( new Resource( - 42, - 24, + $this->faker->uuid, + $this->faker->uuid, '', null, '', From 28165e290acc57647371b065632d850de0293264 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Tue, 9 May 2023 14:55:41 +0200 Subject: [PATCH 08/22] Fix libraries not able to load resources, fix/add unittest --- .../app/Libraries/H5P/Framework.php | 3 +- .../Unit/Libraries/H5P/FrameworkTest.php | 41 ++++++++++--------- .../Unit/Libraries/H5P/ViewConfigTest.php | 40 ++---------------- 3 files changed, 26 insertions(+), 58 deletions(-) diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php index ff5b9af800..34e30de78e 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php @@ -928,6 +928,7 @@ public function deleteLibrary($library): void throw new TypeError(sprintf('Expected object, %s given', get_debug_type($library))); } + /** @var H5PLibrary $libraryModel */ $libraryModel = H5PLibrary::findOrFail($library->id); $libraryModel->deleteOrFail(); @@ -975,7 +976,7 @@ public function loadContent($id) 'libraryMajorVersion' => $h5pcontent->library->major_version, 'libraryMinorVersion' => $h5pcontent->library->minor_version, 'libraryPatchVersion' => $h5pcontent->library->patch_version, - 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(false, false), + 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(), 'libraryEmbedTypes' => $h5pcontent->library->embed_types, 'libraryFullscreen' => $h5pcontent->library->fullscreen, 'language' => $h5pcontent->metadata->default_language ?? null, diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php index 22a937f5af..ee6e0b6735 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\Libraries\H5P; +use App\H5PContent; use App\H5PLibrary; use App\H5PLibraryLibrary; use App\Libraries\H5P\Framework; @@ -281,10 +282,11 @@ public function testLoadLibrary(): void $this->assertSame($editDep->patch_version_in_folder_name, $library['editorDependencies'][0]['patchVersionInFolderName']); } - public function test_deleteLibrary(): void + /** @dataProvider provider_usePatch */ + public function test_deleteLibrary($usePatch): void { /** @var H5PLibrary $library */ - $library = H5PLibrary::factory()->create(); + $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); $storage = $this->createMock(H5PCerpusStorage::class); $this->instance(H5PCerpusStorage::class, $storage); @@ -304,26 +306,25 @@ public function test_deleteLibrary(): void $this->assertDatabaseMissing('h5p_libraries', ['id' => $library->id]); } - public function test_deleteLibrary_withPatch(): void + /** @dataProvider provider_usePatch */ + public function test_loadContent($usePatch): void { - /** @var H5PLibrary $library */ - $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => true]); + /** @var H5PLibrary $h5pLibrary */ + $h5pLibrary = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); + /** @var H5PContent $h5pContent */ + $h5pContent = H5PContent::factory()->create(['library_id' => $h5pLibrary->id]); - $storage = $this->createMock(H5PCerpusStorage::class); - $this->instance(H5PCerpusStorage::class, $storage); - $storage - ->expects($this->once()) - ->method('deleteLibrary') - ->with([ - 'machineName' => $library->name, - 'majorVersion' => $library->major_version, - 'minorVersion' => $library->minor_version, - 'patchVersion' => $library->patch_version, - 'patchVersionInFolderName' => $library->patch_version_in_folder_name, - ]); + $content = $this->framework->loadContent($h5pContent->id); - $this->assertDatabaseHas('h5p_libraries', ['id' => $library->id]); - $this->framework->deleteLibrary($library); - $this->assertDatabaseMissing('h5p_libraries', ['id' => $library->id]); + $this->assertSame($h5pContent->id, $content['id']); + $this->assertSame($h5pContent->id, $content['contentId']); + $this->assertSame($h5pLibrary->id, $content['libraryId']); + $this->assertSame($h5pLibrary->getLibraryString(), $content['libraryFullVersionName']); + } + + public function provider_usePatch(): \Generator + { + yield [false]; + yield [true]; } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php index bc3407aa3f..3e8517ca4b 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php @@ -29,15 +29,8 @@ public function testConfig($usePatch): void /** @var H5PContent $h5pcontent */ $h5pcontent = H5PContent::factory()->create(['user_id' => $user->auth_id, 'library_id' => $library->id]); - $core = $this->createMock(\H5PCore::class); - $this->instance(\H5PCore::class, $core); - $core->expects($this->once())->method('getLocalization')->willReturn([]); - $core->expects($this->exactly(2))->method('loadContentDependencies')->willReturn([]); - $core->expects($this->once())->method('getDependenciesFiles')->willReturn([]); - $storage = $this->createMock(H5PCerpusStorage::class); $this->instance(H5PCerpusStorage::class, $storage); - $core->fs = $storage; $storage->expects($this->once())->method('getDisplayPath')->with(false)->willReturn('/yupp'); $resourceApi = $this->createMock(ResourceApiService::class); @@ -58,35 +51,8 @@ public function testConfig($usePatch): void ) ); - $content = [ - 'id' => $h5pcontent->id, - 'contentId' => $h5pcontent->id, - 'params' => $h5pcontent->parameters, - 'filtered' => $h5pcontent->filtered, - 'embedType' => $h5pcontent->embed_type, - 'title' => $h5pcontent->title, - 'disable' => $h5pcontent->disable, - 'user_id' => $h5pcontent->user_id, - 'slug' => $h5pcontent->slug, - 'libraryId' => $h5pcontent->library->id, - 'libraryName' => $h5pcontent->library->name, - 'libraryMajorVersion' => $h5pcontent->library->major_version, - 'libraryMinorVersion' => $h5pcontent->library->minor_version, - 'libraryPatchVersion' => $h5pcontent->library->patch_version, - 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(false, false), - 'libraryEmbedTypes' => $h5pcontent->library->embed_types, - 'libraryFullscreen' => $h5pcontent->library->fullscreen, - 'language' => $h5pcontent->metadata->default_language ?? null, - 'max_score' => $h5pcontent->max_score, - 'created_at' => $h5pcontent->created_at, - 'updated_at' => $h5pcontent->updated_at, - 'library' => [ - 'id' => $library->id, - 'name' => $library->name, - 'fullscreen' => $library->fullscreen, - ], - 'metadata' => [], - ]; + $core = app(\H5PCore::class); + $content = $core->loadContent($h5pcontent->id); $viewConfig = app(ViewConfig::class) ->setId($h5pcontent->id) @@ -103,7 +69,7 @@ public function testConfig($usePatch): void $this->assertSame('/yupp', $config->url); $this->assertSame($user->email, $config->user->mail); $viewContents = $config->contents->{'cid-' . $h5pcontent->id}; - $this->assertSame('H5P.Foobar 1.2', $viewContents->library); + $this->assertSame($library->getLibraryString(), $viewContents->library); $this->assertSame($h5pcontent->title, $viewContents->title); } From dcf5394a43915e6c2001ab086beb951e2a4e4fdc Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Tue, 9 May 2023 15:03:27 +0200 Subject: [PATCH 09/22] PHPStan baseline update --- sourcecode/apis/contentauthor/phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sourcecode/apis/contentauthor/phpstan-baseline.neon b/sourcecode/apis/contentauthor/phpstan-baseline.neon index 0b95eea921..172059df66 100644 --- a/sourcecode/apis/contentauthor/phpstan-baseline.neon +++ b/sourcecode/apis/contentauthor/phpstan-baseline.neon @@ -1813,7 +1813,7 @@ parameters: - message: "#^Parameter \\#1 \\$library of method App\\\\Libraries\\\\H5P\\\\Framework\\:\\:deleteLibrary\\(\\) expects stdClass, App\\\\H5PLibrary given\\.$#" - count: 2 + count: 1 path: tests/Unit/Libraries/H5P/FrameworkTest.php - From a179651d88c2d1fc0d88ddc7a33d36a2325261da Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Wed, 10 May 2023 13:17:25 +0200 Subject: [PATCH 10/22] Unittest fixes and adjustments --- .../Libraries/H5P/EditorStorageTest.php | 6 +- .../H5P/Storage/H5pCerpusStorageTest.php | 84 +++++++++---------- .../tests/Unit/H5PContentTest.php | 38 +++------ .../tests/Unit/H5PLibraryTest.php | 4 +- 4 files changed, 58 insertions(+), 74 deletions(-) diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php index dfce962b09..20f8141c8b 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php @@ -17,9 +17,9 @@ public function test_getLibraries_forLibrary(): void $library = H5PLibrary::factory()->create(['semantics' => 'something']); $libraries = (object) [ 'uberName' => $library->getLibraryString(false, false), - 'name' => 'H5P.Foobar', - 'majorVersion' => 1, - 'minorVersion' => 2, + 'name' => $library->name, + 'majorVersion' => $library->major_version, + 'minorVersion' => $library->minor_version, ]; $core = $this->createMock(\H5PCore::class); diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php index a697f07943..dd4d753e92 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php @@ -57,17 +57,37 @@ public function test_correct_url_when_file_not_found() $this->assertEquals("", $cerpusStorage->getFileUrl('test.txt')); } - /** @dataProvider provider_getUpdateScript */ - public function test_getUpgradeScript($data): void + /** @dataProvider provider_usePatch */ + public function test_getUpgradeScript($usePatch): void { - H5PLibrary::factory()->create(array_merge($data, ['major_version' => $data['major_version'] - 1])); - H5PLibrary::factory()->create(array_merge($data, ['minor_version' => $data['minor_version'] - 1])); + H5PLibrary::factory()->create([ + 'major_version' => 1, + 'minor_version' => 3, + 'patch_version' => 4, + 'patch_version_in_folder_name' => $usePatch + ]); + H5PLibrary::factory()->create([ + 'major_version' => 2, + 'minor_version' => 2, + 'patch_version' => 4, + 'patch_version_in_folder_name' => $usePatch + ]); /** @var H5PLibrary $library */ - $library = H5PLibrary::factory()->create($data); + $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); - H5PLibrary::factory()->create(array_merge($data, ['minor_version' => $data['minor_version'] + 1])); - H5PLibrary::factory()->create(array_merge($data, ['major_version' => $data['major_version'] + 1])); + H5PLibrary::factory()->create([ + 'major_version' => 2, + 'minor_version' => 4, + 'patch_version' => 4, + 'patch_version_in_folder_name' => $usePatch + ]); + H5PLibrary::factory()->create([ + 'major_version' => 3, + 'minor_version' => 3, + 'patch_version' => 4, + 'patch_version_in_folder_name' => $usePatch + ]); $folder = $library->getLibraryString(true); $expected = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $folder); @@ -81,31 +101,27 @@ public function test_getUpgradeScript($data): void new NullVideoAdapter(), ); - $path = $cerpusStorage->getUpgradeScript($data['name'], $data['major_version'], $data['minor_version']); + $path = $cerpusStorage->getUpgradeScript($library->name, $library->major_version, $library->minor_version); $this->assertSame("/$expected", $path); Storage::delete($expected); } - public function provider_getUpdateScript(): \Generator + public function provider_usePatch(): \Generator { - yield [[ - 'name' => 'H5P.Foobar', - 'major_version' => 2, - 'minor_version' => 3, - 'patch_version' => 4, - ]]; - yield [[ - 'name' => 'H5P.Foobar', - 'major_version' => 2, - 'minor_version' => 3, - 'patch_version' => 4, - 'patch_version_in_folder_name' => true, - ]]; + yield [false]; + yield [true]; } - /** @dataProvider provider_deleteLibrary */ - public function test_deleteLibrary($data): void + /** @dataProvider provider_usePatch */ + public function test_deleteLibrary($usePatch): void { + $data = [ + 'name' => 'H5P.Foobar', + 'majorVersion' => 2, + 'minorVersion' => 3, + 'patchVersion' => 4, + 'patchVersionInFolderName' => $usePatch, + ]; $path = sprintf(ContentStorageSettings::LIBRARY_PATH, H5PLibrary::libraryToFolderName($data)); Storage::fake('test'); @@ -118,25 +134,7 @@ public function test_deleteLibrary($data): void ); $this->assertTrue(Storage::exists($path)); - $ret = $cerpusStorage->deleteLibrary($data); - $this->assertTrue($ret); + $this->assertTrue($cerpusStorage->deleteLibrary($data)); $this->assertFalse(Storage::exists($path)); } - - public function provider_deleteLibrary(): \Generator - { - yield [[ - 'name' => 'H5P.Foobar', - 'majorVersion' => 2, - 'minorVersion' => 3, - 'patchVersion' => 4, - ]]; - yield [[ - 'name' => 'H5P.Foobar', - 'majorVersion' => 2, - 'minorVersion' => 3, - 'patchVersion' => 4, - 'patchVersionInFolderName' => true, - ]]; - } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php index 6aea4592a8..690040e209 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php @@ -12,33 +12,13 @@ class H5PContentTest extends TestCase { use RefreshDatabase; - public function test_getContentTypeInfo_WithIcon(): void - { - /** @var H5PLibrary $library */ - $library = H5PLibrary::factory()->create(['has_icon' => 1]); - - $frameWork = $this->createMock(\H5PFrameworkInterface::class); - $this->instance(\H5PFrameworkInterface::class, $frameWork); - - $frameWork - ->expects($this->once()) - ->method('getLibraryFileUrl') - ->with('H5P.Foobar-1.2', 'icon.svg') - ->willReturn('assets/H5P.Foobar-1.2/icon.svg'); - - $result = H5PContent::getContentTypeInfo('H5P.Foobar'); - $this->assertInstanceOf(ContentTypeDataObject::class, $result); - $this->assertSame('H5P.Foobar', $result->contentType); - $this->assertSame($library->title, $result->title); - $this->assertSame('assets/H5P.Foobar-1.2/icon.svg', $result->icon); - } - - public function test_getContentTypeInfo_PatchName(): void + /** @dataProvider provider_getContentTypeInfo */ + public function test_getContentTypeInfo_WithIcon($usePatch, $expectedPath): void { /** @var H5PLibrary $library */ $library = H5PLibrary::factory()->create([ - 'patch_version_in_folder_name' => true, 'has_icon' => 1, + 'patch_version_in_folder_name' => $usePatch, ]); $frameWork = $this->createMock(\H5PFrameworkInterface::class); @@ -47,14 +27,20 @@ public function test_getContentTypeInfo_PatchName(): void $frameWork ->expects($this->once()) ->method('getLibraryFileUrl') - ->with('H5P.Foobar-1.2.3', 'icon.svg') - ->willReturn('assets/H5P.Foobar-1.2.3/icon.svg'); + ->with($expectedPath, 'icon.svg') + ->willReturn("assets/$expectedPath/icon.svg"); $result = H5PContent::getContentTypeInfo('H5P.Foobar'); $this->assertInstanceOf(ContentTypeDataObject::class, $result); $this->assertSame('H5P.Foobar', $result->contentType); $this->assertSame($library->title, $result->title); - $this->assertSame('assets/H5P.Foobar-1.2.3/icon.svg', $result->icon); + $this->assertSame("assets/$expectedPath/icon.svg", $result->icon); + } + + public function provider_getContentTypeInfo(): \Generator + { + yield [false, 'H5P.Foobar-1.2']; + yield [true, 'H5P.Foobar-1.2.3']; } public function test_getContentTypeInfo_NoIcon(): void diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php index 5a05173b60..084b599e96 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php @@ -78,7 +78,7 @@ public function provider_libraryToString(): \Generator { yield 0 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], null, 'H5P.Foobar 2.1.4']; yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], null, 'H5P.Foobar 2.1']; - yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], false, 'H5P.Foobar 2.1']; - yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], true, 'H5P.Foobar 2.1.4']; + yield 2 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], false, 'H5P.Foobar 2.1']; + yield 3 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], true, 'H5P.Foobar 2.1.4']; } } From 0b9377ed37ac97d1e615c97cae9b6b214b61a2ad Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Tue, 23 May 2023 10:50:24 +0200 Subject: [PATCH 11/22] Extract folder name functionality from getLibraryString() into getFolderName(). Some integration tests was in unit test folder --- .../app/Console/Commands/PublishPresave.php | 2 +- .../apis/contentauthor/app/H5PContent.php | 2 +- .../apis/contentauthor/app/H5PLibrary.php | 44 ++-- .../Admin/AdminH5PDetailsController.php | 2 +- .../app/Http/Controllers/H5PController.php | 4 +- .../app/Libraries/H5P/AjaxRequest.php | 14 +- .../app/Libraries/H5P/EditorStorage.php | 4 +- .../app/Libraries/H5P/H5PExport.php | 2 +- .../H5P/Storage/H5PCerpusStorage.php | 2 +- .../apis/contentauthor/phpstan-baseline.neon | 40 ++-- .../{Unit => Integration}/H5PContentTest.php | 2 +- .../H5PLibrariesHubCacheTest.php | 2 +- .../H5P/API/H5PImportControllerTest.php | 6 +- .../Integration/Libraries/H5P/CRUTest.php | 4 +- .../Libraries/H5P/EditorStorageTest.php | 2 +- .../Libraries/H5P/FrameworkTest.php | 201 ++++++++++++++++++ .../H5P/Storage/H5pCerpusStorageTest.php | 2 +- .../Libraries/H5P/ViewConfigTest.php | 2 +- .../tests/Unit/H5PLibraryTest.php | 48 +++-- .../AdminH5PDetailsControllerTest.php | 11 +- .../Unit/Libraries/H5P/FrameworkTest.php | 157 +------------- 21 files changed, 312 insertions(+), 241 deletions(-) rename sourcecode/apis/contentauthor/tests/{Unit => Integration}/H5PContentTest.php (98%) rename sourcecode/apis/contentauthor/tests/{Unit => Integration}/H5PLibrariesHubCacheTest.php (96%) create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php rename sourcecode/apis/contentauthor/tests/{Unit => Integration}/Libraries/H5P/ViewConfigTest.php (98%) diff --git a/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php b/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php index d54850953c..bc2383837e 100644 --- a/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php +++ b/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php @@ -57,7 +57,7 @@ public function handle(): void private static function getDestination(H5PLibrary $library): string { - $directory = $library->getLibraryString(true); + $directory = $library->getFolderName(); return "libraries/$directory/presave.js"; } diff --git a/sourcecode/apis/contentauthor/app/H5PContent.php b/sourcecode/apis/contentauthor/app/H5PContent.php index d19346a9eb..296ffe2544 100644 --- a/sourcecode/apis/contentauthor/app/H5PContent.php +++ b/sourcecode/apis/contentauthor/app/H5PContent.php @@ -325,7 +325,7 @@ public static function getContentTypeInfo(string $contentType): ?ContentTypeData if ($library->has_icon) { $h5pFramework = app(H5PFrameworkInterface::class); - $library_folder = $library->getLibraryString(true); + $library_folder = $library->getFolderName(); $icon_path = $h5pFramework->getLibraryFileUrl($library_folder, 'icon.svg'); if (!empty($icon_path)) { diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index 4757bbb04b..f4bac2cc49 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -122,47 +122,47 @@ public function getVersions($asModels = false) } /** - * @param bool $folderName True to get name suitable for folder - * @param bool|null $fullVersion Null to use patchVersionInFolderName, true/false to override + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force */ - public function getLibraryString(bool $folderName = false, ?bool $fullVersion = null): string + public function getLibraryString(?bool $withPatchVersion = null): string { - return self::getLibraryName([ - 'machineName' => $this->name, - 'majorVersion' => $this->major_version, - 'minorVersion' => $this->minor_version, - 'patchVersion' => $this->patch_version, - 'patchVersionInFolderName' => $this->patch_version_in_folder_name, + return self::getLibraryName($this->getLibraryH5PFriendly(), false, $withPatchVersion); + } - ], $folderName, $fullVersion); + /** + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force + */ + public function getFolderName(?bool $withPatchVersion = null): string + { + return self::getLibraryName($this->getLibraryH5PFriendly(), true, $withPatchVersion); } /** - * @param array $libraryData Key names: machineName/name, majorVersion, minorVersion, patchVersion, patchVersionInFolderName - * @param bool|null $fullVersion Null to use patchVersionInFolderName, true/false to override + * @param array{machineName?: string, name?: string, majorVersion: int, minorVersion: int, patchVersion: int, patchVersionInFolderName: bool} $libraryData + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force * @throws \InvalidArgumentException If requesting full version without patchVersion present in data */ - public static function libraryToFolderName(array $libraryData, ?bool $fullVersion = null): string + public static function libraryToFolderName(array $libraryData, ?bool $withPatchVersion = null): string { - return self::getLibraryName($libraryData, true, $fullVersion); + return self::getLibraryName($libraryData, true, $withPatchVersion); } /** - * @param array $libraryData Key names: machineName/name, majorVersion, minorVersion, patchVersion, patchVersionInFolderName - * @param bool|null $fullVersion Null to use patchVersionInFolderName, true/false to override + * @param array{machineName?: string, name?: string, majorVersion: int, minorVersion: int, patchVersion: int, patchVersionInFolderName: bool} $libraryData + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force * @throws \InvalidArgumentException If requesting full version without patchVersion present in data */ - public static function libraryToString(array $libraryData, ?bool $fullVersion = null): string + public static function libraryToString(array $libraryData, ?bool $withPatchVersion = null): string { - return self::getLibraryName($libraryData, false, $fullVersion); + return self::getLibraryName($libraryData, false, $withPatchVersion); } /** * @throws \InvalidArgumentException If requesting full version without patchVersion present in data */ - private static function getLibraryName(array $libraryData, bool $asFolder, ?bool $fullVersion): string + private static function getLibraryName(array $libraryData, bool $asFolder, ?bool $withPatchVersion): string { - $usePatch = $fullVersion === true || ($fullVersion === null && array_key_exists('patchVersionInFolderName', $libraryData) && $libraryData['patchVersionInFolderName']); + $usePatch = $withPatchVersion === true || ($withPatchVersion === null && array_key_exists('patchVersionInFolderName', $libraryData) && $libraryData['patchVersionInFolderName']); if ($usePatch && !isset($libraryData['patchVersion'])) { throw new \InvalidArgumentException('Full version name requested but patch version missing'); } @@ -182,7 +182,7 @@ private static function getLibraryName(array $libraryData, bool $asFolder, ?bool ); } - public function getLibraryH5PFriendly($machineName = 'name') + public function getLibraryH5PFriendly($machineName = 'name'): array { return [ 'machineName' => $this->$machineName, @@ -286,7 +286,7 @@ public function getAddons() public function supportsMaxScore(): bool { - $libraryLocation = sprintf('libraries/%s/presave.js', self::getLibraryString(true)); + $libraryLocation = sprintf('libraries/%s/presave.js', self::getFolderName()); if (Storage::disk()->exists($libraryLocation)) { return true; } diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php index 174de114b1..c7bbe3cdb3 100644 --- a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php +++ b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php @@ -31,7 +31,7 @@ public function __construct( public function checkLibrary(H5PLibrary $library): View { - $h5pDataFolderName = $library->getLibraryString(true); + $h5pDataFolderName = $library->getFolderName(); $tmpLibrariesRelative = 'libraries'; $tmpLibraryRelative = 'libraries/' . $h5pDataFolderName; // Download files from bucket to tmp folder diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php index aab2e946eb..2ab5913ac4 100644 --- a/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php +++ b/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php @@ -175,7 +175,7 @@ public function create(Request $request, H5PCore $core, $contenttype = null): Vi ->latestVersion() ->first(); if (!empty($library)) { - $contenttype = $library->getLibraryString(false, false); + $contenttype = $library->getLibraryString(false); } else { $contenttype = false; } @@ -332,7 +332,7 @@ public function edit(Request $request, int $id): View $state = H5PStateDataObject::create($displayOptions + [ 'id' => $h5pContent->id, - 'library' => $library->getLibraryString(false, false), + 'library' => $library->getLibraryString(false), 'libraryid' => $h5pContent->library_id, 'parameters' => $params, 'language_iso_639_3' => $contentLanguage, diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php index 6a33bff4fa..abd9695465 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php @@ -259,8 +259,8 @@ private function libraryRebuild(Request $request): array $libraries = collect(); $this->getLibraryDetails($H5PLibrary, $libraries); - if ($libraries->has($H5PLibrary->getLibraryString(false, false))) { - $libraryData = $libraries->get($H5PLibrary->getLibraryString(false, false)); + if ($libraries->has($H5PLibrary->getLibraryString(false))) { + $libraryData = $libraries->get($H5PLibrary->getLibraryString(false)); if (array_key_exists('semantics', $libraryData)) { $H5PLibrary->semantics = $libraryData['semantics']; $H5PLibrary->save(); @@ -292,7 +292,7 @@ private function getLibraryDetails(H5PLibrary $H5PLibrary, Collection $affectedL { /** @var H5PValidator $validator */ $validator = resolve(H5PValidator::class); - $h5pDataFolderName = $H5PLibrary->getLibraryString(true); + $h5pDataFolderName = $H5PLibrary->getFolderName(); $tmpLibrariesRelative = 'libraries'; $tmpLibraryRelative = 'libraries/' . $h5pDataFolderName; // Download files from bucket to tmp folder @@ -304,18 +304,18 @@ private function getLibraryDetails(H5PLibrary $H5PLibrary, Collection $affectedL ); $tmpLibraries = $this->core->h5pF->getH5pPath($tmpLibrariesRelative); $tmpLibraryFolder = $this->core->h5pF->getH5pPath($tmpLibraryRelative); - $libraryData = $validator->getLibraryData($H5PLibrary->getLibraryString(true), $tmpLibraryFolder, $tmpLibraries); + $libraryData = $validator->getLibraryData($H5PLibrary->getFolderName(), $tmpLibraryFolder, $tmpLibraries); $libraryData['libraryId'] = $H5PLibrary->id; - if (!$affectedLibraries->has($H5PLibrary->getLibraryString(false, false))) { - $affectedLibraries->put($H5PLibrary->getLibraryString(false, false), $libraryData); + if (!$affectedLibraries->has($H5PLibrary->getLibraryString(false))) { + $affectedLibraries->put($H5PLibrary->getLibraryString(false), $libraryData); } foreach (['preloadedDependencies', 'dynamicDependencies', 'editorDependencies'] as $value) { if (!empty($libraryData[$value])) { foreach ($libraryData[$value] as $library) { /** @var H5PLibrary $dependentLibrary */ $dependentLibrary = H5PLibrary::fromLibrary($library)->first(); - if (!$affectedLibraries->has($dependentLibrary->getLibraryString(false, false))) { + if (!$affectedLibraries->has($dependentLibrary->getLibraryString(false))) { $affectedLibraries = $this->getLibraryDetails($dependentLibrary, $affectedLibraries); } } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php index 570e4fbf95..7754d1ea4c 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php @@ -65,7 +65,7 @@ public function getLibraries($libraries = null) ->map(function ($h5pLibrary) { /** @var H5PLibrary $h5pLibrary */ $library = [ - 'uberName' => $h5pLibrary->getLibraryString(false, false), + 'uberName' => $h5pLibrary->getLibraryString(false), 'name' => $h5pLibrary->name, 'majorVersion' => $h5pLibrary->major_version, 'minorVersion' => $h5pLibrary->minor_version, @@ -101,7 +101,7 @@ public function getLibraries($libraries = null) $library->minorVersion = $library->minor_version; // Add new library - $library->uberName = $library->getLibraryString(false, false); + $library->uberName = $library->getLibraryString(false); if ($index > 0) { $library->isOld = true; } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php index 004e51d127..f0da3e42ea 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php @@ -49,7 +49,7 @@ public function generateExport($convertMediaToLocal = false) /** @var \H5PContentValidator $validator */ $validator = resolve(\H5PContentValidator::class); $params = (object)[ - 'library' => $h5PLibrary->getLibraryString(false, false), + 'library' => $h5PLibrary->getLibraryString(false), 'params' => json_decode($this->content->parameters) ]; if (!$params->params) { diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php index 14a2ab4c1b..5d03d467d7 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php @@ -498,7 +498,7 @@ public function getUpgradeScript($machineName, $majorVersion, $minorVersion) { /** @var H5PLibrary $library */ $library = H5PLibrary::fromLibrary([$machineName, $majorVersion, $minorVersion])->latestVersion()->first(); - $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $library->getLibraryString(true)); + $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $library->getFolderName()); return $this->filesystem->exists($path) ? "/$path" : null; } diff --git a/sourcecode/apis/contentauthor/phpstan-baseline.neon b/sourcecode/apis/contentauthor/phpstan-baseline.neon index 7b4946e6ec..4f6960a969 100644 --- a/sourcecode/apis/contentauthor/phpstan-baseline.neon +++ b/sourcecode/apis/contentauthor/phpstan-baseline.neon @@ -1671,6 +1671,11 @@ parameters: count: 1 path: tests/Integration/Libraries/H5P/CRUTest.php + - + message: "#^Parameter \\#1 \\$library of method App\\\\Libraries\\\\H5P\\\\Framework\\:\\:deleteLibrary\\(\\) expects stdClass, App\\\\H5PLibrary given\\.$#" + count: 1 + path: tests/Integration/Libraries/H5P/FrameworkTest.php + - message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$id\\.$#" count: 7 @@ -1746,6 +1751,21 @@ parameters: count: 1 path: tests/Integration/Libraries/H5P/Package/VideoTest.php + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$auth_id\\.$#" + count: 2 + path: tests/Integration/Libraries/H5P/ViewConfigTest.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#" + count: 2 + path: tests/Integration/Libraries/H5P/ViewConfigTest.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$name\\.$#" + count: 1 + path: tests/Integration/Libraries/H5P/ViewConfigTest.php + - message: "#^Parameter \\$share of class App\\\\Libraries\\\\DataObjects\\\\ResourceMetadataDataObject constructor expects string\\|null, true given\\.$#" count: 2 @@ -1815,23 +1835,3 @@ parameters: message: "#^Method Tests\\\\TestCase\\:\\:setUpTraits\\(\\) should return array but return statement is missing\\.$#" count: 1 path: tests/TestCase.php - - - - message: "#^Parameter \\#1 \\$library of method App\\\\Libraries\\\\H5P\\\\Framework\\:\\:deleteLibrary\\(\\) expects stdClass, App\\\\H5PLibrary given\\.$#" - count: 1 - path: tests/Unit/Libraries/H5P/FrameworkTest.php - - - - message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$auth_id\\.$#" - count: 2 - path: tests/Unit/Libraries/H5P/ViewConfigTest.php - - - - message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#" - count: 2 - path: tests/Unit/Libraries/H5P/ViewConfigTest.php - - - - message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$name\\.$#" - count: 1 - path: tests/Unit/Libraries/H5P/ViewConfigTest.php diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php b/sourcecode/apis/contentauthor/tests/Integration/H5PContentTest.php similarity index 98% rename from sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php rename to sourcecode/apis/contentauthor/tests/Integration/H5PContentTest.php index 690040e209..2ae26ac0b9 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/H5PContentTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/H5PContentTest.php @@ -1,6 +1,6 @@ $title, 'library_id' => $library->id, ]); - $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getLibraryString(true)))); + $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getFolderName()))); /** @var H5PContent $h5pContent */ $h5pContent = H5PContent::with('metadata') @@ -174,7 +174,7 @@ public function importH5PWithImage() 'title' => $title, 'library_id' => $library->id, ]); - $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getLibraryString(true)))); + $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getFolderName()))); $h5pContent = H5PContent::with('metadata') ->where('title', $title) @@ -230,7 +230,7 @@ public function importH5PWithMetadata() 'title' => $title, 'library_id' => $library->id, ]); - $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getLibraryString(true)))); + $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getFolderName()))); $h5pContent = H5PContent::with('metadata') ->where('title', $title) diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php index b12bb73841..b6e94c1ec1 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php @@ -514,7 +514,7 @@ public function enabledUserPublish_NotOwner() '_token' => csrf_token(), 'title' => 'New resource', 'action' => 'create', - 'library' => $library->getLibraryString(false, false), + 'library' => $library->getLibraryString(false), 'parameters' => '{"params":{"simpleTest":"SimpleTest"},"metadata":{}}', 'license' => "PRIVATE", 'lti_message_type' => $this->faker->word, @@ -537,7 +537,7 @@ public function enabledUserPublish_NotOwner() ->put(route('h5p.update', $newContent->id), [ '_token' => csrf_token(), 'title' => $newContent->title, - 'library' => $library->getLibraryString(false, false), + 'library' => $library->getLibraryString(false), 'parameters' => '{"params":{"simpleTest":"SimpleTest"},"metadata":{}}', 'license' => "PRIVATE", 'lti_message_type' => $this->faker->word, diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php index 20f8141c8b..3f5bf69cc4 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php @@ -16,7 +16,7 @@ public function test_getLibraries_forLibrary(): void /** @var H5PLibrary $library */ $library = H5PLibrary::factory()->create(['semantics' => 'something']); $libraries = (object) [ - 'uberName' => $library->getLibraryString(false, false), + 'uberName' => $library->getLibraryString(false), 'name' => $library->name, 'majorVersion' => $library->major_version, 'minorVersion' => $library->minor_version, diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php new file mode 100644 index 0000000000..161b13cd13 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php @@ -0,0 +1,201 @@ + */ + private ArrayObject $history; + + private Framework $framework; + + private MockHandler $mockedResponses; + + protected function setUp(): void + { + parent::setUp(); + + $this->history = new ArrayObject(); + $this->mockedResponses = new MockHandler(); + + $handler = HandlerStack::create($this->mockedResponses); + $handler->push(Middleware::history($this->history)); + + $client = new Client(['handler' => $handler]); + + $this->framework = new Framework( + $client, + $this->createMock(PDO::class), + $this->createMock(Filesystem::class), + ); + } + + public function testSaveLibrary(): void + { + $input = [ + 'machineName' => 'H5P.UnitTest', + 'title' => 'Unit Test', + 'majorVersion' => 2, + 'minorVersion' => 4, + 'patchVersion' => 6, + 'runnable' => 1, + 'metadataSettings' => 'Yupp', + 'addTo' => ['machineName' => 'Something'], + 'hasIcon' => 1, + 'embedTypes' => ['E1', 'E2'], + 'preloadedJs' => [ + ['path' => 'PJ1', 'name' => 'PJ1 name', 'machineName' => 'H5P.Pj1'], + ['path' => 'PJ2', 'name' => 'PJ2 name', 'machineName' => 'H5P.Pj2'], + ], + 'preloadedCss' => [ + ['path' => 'PC1', 'name' => 'PC1 name', 'machineName' => 'H5P.Pc1'], + ['path' => 'PC2', 'name' => 'PC2 name', 'machineName' => 'H5P.Pc1'], + ], + 'dropLibraryCss' => [ + ['path' => 'DC1', 'name' => 'DC1 name', 'machineName' => 'H5P.Dc1'], + ['path' => 'DC2', 'name' => 'DC2 name', 'machineName' => 'H5P.Dc2'], + ], + 'language' => [ + 'nb' => 'Norsk Bokmål', + 'nn' => 'Norsk Nynorsk', + ], + ]; + $this->framework->saveLibraryData($input); + + $this->assertDatabaseHas('h5p_libraries', ['id' => $input['libraryId']]); + $this->assertDatabaseHas('h5p_libraries_languages', [ + 'library_id' => $input['libraryId'], + 'language_code' => 'nb', + 'translation' => 'Norsk Bokmål', + ]); + $this->assertDatabaseHas('h5p_libraries_languages', [ + 'library_id' => $input['libraryId'], + 'language_code' => 'nn', + 'translation' => 'Norsk Nynorsk', + ]); + + /** @var H5PLibrary $library */ + $library = H5PLibrary::find($input['libraryId']); + + $this->assertSame('H5P.UnitTest', $library->name); + $this->assertSame('Unit Test', $library->title); + $this->assertSame(2, $library->major_version); + $this->assertSame(4, $library->minor_version); + $this->assertSame(6, $library->patch_version); + $this->assertSame(1, $library->runnable); + $this->assertSame(0, $library->fullscreen); + $this->assertSame('E1, E2', $library->embed_types); + $this->assertSame('PJ1, PJ2', $library->preloaded_js); + $this->assertSame('PC1, PC2', $library->preloaded_css); + $this->assertSame('H5P.Dc1, H5P.Dc2', $library->drop_library_css); + $this->assertSame('', $library->semantics); + $this->assertSame(1, $library->has_icon); + $this->assertSame(true, $library->patch_version_in_folder_name); + } + + public function testLoadLibrary(): void + { + H5PLibrary::factory()->create([ + 'major_version' => 1, + 'minor_version' => 1, + 'patch_version' => 9, + ]); + H5PLibrary::factory()->create([ + 'major_version' => 1, + 'minor_version' => 2, + 'patch_version' => 2, + ]); + /** @var H5PLibrary $editDep */ + $editDep = H5PLibrary::factory()->create([ + 'name' => 'H5PEditor.Foobar', + 'patch_version_in_folder_name' => true, + ]); + /** @var H5PLibrary $saved */ + $saved = H5PLibrary::factory()->create([ + 'patch_version_in_folder_name' => true, + ]); + H5PLibraryLibrary::create([ + 'library_id' => $saved->id, + 'required_library_id' => $editDep->id, + 'dependency_type' => 'editor', + ]); + + $library = $this->framework->loadLibrary('H5P.Foobar', 1, 2); + $this->assertSame($saved->id, $library['libraryId']); + $this->assertSame($saved->name, $library['machineName']); + $this->assertSame($saved->major_version, $library['majorVersion']); + $this->assertSame($saved->minor_version, $library['minorVersion']); + $this->assertSame($saved->patch_version, $library['patchVersion']); + $this->assertSame($saved->patch_version_in_folder_name, $library['patchVersionInFolderName']); + + $this->assertSame($editDep->name, $library['editorDependencies'][0]['machineName']); + $this->assertSame($editDep->patch_version_in_folder_name, $library['editorDependencies'][0]['patchVersionInFolderName']); + } + + /** @dataProvider provider_usePatch */ + public function test_deleteLibrary($usePatch): void + { + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); + + $storage = $this->createMock(H5PCerpusStorage::class); + $this->instance(H5PCerpusStorage::class, $storage); + $storage + ->expects($this->once()) + ->method('deleteLibrary') + ->with([ + 'machineName' => $library->name, + 'majorVersion' => $library->major_version, + 'minorVersion' => $library->minor_version, + 'patchVersion' => $library->patch_version, + 'patchVersionInFolderName' => $library->patch_version_in_folder_name, + ]); + + $this->assertDatabaseHas('h5p_libraries', ['id' => $library->id]); + $this->framework->deleteLibrary($library); + $this->assertDatabaseMissing('h5p_libraries', ['id' => $library->id]); + } + + /** @dataProvider provider_usePatch */ + public function test_loadContent($usePatch): void + { + /** @var H5PLibrary $h5pLibrary */ + $h5pLibrary = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); + /** @var H5PContent $h5pContent */ + $h5pContent = H5PContent::factory()->create(['library_id' => $h5pLibrary->id]); + + $content = $this->framework->loadContent($h5pContent->id); + + $this->assertSame($h5pContent->id, $content['id']); + $this->assertSame($h5pContent->id, $content['contentId']); + $this->assertSame($h5pLibrary->id, $content['libraryId']); + $this->assertSame($h5pLibrary->getLibraryString(), $content['libraryFullVersionName']); + } + + public function provider_usePatch(): \Generator + { + yield [false]; + yield [true]; + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php index dd4d753e92..fc24dd6dc7 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php @@ -89,7 +89,7 @@ public function test_getUpgradeScript($usePatch): void 'patch_version_in_folder_name' => $usePatch ]); - $folder = $library->getLibraryString(true); + $folder = $library->getFolderName(); $expected = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $folder); Storage::fake('test'); diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/ViewConfigTest.php similarity index 98% rename from sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php rename to sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/ViewConfigTest.php index 3e8517ca4b..646bffe703 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/ViewConfigTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/ViewConfigTest.php @@ -1,6 +1,6 @@ make([ 'patch_version_in_folder_name' => $hasPatch, ]); - $this->assertSame($expected, $lib->getLibraryString($isFolder, $usePatch)); + $this->assertSame($expected, $lib->getLibraryString($usePatch)); } - public function provider_libraryString(): \Generator + public function provider_getLibraryString(): \Generator { - yield 'Folder 1' => [true, null, false, 'H5P.Foobar-1.2']; - yield 'Folder 2' => [true, null, true, 'H5P.Foobar-1.2.3']; - yield 'Folder 3' => [true, true, false, 'H5P.Foobar-1.2.3']; - yield 'Folder 4' => [true, true, true, 'H5P.Foobar-1.2.3']; - yield 'Folder 5' => [true, false, false, 'H5P.Foobar-1.2']; - yield 'Folder 6' => [true, false, true, 'H5P.Foobar-1.2']; + yield 0 => [null, false, 'H5P.Foobar 1.2']; + yield 1 => [null, true, 'H5P.Foobar 1.2.3']; + yield 2 => [true, false, 'H5P.Foobar 1.2.3']; + yield 3 => [true, true, 'H5P.Foobar 1.2.3']; + yield 4 => [false, false, 'H5P.Foobar 1.2']; + yield 5 => [false, true, 'H5P.Foobar 1.2']; + } + + /** + * @dataProvider provider_getFolderName + */ + public function test_getFolderName($usePatch, $hasPatch, $expected): void + { + /** @var H5PLibrary $lib */ + $lib = H5PLibrary::factory()->make([ + 'patch_version_in_folder_name' => $hasPatch, + ]); - yield 'Name 1' => [false, null, false, 'H5P.Foobar 1.2']; - yield 'Name 2' => [false, null, true, 'H5P.Foobar 1.2.3']; - yield 'Name 3' => [false, true, false, 'H5P.Foobar 1.2.3']; - yield 'Name 4' => [false, true, true, 'H5P.Foobar 1.2.3']; - yield 'Name 5' => [false, false, false, 'H5P.Foobar 1.2']; - yield 'Name 6' => [false, false, true, 'H5P.Foobar 1.2']; + $this->assertSame($expected, $lib->getFolderName($usePatch)); + } + + public function provider_getFolderName(): \Generator + { + yield 0 => [null, false, 'H5P.Foobar-1.2']; + yield 1 => [null, true, 'H5P.Foobar-1.2.3']; + yield 2 => [true, false, 'H5P.Foobar-1.2.3']; + yield 3 => [true, true, 'H5P.Foobar-1.2.3']; + yield 4 => [false, false, 'H5P.Foobar-1.2']; + yield 5 => [false, true, 'H5P.Foobar-1.2']; } /** @dataProvider provider_libraryToFolderName */ diff --git a/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php b/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php index 7f953b9f00..855444842e 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php @@ -10,6 +10,7 @@ use App\Libraries\ContentAuthorStorage; use App\Libraries\H5P\Framework; use Cerpus\VersionClient\VersionData; +use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Http\Request; @@ -81,7 +82,14 @@ public function test_checkLibrary(): void $this->instance(ContentAuthorStorage::class, $storage); $storage ->expects($this->once()) - ->method('copyFolder'); + ->method('copyFolder') + ->with( + $this->isInstanceOf(FilesystemAdapter::class), + $this->isInstanceOf(FilesystemAdapter::class), + $this->equalTo('libraries/H5P.Foobar-1.2'), + $this->equalTo('libraries/H5P.Foobar-1.2'), + $this->equalTo([]), + ); $framework = $this->createMock(Framework::class); $this->instance(Framework::class, $framework); @@ -97,6 +105,7 @@ public function test_checkLibrary(): void $validator ->expects($this->once()) ->method('getLibraryData') + ->with($this->equalTo('H5P.Foobar-1.2'), $this->isNull(), $this->isNull()) ->willReturn([ 'editorDependencies' => [ [ diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php index ee6e0b6735..29deee12d3 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php @@ -4,11 +4,7 @@ namespace Tests\Unit\Libraries\H5P; -use App\H5PContent; -use App\H5PLibrary; -use App\H5PLibraryLibrary; use App\Libraries\H5P\Framework; -use App\Libraries\H5P\Storage\H5PCerpusStorage; use ArrayObject; use Exception; use GuzzleHttp\Client; @@ -18,17 +14,14 @@ use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Response; use Illuminate\Contracts\Filesystem\Filesystem; -use Illuminate\Foundation\Testing\RefreshDatabase; use InvalidArgumentException; use PDO; +use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Tests\TestCase; final class FrameworkTest extends TestCase { - use RefreshDatabase; - /** @var ArrayObject */ private ArrayObject $history; @@ -179,152 +172,4 @@ public function testGetMessagesOfUnknownType(): void $this->framework->getMessages('unknown'); } - - public function testSaveLibrary(): void - { - $input = [ - 'machineName' => 'H5P.UnitTest', - 'title' => 'Unit Test', - 'majorVersion' => 2, - 'minorVersion' => 4, - 'patchVersion' => 6, - 'runnable' => 1, - 'metadataSettings' => 'Yupp', - 'addTo' => ['machineName' => 'Something'], - 'hasIcon' => 1, - 'embedTypes' => ['E1', 'E2'], - 'preloadedJs' => [ - ['path' => 'PJ1', 'name' => 'PJ1 name', 'machineName' => 'H5P.Pj1'], - ['path' => 'PJ2', 'name' => 'PJ2 name', 'machineName' => 'H5P.Pj2'], - ], - 'preloadedCss' => [ - ['path' => 'PC1', 'name' => 'PC1 name', 'machineName' => 'H5P.Pc1'], - ['path' => 'PC2', 'name' => 'PC2 name', 'machineName' => 'H5P.Pc1'], - ], - 'dropLibraryCss' => [ - ['path' => 'DC1', 'name' => 'DC1 name', 'machineName' => 'H5P.Dc1'], - ['path' => 'DC2', 'name' => 'DC2 name', 'machineName' => 'H5P.Dc2'], - ], - 'language' => [ - 'nb' => 'Norsk Bokmål', - 'nn' => 'Norsk Nynorsk', - ], - ]; - $this->framework->saveLibraryData($input); - - $this->assertDatabaseHas('h5p_libraries', ['id' => $input['libraryId']]); - $this->assertDatabaseHas('h5p_libraries_languages', [ - 'library_id' => $input['libraryId'], - 'language_code' => 'nb', - 'translation' => 'Norsk Bokmål', - ]); - $this->assertDatabaseHas('h5p_libraries_languages', [ - 'library_id' => $input['libraryId'], - 'language_code' => 'nn', - 'translation' => 'Norsk Nynorsk', - ]); - - /** @var H5PLibrary $library */ - $library = H5PLibrary::find($input['libraryId']); - - $this->assertSame('H5P.UnitTest', $library->name); - $this->assertSame('Unit Test', $library->title); - $this->assertSame(2, $library->major_version); - $this->assertSame(4, $library->minor_version); - $this->assertSame(6, $library->patch_version); - $this->assertSame(1, $library->runnable); - $this->assertSame(0, $library->fullscreen); - $this->assertSame('E1, E2', $library->embed_types); - $this->assertSame('PJ1, PJ2', $library->preloaded_js); - $this->assertSame('PC1, PC2', $library->preloaded_css); - $this->assertSame('H5P.Dc1, H5P.Dc2', $library->drop_library_css); - $this->assertSame('', $library->semantics); - $this->assertSame(1, $library->has_icon); - $this->assertSame(true, $library->patch_version_in_folder_name); - } - - public function testLoadLibrary(): void - { - H5PLibrary::factory()->create([ - 'major_version' => 1, - 'minor_version' => 1, - 'patch_version' => 9, - ]); - H5PLibrary::factory()->create([ - 'major_version' => 1, - 'minor_version' => 2, - 'patch_version' => 2, - ]); - /** @var H5PLibrary $editDep */ - $editDep = H5PLibrary::factory()->create([ - 'name' => 'H5PEditor.Foobar', - 'patch_version_in_folder_name' => true, - ]); - /** @var H5PLibrary $saved */ - $saved = H5PLibrary::factory()->create([ - 'patch_version_in_folder_name' => true, - ]); - H5PLibraryLibrary::create([ - 'library_id' => $saved->id, - 'required_library_id' => $editDep->id, - 'dependency_type' => 'editor', - ]); - - $library = $this->framework->loadLibrary('H5P.Foobar', 1, 2); - $this->assertSame($saved->id, $library['libraryId']); - $this->assertSame($saved->name, $library['machineName']); - $this->assertSame($saved->major_version, $library['majorVersion']); - $this->assertSame($saved->minor_version, $library['minorVersion']); - $this->assertSame($saved->patch_version, $library['patchVersion']); - $this->assertSame($saved->patch_version_in_folder_name, $library['patchVersionInFolderName']); - - $this->assertSame($editDep->name, $library['editorDependencies'][0]['machineName']); - $this->assertSame($editDep->patch_version_in_folder_name, $library['editorDependencies'][0]['patchVersionInFolderName']); - } - - /** @dataProvider provider_usePatch */ - public function test_deleteLibrary($usePatch): void - { - /** @var H5PLibrary $library */ - $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); - - $storage = $this->createMock(H5PCerpusStorage::class); - $this->instance(H5PCerpusStorage::class, $storage); - $storage - ->expects($this->once()) - ->method('deleteLibrary') - ->with([ - 'machineName' => $library->name, - 'majorVersion' => $library->major_version, - 'minorVersion' => $library->minor_version, - 'patchVersion' => $library->patch_version, - 'patchVersionInFolderName' => $library->patch_version_in_folder_name, - ]); - - $this->assertDatabaseHas('h5p_libraries', ['id' => $library->id]); - $this->framework->deleteLibrary($library); - $this->assertDatabaseMissing('h5p_libraries', ['id' => $library->id]); - } - - /** @dataProvider provider_usePatch */ - public function test_loadContent($usePatch): void - { - /** @var H5PLibrary $h5pLibrary */ - $h5pLibrary = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); - /** @var H5PContent $h5pContent */ - $h5pContent = H5PContent::factory()->create(['library_id' => $h5pLibrary->id]); - - $content = $this->framework->loadContent($h5pContent->id); - - $this->assertSame($h5pContent->id, $content['id']); - $this->assertSame($h5pContent->id, $content['contentId']); - $this->assertSame($h5pLibrary->id, $content['libraryId']); - $this->assertSame($h5pLibrary->getLibraryString(), $content['libraryFullVersionName']); - } - - public function provider_usePatch(): \Generator - { - yield [false]; - yield [true]; - } } From e57a56232c0586597ed4651f0e14dee9209484a7 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Thu, 29 Jun 2023 09:34:59 +0200 Subject: [PATCH 12/22] Update to latest h5p-core --- sourcecode/apis/contentauthor/composer.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sourcecode/apis/contentauthor/composer.lock b/sourcecode/apis/contentauthor/composer.lock index 97ee609b53..cb4dfe5ae0 100644 --- a/sourcecode/apis/contentauthor/composer.lock +++ b/sourcecode/apis/contentauthor/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1fd674e156313954899ecd7a6d284612", + "content-hash": "8dd21089be96538bc9d9d836ddb9b0eb", "packages": [ { "name": "auth0/auth0-php", @@ -2444,12 +2444,12 @@ "source": { "type": "git", "url": "https://github.com/h5p/h5p-php-library.git", - "reference": "06dab4041b7ae59a4f579d14a12297507145b7f8" + "reference": "0a82667e00175dea55e37ad8bbebedbfed25b5b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/h5p/h5p-php-library/zipball/06dab4041b7ae59a4f579d14a12297507145b7f8", - "reference": "06dab4041b7ae59a4f579d14a12297507145b7f8", + "url": "https://api.github.com/repos/h5p/h5p-php-library/zipball/0a82667e00175dea55e37ad8bbebedbfed25b5b6", + "reference": "0a82667e00175dea55e37ad8bbebedbfed25b5b6", "shasum": "" }, "require": { @@ -2498,7 +2498,7 @@ "issues": "https://github.com/h5p/h5p-php-library/issues", "source": "https://github.com/h5p/h5p-php-library/tree/master" }, - "time": "2023-03-09T14:48:41+00:00" + "time": "2023-06-27T13:26:01+00:00" }, { "name": "h5p/h5p-editor", From 528173f6a35a0e988c8b5631b958ab73a25cdf6a Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Thu, 29 Jun 2023 09:44:33 +0200 Subject: [PATCH 13/22] PHPStan fix --- .../tests/Integration/Libraries/H5P/FrameworkTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php index 161b13cd13..d3b2a8ec7a 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php @@ -172,9 +172,10 @@ public function test_deleteLibrary($usePatch): void 'patchVersionInFolderName' => $library->patch_version_in_folder_name, ]); - $this->assertDatabaseHas('h5p_libraries', ['id' => $library->id]); - $this->framework->deleteLibrary($library); - $this->assertDatabaseMissing('h5p_libraries', ['id' => $library->id]); + $lib = ['id' => $library->id]; + $this->assertDatabaseHas('h5p_libraries', $lib); + $this->framework->deleteLibrary((object) $lib); + $this->assertDatabaseMissing('h5p_libraries', $lib); } /** @dataProvider provider_usePatch */ From 3dcd0e1b3dbba6eae8657d0426d149900b1fc536 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Thu, 29 Jun 2023 10:06:31 +0200 Subject: [PATCH 14/22] Update to latest h5p-core --- sourcecode/apis/contentauthor/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sourcecode/apis/contentauthor/composer.json b/sourcecode/apis/contentauthor/composer.json index f18b1e3e32..366beebc15 100644 --- a/sourcecode/apis/contentauthor/composer.json +++ b/sourcecode/apis/contentauthor/composer.json @@ -27,7 +27,7 @@ "embed/embed": "^3.2", "firebase/php-jwt": "^6.3", "guzzlehttp/guzzle": "^7.4", - "h5p/h5p-core": "dev-master#06dab4041b7ae59a4f579d14a12297507145b7f8", + "h5p/h5p-core": "dev-master#0a82667e00175dea55e37ad8bbebedbfed25b5b6", "h5p/h5p-editor": "dev-master#0365b081efa8b55ab9fd58594aa599f9630268f6", "laravel/framework": "^9.17", "laravel/horizon": "^5.7", From 8dc8f85bce22e2a4ed245ab490a226fb9df008a3 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Thu, 29 Jun 2023 10:18:50 +0200 Subject: [PATCH 15/22] Remove tests for deleted ViewConfig --- .../Libraries/H5P/ViewConfigTest.php | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/ViewConfigTest.php diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/ViewConfigTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/ViewConfigTest.php deleted file mode 100644 index 646bffe703..0000000000 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/ViewConfigTest.php +++ /dev/null @@ -1,81 +0,0 @@ -make(); - /** @var H5PLibrary $library */ - $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); - /** @var H5PContent $h5pcontent */ - $h5pcontent = H5PContent::factory()->create(['user_id' => $user->auth_id, 'library_id' => $library->id]); - - $storage = $this->createMock(H5PCerpusStorage::class); - $this->instance(H5PCerpusStorage::class, $storage); - $storage->expects($this->once())->method('getDisplayPath')->with(false)->willReturn('/yupp'); - - $resourceApi = $this->createMock(ResourceApiService::class); - $this->instance(ResourceApiService::class, $resourceApi); - $resourceApi - ->expects($this->exactly(2)) - ->method('getResourceFromExternalReference') - ->with('contentauthor', $h5pcontent->id) - ->willReturn( - new Resource( - $this->faker->uuid, - $this->faker->uuid, - '', - null, - '', - '', - 'Test resource' - ) - ); - - $core = app(\H5PCore::class); - $content = $core->loadContent($h5pcontent->id); - - $viewConfig = app(ViewConfig::class) - ->setId($h5pcontent->id) - ->setUserId($user->auth_id) - ->setUserName($user->name) - ->setEmail($user->email) - ->setPreview(false) - ->setContext('nope'); - - $viewConfig->setContent($content); - - $config = $viewConfig->getConfig(); - - $this->assertSame('/yupp', $config->url); - $this->assertSame($user->email, $config->user->mail); - $viewContents = $config->contents->{'cid-' . $h5pcontent->id}; - $this->assertSame($library->getLibraryString(), $viewContents->library); - $this->assertSame($h5pcontent->title, $viewContents->title); - } - - public function provider_testConfig(): \Generator - { - yield [false]; - yield [true]; - } -} From 9b4b23467c02b9756c254660047bd9d96fe0565e Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Thu, 29 Jun 2023 10:25:30 +0200 Subject: [PATCH 16/22] Update PHPStan baseline --- .../apis/contentauthor/phpstan-baseline.neon | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sourcecode/apis/contentauthor/phpstan-baseline.neon b/sourcecode/apis/contentauthor/phpstan-baseline.neon index c6b5e15098..796e9dc89a 100644 --- a/sourcecode/apis/contentauthor/phpstan-baseline.neon +++ b/sourcecode/apis/contentauthor/phpstan-baseline.neon @@ -1386,21 +1386,6 @@ parameters: count: 1 path: tests/Integration/Libraries/H5P/Package/VideoTest.php - - - message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$auth_id\\.$#" - count: 2 - path: tests/Integration/Libraries/H5P/ViewConfigTest.php - - - - message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#" - count: 2 - path: tests/Integration/Libraries/H5P/ViewConfigTest.php - - - - message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$name\\.$#" - count: 1 - path: tests/Integration/Libraries/H5P/ViewConfigTest.php - - message: "#^Parameter \\$share of class App\\\\Libraries\\\\DataObjects\\\\ResourceMetadataDataObject constructor expects string\\|null, true given\\.$#" count: 2 From eacf9b5f64ab8590cfdeb3855a5869238dfd6b57 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Tue, 4 Jul 2023 14:31:33 +0200 Subject: [PATCH 17/22] Adjust export of H5P --- .../app/Libraries/H5P/H5PExport.php | 4 +-- sourcecode/apis/contentauthor/composer.lock | 4 +-- .../Libraries/H5P/H5PExportTest.php | 35 +++++++++++++------ 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php index ef6b211808..e212c435d9 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php @@ -44,7 +44,7 @@ public function generateExport(H5PContent $content): bool $parameters = $content->parameters; } - $library = H5PCore::libraryFromString($content->library->getLibraryString()) + $library = H5PCore::libraryFromString($content->library->getLibraryString(false)) ?: throw new UnexpectedValueException('Bad library string'); $library['libraryId'] = $content->library->id; $library['name'] = $content->library->name; @@ -60,7 +60,7 @@ public function generateExport(H5PContent $content): bool // resolving dependencies, as it likes to corrupt the data $validatorParams = (object)[ - 'library' => $content->library->getLibraryString(), + 'library' => $content->library->getLibraryString(false), 'params' => json_decode($content->parameters) ]; $this->validator->validateLibrary($validatorParams, (object) [ diff --git a/sourcecode/apis/contentauthor/composer.lock b/sourcecode/apis/contentauthor/composer.lock index 1382a16d24..18b24205ca 100644 --- a/sourcecode/apis/contentauthor/composer.lock +++ b/sourcecode/apis/contentauthor/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5bad76ecae7092883dc8eda732e02555", + "content-hash": "a12752af85d256cb5f8864ffe7b6b431", "packages": [ { "name": "auth0/auth0-php", @@ -11658,9 +11658,9 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "h5p/h5p-core": 20, "cerpus/edlib-resource-kit": 20, "cerpus/edlib-resource-kit-laravel": 20, + "h5p/h5p-core": 20, "h5p/h5p-editor": 20 }, "prefer-stable": false, diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php index 008dafd0e3..7d6dd1ae15 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php @@ -3,6 +3,7 @@ namespace Tests\Integration\Libraries\H5P; use App\H5PContent; +use App\H5PLibrary; use App\Libraries\DataObjects\ContentStorageSettings; use App\Libraries\H5P\H5PExport; use App\Libraries\H5P\Interfaces\H5PExternalProviderInterface; @@ -33,26 +34,38 @@ protected function setUp(): void $this->testDisk = Storage::disk('testDisk'); $this->exportDisk = Storage::fake(); config(['h5p.storage.path' => $this->exportDisk->path("")]); + symlink($this->testDisk->path('files/libraries'), $this->exportDisk->path('libraries')); } - private function linkLibrariesFolder() + protected function tearDown(): void { - symlink($this->testDisk->path('files/libraries'), $this->exportDisk->path('libraries')); + parent::tearDown(); + + unlink($this->exportDisk->path('libraries')); } /** * @test * @throws Exception + * @dataProvider provider_noMultimedia */ - public function noMultimedia() + public function noMultimedia(bool $usePatchFolder) { $this->setupH5PAdapter([ 'alterLibrarySemantics' => null, 'getExternalProviders' => [], ]); - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); + + if ($usePatchFolder) { + $lib = H5PLibrary::find(284); + $lib->minor_version = 12; + $lib->patch_version = 2; + $lib->patch_version_in_folder_name = true; + $lib->save(); + } + $params = '{"text":"

Fill in the missing words<\/p>\n","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"\':ans\' is correct","answerIsWrong":"\':ans\' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

*Fishing* is a fun *activity*.<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ 'parameters' => $params, @@ -74,6 +87,12 @@ public function noMultimedia() $zipArchive->close(); } + public function provider_noMultimedia(): \Generator + { + yield 'minorFolder' => [false]; + yield 'patchFolder' => [true]; + } + /** * @test * @throws Exception @@ -85,7 +104,6 @@ public function withLocalImage() 'getExternalProviders' => [], ]); - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"media":{"params":{"contentName":"Image","file":{"path":"images\/file-5f6ca98160e6c.jpg","mime":"image\/jpeg","copyright":{"license":"U"},"width":196,"height":358}},"library":"H5P.Image 1.1","metadata":{"contentType":"Image","license":"U","title":"Untitled Image","authors":[],"changes":[],"extraTitle":"Untitled Image"},"subContentId":"ca86c100-d25c-4e19-ac6b-f50f843da292"},"text":"Fill in the missing words","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"':ans' is correct","answerIsWrong":"':ans' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

Not all *superheros* wear capes!<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ @@ -125,7 +143,6 @@ public function withRemoteImage_noLocalConvert() $imageUrl = $this->faker->imageUrl(); - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"media":{"params":{"contentName":"Image","file":{"path":"' . $imageUrl . '","mime":"image\/jpeg","copyright":{"license":"U"},"width":196,"height":358}},"library":"H5P.Image 1.1","metadata":{"contentType":"Image","license":"U","title":"Untitled Image","authors":[],"changes":[],"extraTitle":"Untitled Image"},"subContentId":"ca86c100-d25c-4e19-ac6b-f50f843da292"},"text":"Fill in the missing words","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"':ans' is correct","answerIsWrong":"':ans' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

Not all *superheros* wear capes!<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ @@ -159,7 +176,7 @@ public function withRemoteImage_noLocalConvert() public function withRemoteImage_storeLocally() { $imageUrl = $this->faker->imageUrl(); - $this->linkLibrariesFolder(); + $this->seed(TestH5PSeeder::class); $params = '{"media":{"params":{"contentName":"Image","file":{"path":"' . $imageUrl . '","mime":"image\/jpeg","copyright":{"license":"U"},"width":196,"height":358}},"library":"H5P.Image 1.1","metadata":{"contentType":"Image","license":"U","title":"Untitled Image","authors":[],"changes":[],"extraTitle":"Untitled Image"},"subContentId":"ca86c100-d25c-4e19-ac6b-f50f843da292"},"text":"Fill in the missing words","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"':ans' is correct","answerIsWrong":"':ans' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

Not all *superheros* wear capes!<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ @@ -219,7 +236,6 @@ public function withRemoteImage_storeLocally() */ public function withRemoteVideo_storeLocally() { - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"interactiveVideo":{"video":{"startScreenOptions":{"title":"Interactive Video","hideStartTitle":false,"copyright":""},"textTracks":[{"label":"Subtitles","kind":"subtitles","srcLang":"en"}],"files":[{"path":"https://bc/12456","mime":"video/BrightCove","copyright":{"license":"U"}}]},"assets":{"interactions":[]},"summary":{"task":{"library":"H5P.Summary 1.8","params":{"intro":"Choose the correct statement.","summaries":[{"subContentId":"2363644d-ab92-4f93-8d11-0d6e916bd83d","tip":""}],"overallFeedback":[{"from":0,"to":100}],"solvedLabel":"Progress:","scoreLabel":"Wrong answers:","resultLabel":"Your result","labelCorrect":"Correct.","labelIncorrect":"Incorrect! Please try again.","labelCorrectAnswers":"Correct answers.","tipButtonLabel":"Show tip","scoreBarLabel":"You got :num out of :total points","progressText":"Progress :num of :total"},"subContentId":"211f821f-67f9-4717-a7a4-362c4d507545","metadata":{"contentType":"Summary","license":"U","title":"Untitled Summary"}},"displayAt":3}},"override":{"autoplay":false,"loop":false,"showBookmarksmenuOnLoad":false,"showRewind10":false,"preventSkipping":false,"deactivateSound":false},"l10n":{"interaction":"Interaction","play":"Play","pause":"Pause","mute":"Mute","unmute":"Unmute","quality":"Video Quality","captions":"Captions","close":"Close","fullscreen":"Fullscreen","exitFullscreen":"Exit Fullscreen","summary":"Summary","bookmarks":"Bookmarks","defaultAdaptivitySeekLabel":"Continue","continueWithVideo":"Continue with video","playbackRate":"Playback Rate","rewind10":"Rewind 10 Seconds","navDisabled":"Navigation is disabled","sndDisabled":"Sound is disabled","requiresCompletionWarning":"You need to answer all the questions correctly before continuing.","back":"Back","hours":"Hours","minutes":"Minutes","seconds":"Seconds","currentTime":"Current time:","totalTime":"Total time:","navigationHotkeyInstructions":"Use key k for starting and stopping video at any time","singleInteractionAnnouncement":"Interaction appeared:","multipleInteractionsAnnouncement":"Multiple interactions appeared.","videoPausedAnnouncement":"Video is paused","content":"Content"}}'; $h5p = H5PContent::factory()->create([ @@ -278,7 +294,7 @@ public function withRemoteVideo_storeLocally() public function withRemoteVideoAndImage_storeLocally() { $imageUrl = $this->faker->imageUrl(); - $this->linkLibrariesFolder(); + $this->seed(TestH5PSeeder::class); $params = '{"interactiveVideo":{"video":{"startScreenOptions":{"title":"Interactive Video","hideStartTitle":false,"copyright":""},"textTracks":[{"label":"Subtitles","kind":"subtitles","srcLang":"en"}],"files":[{"path":"https://bc/ref:12456","mime":"video/BrightCove","copyright":{"license":"U"}}]},"assets":{"interactions":[{"x":5.230125523012552,"y":13.011152416356877,"width":14.986376021798364,"height":10,"duration":{"from":0,"to":10},"libraryTitle":"Image","action":{"library":"H5P.Image 1.0","params":{"contentName":"Image","file":{"path":"' . $imageUrl . '","mime":"image/jpeg","copyright":{"license":"U"},"width":1100,"height":734},"alt":"test"},"subContentId":"b0bd1110-ada3-4afe-9be9-1a822ea9643d","metadata":{"contentType":"Image","license":"U"}},"visuals":{"backgroundColor":"rgba(0,0,0,0)","boxShadow":true},"pause":false,"displayType":"poster","buttonOnMobile":false,"goto":{"url":{"protocol":"http://"},"visualize":false,"type":""},"label":""}]},"summary":{"task":{"library":"H5P.Summary 1.8","params":{"intro":"Choose the correct statement.","summaries":[{"subContentId":"2363644d-ab92-4f93-8d11-0d6e916bd83d","tip":""}],"overallFeedback":[{"from":0,"to":100}],"solvedLabel":"Progress:","scoreLabel":"Wrong answers:","resultLabel":"Your result","labelCorrect":"Correct.","labelIncorrect":"Incorrect! Please try again.","labelCorrectAnswers":"Correct answers.","tipButtonLabel":"Show tip","scoreBarLabel":"You got :num out of :total points","progressText":"Progress :num of :total"},"subContentId":"211f821f-67f9-4717-a7a4-362c4d507545","metadata":{"contentType":"Summary","license":"U","title":"Untitled Summary"}},"displayAt":3}},"override":{"autoplay":false,"loop":false,"showBookmarksmenuOnLoad":false,"showRewind10":false,"preventSkipping":false,"deactivateSound":false},"l10n":{"interaction":"Interaction","play":"Play","pause":"Pause","mute":"Mute","unmute":"Unmute","quality":"Video Quality","captions":"Captions","close":"Close","fullscreen":"Fullscreen","exitFullscreen":"Exit Fullscreen","summary":"Summary","bookmarks":"Bookmarks","defaultAdaptivitySeekLabel":"Continue","continueWithVideo":"Continue with video","playbackRate":"Playback Rate","rewind10":"Rewind 10 Seconds","navDisabled":"Navigation is disabled","sndDisabled":"Sound is disabled","requiresCompletionWarning":"You need to answer all the questions correctly before continuing.","back":"Back","hours":"Hours","minutes":"Minutes","seconds":"Seconds","currentTime":"Current time:","totalTime":"Total time:","navigationHotkeyInstructions":"Use key k for starting and stopping video at any time","singleInteractionAnnouncement":"Interaction appeared:","multipleInteractionsAnnouncement":"Multiple interactions appeared.","videoPausedAnnouncement":"Video is paused","content":"Content"}}'; $h5p = H5PContent::factory()->create([ @@ -352,7 +368,6 @@ public function withRemoteVideoAndImage_storeLocally() */ public function withRemoteCaptions_storeLocally() { - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"interactiveVideo":{"video":{"startScreenOptions":{"title":"Interactive Video","hideStartTitle":false,"copyright":""},"textTracks":[{"kind":"captions","label":"Nynorsk","srcLang":"nn","track":{"externalId":"87ada56a-5670-4b43-96e3-4aef54284197","path":"https:\/\/urltocaptions/text.vtt","mime":"text\/webvtt","copyright":{"license":"U"}}}],"files":[{"path":"https://bc/12456","mime":"video/BrightCove","copyright":{"license":"U"}}]},"assets":{"interactions":[]},"summary":{"task":{"library":"H5P.Summary 1.8","params":{"intro":"Choose the correct statement.","summaries":[{"subContentId":"2363644d-ab92-4f93-8d11-0d6e916bd83d","tip":""}],"overallFeedback":[{"from":0,"to":100}],"solvedLabel":"Progress:","scoreLabel":"Wrong answers:","resultLabel":"Your result","labelCorrect":"Correct.","labelIncorrect":"Incorrect! Please try again.","labelCorrectAnswers":"Correct answers.","tipButtonLabel":"Show tip","scoreBarLabel":"You got :num out of :total points","progressText":"Progress :num of :total"},"subContentId":"211f821f-67f9-4717-a7a4-362c4d507545","metadata":{"contentType":"Summary","license":"U","title":"Untitled Summary"}},"displayAt":3}},"override":{"autoplay":false,"loop":false,"showBookmarksmenuOnLoad":false,"showRewind10":false,"preventSkipping":false,"deactivateSound":false},"l10n":{"interaction":"Interaction","play":"Play","pause":"Pause","mute":"Mute","unmute":"Unmute","quality":"Video Quality","captions":"Captions","close":"Close","fullscreen":"Fullscreen","exitFullscreen":"Exit Fullscreen","summary":"Summary","bookmarks":"Bookmarks","defaultAdaptivitySeekLabel":"Continue","continueWithVideo":"Continue with video","playbackRate":"Playback Rate","rewind10":"Rewind 10 Seconds","navDisabled":"Navigation is disabled","sndDisabled":"Sound is disabled","requiresCompletionWarning":"You need to answer all the questions correctly before continuing.","back":"Back","hours":"Hours","minutes":"Minutes","seconds":"Seconds","currentTime":"Current time:","totalTime":"Total time:","navigationHotkeyInstructions":"Use key k for starting and stopping video at any time","singleInteractionAnnouncement":"Interaction appeared:","multipleInteractionsAnnouncement":"Multiple interactions appeared.","videoPausedAnnouncement":"Video is paused","content":"Content"}}'; $h5p = H5PContent::factory()->create([ From adab9914ebe9eb864313442e9c7c67b89c18a781 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Tue, 4 Jul 2023 14:40:59 +0200 Subject: [PATCH 18/22] Fix unittests --- .../Http/Controllers/H5PControllerTest.php | 25 +- .../Libraries/H5P/H5PExportTest.php | 4 +- .../H5P.Blanks-1.14.6/css/blanks.css | 113 ++ .../libraries/H5P.Blanks-1.14.6/icon.svg | 68 ++ .../libraries/H5P.Blanks-1.14.6/js/blanks.js | 1004 +++++++++++++++++ .../libraries/H5P.Blanks-1.14.6/js/cloze.js | 238 ++++ .../H5P.Blanks-1.14.6/language/nb.json | 214 ++++ .../libraries/H5P.Blanks-1.14.6/library.json | 65 ++ .../libraries/H5P.Blanks-1.14.6/presave.js | 40 + .../H5P.Blanks-1.14.6/semantics.json | 489 ++++++++ .../libraries/H5P.Blanks-1.14.6/upgrades.js | 124 ++ 11 files changed, 2378 insertions(+), 6 deletions(-) create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js diff --git a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php index 64a2ad414d..a0b36ebaed 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php @@ -27,7 +27,8 @@ class H5PControllerTest extends TestCase use RefreshDatabase; use MockAuthApi; - public function testCreate(): void + /** @dataProvider provider_testCreate */ + public function testCreate(?string $contentType): void { $faker = Factory::create(); $this->session([ @@ -44,11 +45,15 @@ public function testCreate(): void 'redirectToken' => $faker->uuid, ]); + H5PLibrary::factory()->create(); + /** @var H5PCore $h5pCore */ $h5pCore = app(H5pCore::class); + /** @var H5PController $articleController */ $articleController = app(H5PController::class); - $result = $articleController->create($request, $h5pCore); + $result = $articleController->create($request, $h5pCore, $contentType); + $this->assertInstanceOf(View::class, $result); $data = $result->getData(); @@ -62,9 +67,14 @@ public function testCreate(): void $this->assertNotEmpty($data['editorSetup']); $this->assertNotEmpty($data['state']); $this->assertArrayHasKey('configJs', $data); + $this->assertSame($contentType, $data['libName']); $config = json_decode(substr($result['config'], 25, -9), true, flags: JSON_THROW_ON_ERROR); - $this->assertTrue($config['hubIsEnabled']); + if ($contentType === null) { + $this->assertTrue($config['hubIsEnabled']); + } else { + $this->assertFalse($config['hubIsEnabled']); + } $this->assertEmpty($config['contents']); $this->assertSame('nb-no', $config['locale']); $this->assertSame('nb', $config['localeConverted']); @@ -85,15 +95,22 @@ public function testCreate(): void $this->assertEquals('Emily Quackfaster', $editorSetup['creatorName']); $state = json_decode($data['state'], true, flags: JSON_THROW_ON_ERROR); + $this->assertNull($state['id']); $this->assertNull($state['title']); $this->assertFalse($state['isPublished']); - $this->assertNull($state['library']); + $this->assertSame($contentType, $state['library']); $this->assertNull($state['libraryid']); $this->assertSame('nob', $state['language_iso_639_3']); $this->assertEquals(config('license.default-license'), $state['license']); } + public function provider_testCreate(): \Generator + { + yield 'withoutContentType' => [null]; + yield 'withContentType' => ['H5P.Toolbar 1.2']; + } + public function testEdit(): void { $faker = Factory::create(); diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php index 7d6dd1ae15..7bb85b835c 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php @@ -60,8 +60,8 @@ public function noMultimedia(bool $usePatchFolder) if ($usePatchFolder) { $lib = H5PLibrary::find(284); - $lib->minor_version = 12; - $lib->patch_version = 2; + $lib->minor_version = 14; + $lib->patch_version = 6; $lib->patch_version_in_folder_name = true; $lib->save(); } diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css new file mode 100644 index 0000000000..b5e99c0986 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css @@ -0,0 +1,113 @@ +.h5p-blanks { + position: relative; +} +.h5p-blanks .h5p-input-wrapper { + display: inline-block; + position: relative; +} +.h5p-blanks .h5p-text-input { + font-family: H5PDroidSans, sans-serif; + font-size: 1em; + border-radius: 0.25em; + border: 1px solid #a0a0a0; + padding: 0.1875em 1em 0.1875em 0.5em; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 6em; +} +.h5p-blanks .h5p-text-input:focus { + outline: none; + box-shadow: 0 0 0.5em 0 #6391CA; + border-color: #6391CA; +} +.h5p-blanks .h5p-text-input:disabled { + opacity: 1; +} +.h5p-blanks .h5p-text-input.h5p-not-filled-out { + background: #fff0f0; +} +.h5p-blanks .h5p-separate-lines .h5p-input-wrapper { + display: block; +} +.h5p-blanks .h5p-separate-lines .h5p-text-input { + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; +} + +/* Correctly answered input */ +.h5p-blanks .h5p-correct .h5p-text-input { + background: #9dd8bb; + border: 1px solid #9dd8bb; + color: #255c41; +} +/* Showing solution */ +.h5p-blanks .h5p-correct-answer { + color: #255c41; + font-weight: bold; + border: 1px #255c41 dashed; + background-color: #d4f6e6; + padding: 0.15em; + border-radius: 0.25em; + margin-left: .5em; +} +.h5p-blanks .h5p-correct:after { + position: absolute; + right: 0.5em; + top: 0; + text-decoration: none; + content: "\f00c"; + font-family: 'H5PFontAwesome4'; + color: #255c41; +} + +/* Wrongly answered input */ +.h5p-blanks .h5p-wrong .h5p-text-input { + background-color: #f7d0d0; + border: 1px solid #f7d0d0; + color: #b71c1c; + text-decoration: line-through; +} +.h5p-blanks .h5p-wrong:after { + position: absolute; + right: 0.5em; + top: 0; + font-family: 'H5PFontAwesome4'; + text-decoration: none; + content: "\f00d"; + color: #b71c1c; +} + +/* Actual text paragraphs */ +.h5p-blanks .h5p-question-content p { + line-height: 1.75em; + margin: 0 0 1em; +} + +/* Header and footer blocks (title + evaluation, buttons) */ +.h5p-blanks .joubel-tip-container { + position: absolute; + right: 0.4em; + font-size: 1em; +} +.h5p-blanks .joubel-tip-container .joubel-icon-tip-normal { + line-height: 1em; +} +.h5p-blanks .has-tip .h5p-text-input { + padding-right: 2.25em; +} +.h5p-blanks .has-tip.h5p-correct:after, +.h5p-blanks .has-tip.h5p-wrong:after { + right: 2.25em; +} +.h5p-blanks .has-tip.h5p-correct .h5p-text-input, +.h5p-blanks .has-tip.h5p-wrong .h5p-text-input { + padding-right: 3.5em; +} +.h5p-blanks .hidden-but-read { + position: absolute; + height: 0; + width: 0; + overflow: hidden; +} diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg new file mode 100644 index 0000000000..ef1db071e0 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg @@ -0,0 +1,68 @@ + + + + + fill in the blanks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js new file mode 100644 index 0000000000..30194deb3b --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js @@ -0,0 +1,1004 @@ +/*global H5P*/ +H5P.Blanks = (function ($, Question) { + /** + * @constant + * @default + */ + var STATE_ONGOING = 'ongoing'; + var STATE_CHECKING = 'checking'; + var STATE_SHOWING_SOLUTION = 'showing-solution'; + var STATE_FINISHED = 'finished'; + + const XAPI_ALTERNATIVE_EXTENSION = 'https://h5p.org/x-api/alternatives'; + const XAPI_CASE_SENSITIVITY = 'https://h5p.org/x-api/case-sensitivity'; + const XAPI_REPORTING_VERSION_EXTENSION = 'https://h5p.org/x-api/h5p-reporting-version'; + + /** + * @typedef {Object} Params + * Parameters/configuration object for Blanks + * + * @property {Object} Params.behaviour + * @property {string} Params.behaviour.confirmRetryDialog + * @property {string} Params.behaviour.confirmCheckDialog + * + * @property {Object} Params.confirmRetry + * @property {string} Params.confirmRetry.header + * @property {string} Params.confirmRetry.body + * @property {string} Params.confirmRetry.cancelLabel + * @property {string} Params.confirmRetry.confirmLabel + * + * @property {Object} Params.confirmCheck + * @property {string} Params.confirmCheck.header + * @property {string} Params.confirmCheck.body + * @property {string} Params.confirmCheck.cancelLabel + * @property {string} Params.confirmCheck.confirmLabel + */ + + /** + * Initialize module. + * + * @class H5P.Blanks + * @extends H5P.Question + * @param {Params} params + * @param {number} id Content identification + * @param {Object} contentData Task specific content data + */ + function Blanks(params, id, contentData) { + var self = this; + + // Inheritance + Question.call(self, 'blanks'); + + // IDs + this.contentId = id; + this.contentData = contentData; + + this.params = $.extend(true, {}, { + text: "Fill in", + questions: [ + "

Oslo is the capital of *Norway*.

" + ], + overallFeedback: [], + userAnswers: [], // TODO This isn't in semantics? + showSolutions: "Show solution", + tryAgain: "Try again", + checkAnswer: "Check", + changeAnswer: "Change answer", + notFilledOut: "Please fill in all blanks to view solution", + answerIsCorrect: "':ans' is correct", + answerIsWrong: "':ans' is wrong", + answeredCorrectly: "Answered correctly", + answeredIncorrectly: "Answered incorrectly", + solutionLabel: "Correct answer:", + inputLabel: "Blank input @num of @total", + inputHasTipLabel: "Tip available", + tipLabel: "Tip", + scoreBarLabel: 'You got :num out of :total points', + behaviour: { + enableRetry: true, + enableSolutionsButton: true, + enableCheckButton: true, + caseSensitive: true, + showSolutionsRequiresInput: true, + autoCheck: false, + separateLines: false + }, + a11yCheck: 'Check the answers. The responses will be marked as correct, incorrect, or unanswered.', + a11yShowSolution: 'Show the solution. The task will be marked with its correct solution.', + a11yRetry: 'Retry the task. Reset all responses and start the task over again.', + a11yHeader: 'Checking mode', + submitAnswer: 'Submit', + }, params); + + // Delete empty questions + for (var i = this.params.questions.length - 1; i >= 0; i--) { + if (!this.params.questions[i]) { + this.params.questions.splice(i, 1); + } + } + + // Previous state + this.contentData = contentData; + if (this.contentData !== undefined && this.contentData.previousState !== undefined) { + this.previousState = this.contentData.previousState; + } + + // Clozes + this.clozes = []; + + // Keep track tabbing forward or backwards + this.shiftPressed = false; + + H5P.$body.keydown(function (event) { + if (event.keyCode === 16) { + self.shiftPressed = true; + } + }).keyup(function (event) { + if (event.keyCode === 16) { + self.shiftPressed = false; + } + }); + + // Using instructions as label for our text groups + this.labelId = 'h5p-blanks-instructions-' + Blanks.idCounter; + this.content = self.createQuestions(); + + // Check for task media + var media = self.params.media; + if (media && media.type && media.type.library) { + media = media.type; + var type = media.library.split(' ')[0]; + if (type === 'H5P.Image') { + if (media.params.file) { + // Register task image + self.setImage(media.params.file.path, { + disableImageZooming: self.params.media.disableImageZooming || false, + alt: media.params.alt, + title: media.params.title + }); + } + } + else if (type === 'H5P.Video') { + if (media.params.sources) { + // Register task video + self.setVideo(media); + } + } + else if (type === 'H5P.Audio') { + if (media.params.files) { + // Register task audio + self.setAudio(media); + } + } + } + + // Register task introduction text + self.setIntroduction('
' + self.params.text + '
'); + + // Register task content area + self.setContent(self.content, { + 'class': self.params.behaviour.separateLines ? 'h5p-separate-lines' : '' + }); + + // ... and buttons + self.registerButtons(); + + // Restore previous state + self.setH5PUserState(); + } + + // Inheritance + Blanks.prototype = Object.create(Question.prototype); + Blanks.prototype.constructor = Blanks; + + /** + * Create all the buttons for the task + */ + Blanks.prototype.registerButtons = function () { + var self = this; + + var $content = $('[data-content-id="' + self.contentId + '"].h5p-content'); + var $containerParents = $content.parents('.h5p-container'); + + // select find container to attach dialogs to + var $container; + if ($containerParents.length !== 0) { + // use parent highest up if any + $container = $containerParents.last(); + } + else if ($content.length !== 0) { + $container = $content; + } + else { + $container = $(document.body); + } + + if (!self.params.behaviour.autoCheck && this.params.behaviour.enableCheckButton) { + // Check answer button + self.addButton('check-answer', self.params.checkAnswer, function () { + // Move focus to top of content + self.a11yHeader.innerHTML = self.params.a11yHeader; + self.a11yHeader.focus(); + + self.toggleButtonVisibility(STATE_CHECKING); + self.markResults(); + self.showEvaluation(); + self.triggerAnswered(); + }, true, { + 'aria-label': self.params.a11yCheck, + }, { + confirmationDialog: { + enable: self.params.behaviour.confirmCheckDialog, + l10n: self.params.confirmCheck, + instance: self, + $parentElement: $container + }, + textIfSubmitting: self.params.submitAnswer, + contentData: self.contentData, + }); + } + + // Show solution button + self.addButton('show-solution', self.params.showSolutions, function () { + self.showCorrectAnswers(false); + }, self.params.behaviour.enableSolutionsButton, { + 'aria-label': self.params.a11yShowSolution, + }); + + // Try again button + if (self.params.behaviour.enableRetry === true) { + self.addButton('try-again', self.params.tryAgain, function () { + self.a11yHeader.innerHTML = ''; + self.resetTask(); + self.$questions.filter(':first').find('input:first').focus(); + }, true, { + 'aria-label': self.params.a11yRetry, + }, { + confirmationDialog: { + enable: self.params.behaviour.confirmRetryDialog, + l10n: self.params.confirmRetry, + instance: self, + $parentElement: $container + } + }); + } + self.toggleButtonVisibility(STATE_ONGOING); + }; + + /** + * Find blanks in a string and run a handler on those blanks + * + * @param {string} question + * Question text containing blanks enclosed in asterisks. + * @param {function} handler + * Replaces the blanks text with an input field. + * @returns {string} + * The question with blanks replaced by the given handler. + */ + Blanks.prototype.handleBlanks = function (question, handler) { + // Go through the text and run handler on all asterisk + var clozeEnd, clozeStart = question.indexOf('*'); + var self = this; + while (clozeStart !== -1 && clozeEnd !== -1) { + clozeStart++; + clozeEnd = question.indexOf('*', clozeStart); + if (clozeEnd === -1) { + continue; // No end + } + var clozeContent = question.substring(clozeStart, clozeEnd); + var replacer = ''; + if (clozeContent.length) { + replacer = handler(self.parseSolution(clozeContent)); + clozeEnd++; + } + else { + clozeStart += 1; + } + question = question.slice(0, clozeStart - 1) + replacer + question.slice(clozeEnd); + clozeEnd -= clozeEnd - clozeStart - replacer.length; + + // Find the next cloze + clozeStart = question.indexOf('*', clozeEnd); + } + return question; + }; + + /** + * Create questitons html for DOM + */ + Blanks.prototype.createQuestions = function () { + var self = this; + + var html = ''; + var clozeNumber = 0; + for (var i = 0; i < self.params.questions.length; i++) { + var question = self.params.questions[i]; + + // Go through the question text and replace all the asterisks with input fields + question = self.handleBlanks(question, function (solution) { + // Create new cloze + clozeNumber += 1; + var defaultUserAnswer = (self.params.userAnswers.length > self.clozes.length ? self.params.userAnswers[self.clozes.length] : null); + var cloze = new Blanks.Cloze(solution, self.params.behaviour, defaultUserAnswer, { + answeredCorrectly: self.params.answeredCorrectly, + answeredIncorrectly: self.params.answeredIncorrectly, + solutionLabel: self.params.solutionLabel, + inputLabel: self.params.inputLabel, + inputHasTipLabel: self.params.inputHasTipLabel, + tipLabel: self.params.tipLabel + }); + + self.clozes.push(cloze); + return cloze; + }); + + html += '
' + question + '
'; + } + + self.hasClozes = clozeNumber > 0; + this.$questions = $(html); + + self.a11yHeader = document.createElement('div'); + self.a11yHeader.classList.add('hidden-but-read'); + self.a11yHeader.tabIndex = -1; + self.$questions[0].insertBefore(self.a11yHeader, this.$questions[0].childNodes[0] || null); + + // Set input fields. + this.$questions.find('input').each(function (i) { + + /** + * Observe resizing of input field, so that we can resize + * the H5P to fit all content when the input field grows in size + */ + let resizeTimer; + new ResizeObserver(function () { + // To avoid triggering resize too often, we wait a second after the last + // resize event has been received + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + self.trigger('resize'); + }, 1000); + }).observe(this); + + var afterCheck; + if (self.params.behaviour.autoCheck) { + afterCheck = function () { + var answer = $("
").text(this.getUserAnswer()).html(); + self.read((this.correct() ? self.params.answerIsCorrect : self.params.answerIsWrong).replace(':ans', answer)); + if (self.done || self.allBlanksFilledOut()) { + // All answers has been given. Show solutions button. + self.toggleButtonVisibility(STATE_CHECKING); + self.showEvaluation(); + self.triggerAnswered(); + self.done = true; + } + }; + } + self.clozes[i].setInput($(this), afterCheck, function () { + self.toggleButtonVisibility(STATE_ONGOING); + if (!self.params.behaviour.autoCheck) { + self.hideEvaluation(); + } + }, i, self.clozes.length); + }).keydown(function (event) { + var $this = $(this); + + // Adjust width of text input field to match value + self.autoGrowTextField($this); + + var $inputs, isLastInput; + var enterPressed = (event.keyCode === 13); + var tabPressedAutoCheck = (event.keyCode === 9 && self.params.behaviour.autoCheck); + + if (enterPressed || tabPressedAutoCheck) { + // Figure out which inputs are left to answer + $inputs = self.$questions.find('.h5p-input-wrapper:not(.h5p-correct) .h5p-text-input'); + + // Figure out if this is the last input + isLastInput = $this.is($inputs[$inputs.length - 1]); + } + + if ((tabPressedAutoCheck && isLastInput && !self.shiftPressed) || + (enterPressed && isLastInput)) { + // Focus first button on next tick + setTimeout(function () { + self.focusButton(); + }, 10); + } + + if (enterPressed) { + if (isLastInput) { + // Check answers + $this.trigger('blur'); + } + else { + // Find next input to focus + $inputs.eq($inputs.index($this) + 1).focus(); + } + + return false; // Prevent form submission on enter key + } + }).on('change', function () { + self.answered = true; + self.triggerXAPI('interacted'); + }); + + self.on('resize', function () { + self.resetGrowTextField(); + }); + + return this.$questions; + }; + + /** + * + */ + Blanks.prototype.autoGrowTextField = function ($input) { + // Do not set text field size when separate lines is enabled + if (this.params.behaviour.separateLines) { + return; + } + + var self = this; + var fontSize = parseInt($input.css('font-size'), 10); + var minEm = 3; + var minPx = fontSize * minEm; + var rightPadEm = 3.25; + var rightPadPx = fontSize * rightPadEm; + var static_min_pad = 0.5 * fontSize; + + setTimeout(function () { + var tmp = $('
', { + 'text': $input.val() + }); + tmp.css({ + 'position': 'absolute', + 'white-space': 'nowrap', + 'font-size': $input.css('font-size'), + 'font-family': $input.css('font-family'), + 'padding': $input.css('padding'), + 'width': 'initial' + }); + $input.parent().append(tmp); + var width = tmp.width(); + var parentWidth = self.$questions.width(); + tmp.remove(); + if (width <= minPx) { + // Apply min width + $input.width(minPx + static_min_pad); + } + else if (width + rightPadPx >= parentWidth) { + // Apply max width of parent + $input.width(parentWidth - rightPadPx); + } + else { + // Apply width that wraps input + $input.width(width + static_min_pad); + } + }, 1); + }; + + /** + * Resize all text field growth to current size. + */ + Blanks.prototype.resetGrowTextField = function () { + var self = this; + + this.$questions.find('input').each(function () { + self.autoGrowTextField($(this)); + }); + }; + + /** + * Toggle buttons dependent of state. + * + * Using CSS-rules to conditionally show/hide using the data-attribute [data-state] + */ + Blanks.prototype.toggleButtonVisibility = function (state) { + // The show solutions button is hidden if all answers are correct + var allCorrect = (this.getScore() === this.getMaxScore()); + if (this.params.behaviour.autoCheck && allCorrect) { + // We are viewing the solutions + state = STATE_FINISHED; + } + + if (this.params.behaviour.enableSolutionsButton) { + if (state === STATE_CHECKING && !allCorrect) { + this.showButton('show-solution'); + } + else { + this.hideButton('show-solution'); + } + } + + if (this.params.behaviour.enableRetry) { + if ((state === STATE_CHECKING && !allCorrect) || state === STATE_SHOWING_SOLUTION) { + this.showButton('try-again'); + } + else { + this.hideButton('try-again'); + } + } + + if (state === STATE_ONGOING) { + this.showButton('check-answer'); + } + else { + this.hideButton('check-answer'); + } + + this.trigger('resize'); + }; + + /** + * Check if solution is allowed. Warn user if not + */ + Blanks.prototype.allowSolution = function () { + if (this.params.behaviour.showSolutionsRequiresInput === true) { + if (!this.allBlanksFilledOut()) { + this.updateFeedbackContent(this.params.notFilledOut); + this.read(this.params.notFilledOut); + return false; + } + } + return true; + }; + + /** + * Check if all blanks are filled out + * + * @method allBlanksFilledOut + * @return {boolean} Returns true if all blanks are filled out. + */ + Blanks.prototype.allBlanksFilledOut = function () { + return !this.clozes.some(function (cloze) { + return !cloze.filledOut(); + }); + }; + + /** + * Mark which answers are correct and which are wrong and disable fields if retry is off. + */ + Blanks.prototype.markResults = function () { + var self = this; + for (var i = 0; i < self.clozes.length; i++) { + self.clozes[i].checkAnswer(); + if (!self.params.behaviour.enableRetry) { + self.clozes[i].disableInput(); + } + } + this.trigger('resize'); + }; + + /** + * Removed marked results + */ + Blanks.prototype.removeMarkedResults = function () { + this.$questions.find('.h5p-input-wrapper').removeClass('h5p-correct h5p-wrong'); + this.$questions.find('.h5p-input-wrapper > input').attr('disabled', false); + this.trigger('resize'); + }; + + + /** + * Displays the correct answers + * @param {boolean} [alwaysShowSolution] + * Will always show solution if true + */ + Blanks.prototype.showCorrectAnswers = function (alwaysShowSolution) { + if (!alwaysShowSolution && !this.allowSolution()) { + return; + } + + this.toggleButtonVisibility(STATE_SHOWING_SOLUTION); + this.hideSolutions(); + + for (var i = 0; i < this.clozes.length; i++) { + this.clozes[i].showSolution(); + } + this.trigger('resize'); + }; + + /** + * Toggle input allowed for all input fields + * + * @method function + * @param {boolean} enabled True if fields should allow input, otherwise false + */ + Blanks.prototype.toggleAllInputs = function (enabled) { + for (var i = 0; i < this.clozes.length; i++) { + this.clozes[i].toggleInput(enabled); + } + }; + + /** + * Display the correct solution for the input boxes. + * + * This is invoked from CP and QS - be carefull! + */ + Blanks.prototype.showSolutions = function () { + this.params.behaviour.enableSolutionsButton = true; + this.toggleButtonVisibility(STATE_FINISHED); + this.markResults(); + this.showEvaluation(); + this.showCorrectAnswers(true); + this.toggleAllInputs(false); + //Hides all buttons in "show solution" mode. + this.hideButtons(); + }; + + /** + * Resets the complete task. + * Used in contracts. + * @public + */ + Blanks.prototype.resetTask = function () { + this.answered = false; + this.hideEvaluation(); + this.hideSolutions(); + this.clearAnswers(); + this.removeMarkedResults(); + this.toggleButtonVisibility(STATE_ONGOING); + this.resetGrowTextField(); + this.toggleAllInputs(true); + this.done = false; + }; + + /** + * Hides all buttons. + * @public + */ + Blanks.prototype.hideButtons = function () { + this.toggleButtonVisibility(STATE_FINISHED); + }; + + /** + * Trigger xAPI answered event + */ + Blanks.prototype.triggerAnswered = function () { + this.answered = true; + var xAPIEvent = this.createXAPIEventTemplate('answered'); + this.addQuestionToXAPI(xAPIEvent); + this.addResponseToXAPI(xAPIEvent); + this.trigger(xAPIEvent); + }; + + /** + * Get xAPI data. + * Contract used by report rendering engine. + * + * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} + */ + Blanks.prototype.getXAPIData = function () { + var xAPIEvent = this.createXAPIEventTemplate('answered'); + this.addQuestionToXAPI(xAPIEvent); + this.addResponseToXAPI(xAPIEvent); + return { + statement: xAPIEvent.data.statement + }; + }; + + /** + * Generate xAPI object definition used in xAPI statements. + * @return {Object} + */ + Blanks.prototype.getxAPIDefinition = function () { + var definition = {}; + definition.description = { + 'en-US': this.params.text + }; + definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; + definition.interactionType = 'fill-in'; + + const clozeSolutions = []; + let crp = ''; + // xAPI forces us to create solution patterns for all possible solution combinations + for (var i = 0; i < this.params.questions.length; i++) { + var question = this.handleBlanks(this.params.questions[i], function (solution) { + // Collect all solution combinations for the H5P Alternative extension + clozeSolutions.push(solution.solutions); + + // Create a basic response pattern out of the first alternative for each blanks field + crp += (!crp ? '' : '[,]') + solution.solutions[0]; + + // We replace the solutions in the question with a "blank" + return '__________'; + }); + definition.description['en-US'] += question; + } + + // Set the basic response pattern (not supporting multiple alternatives for blanks) + definition.correctResponsesPattern = [ + '{case_matters=' + this.params.behaviour.caseSensitive + '}' + crp, + ]; + + // Add the H5P Alternative extension which provides all the combinations of different answers + // Reporting software will need to support this extension for alternatives to work. + definition.extensions = definition.extensions || {}; + definition.extensions[XAPI_CASE_SENSITIVITY] = this.params.behaviour.caseSensitive; + definition.extensions[XAPI_ALTERNATIVE_EXTENSION] = clozeSolutions; + + return definition; + }; + + /** + * Add the question itselt to the definition part of an xAPIEvent + */ + Blanks.prototype.addQuestionToXAPI = function (xAPIEvent) { + var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); + $.extend(true, definition, this.getxAPIDefinition()); + + // Set reporting module version if alternative extension is used + if (this.hasAlternatives) { + const context = xAPIEvent.getVerifiedStatementValue(['context']); + context.extensions = context.extensions || {}; + context.extensions[XAPI_REPORTING_VERSION_EXTENSION] = '1.1.0'; + } + }; + + /** + * Parse the solution text (text between the asterisks) + * + * @param {string} solutionText + * @returns {object} with the following properties + * - tip: the tip text for this solution, undefined if no tip + * - solutions: array of solution words + */ + Blanks.prototype.parseSolution = function (solutionText) { + var tip, solution; + + var tipStart = solutionText.indexOf(':'); + if (tipStart !== -1) { + // Found tip, now extract + tip = solutionText.slice(tipStart + 1); + solution = solutionText.slice(0, tipStart); + } + else { + solution = solutionText; + } + + // Split up alternatives + var solutions = solution.split('/'); + this.hasAlternatives = this.hasAlternatives || solutions.length > 1; + + // Trim solutions + for (var i = 0; i < solutions.length; i++) { + solutions[i] = H5P.trim(solutions[i]); + + //decodes html entities + var elem = document.createElement('textarea'); + elem.innerHTML = solutions[i]; + solutions[i] = elem.value; + } + + return { + tip: tip, + solutions: solutions + }; + }; + + /** + * Add the response part to an xAPI event + * + * @param {H5P.XAPIEvent} xAPIEvent + * The xAPI event we will add a response to + */ + Blanks.prototype.addResponseToXAPI = function (xAPIEvent) { + xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this); + xAPIEvent.data.statement.result.response = this.getxAPIResponse(); + }; + + /** + * Generate xAPI user response, used in xAPI statements. + * @return {string} User answers separated by the "[,]" pattern + */ + Blanks.prototype.getxAPIResponse = function () { + var usersAnswers = this.getCurrentState(); + return usersAnswers.join('[,]'); + }; + + /** + * Show evaluation widget, i.e: 'You got x of y blanks correct' + */ + Blanks.prototype.showEvaluation = function () { + var maxScore = this.getMaxScore(); + var score = this.getScore(); + var scoreText = H5P.Question.determineOverallFeedback(this.params.overallFeedback, score / maxScore).replace('@score', score).replace('@total', maxScore); + + this.setFeedback(scoreText, score, maxScore, this.params.scoreBarLabel); + + if (score === maxScore) { + this.toggleButtonVisibility(STATE_FINISHED); + } + }; + + /** + * Hide the evaluation widget + */ + Blanks.prototype.hideEvaluation = function () { + // Clear evaluation section. + this.removeFeedback(); + }; + + /** + * Hide solutions. (/try again) + */ + Blanks.prototype.hideSolutions = function () { + // Clean solution from quiz + this.$questions.find('.h5p-correct-answer').remove(); + }; + + /** + * Get maximum number of correct answers. + * + * @returns {Number} Max points + */ + Blanks.prototype.getMaxScore = function () { + var self = this; + return self.clozes.length; + }; + + /** + * Count the number of correct answers. + * + * @returns {Number} Points + */ + Blanks.prototype.getScore = function () { + var self = this; + var correct = 0; + for (var i = 0; i < self.clozes.length; i++) { + if (self.clozes[i].correct()) { + correct++; + } + self.params.userAnswers[i] = self.clozes[i].getUserAnswer(); + } + + return correct; + }; + + Blanks.prototype.getTitle = function () { + return H5P.createTitle((this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Fill In'); + }; + + /** + * Clear the user's answers + */ + Blanks.prototype.clearAnswers = function () { + this.clozes.forEach(function (cloze) { + cloze.setUserInput(''); + cloze.resetAriaLabel(); + }); + }; + + /** + * Checks if all has been answered. + * + * @returns {Boolean} + */ + Blanks.prototype.getAnswerGiven = function () { + return this.answered || !this.hasClozes; + }; + + /** + * Helps set focus the given input field. + * @param {jQuery} $input + */ + Blanks.setFocus = function ($input) { + setTimeout(function () { + $input.focus(); + }, 1); + }; + + /** + * Returns an object containing content of each cloze + * + * @returns {object} object containing content for each cloze + */ + Blanks.prototype.getCurrentState = function () { + var clozesContent = []; + + // Get user input for every cloze + this.clozes.forEach(function (cloze) { + clozesContent.push(cloze.getUserAnswer()); + }); + return clozesContent; + }; + + /** + * Sets answers to current user state + */ + Blanks.prototype.setH5PUserState = function () { + var self = this; + var isValidState = (this.previousState !== undefined && + this.previousState.length && + this.previousState.length === this.clozes.length); + + // Check that stored user state is valid + if (!isValidState) { + return; + } + + // Set input from user state + var hasAllClozesFilled = true; + this.previousState.forEach(function (clozeContent, ccIndex) { + + // Register that an answer has been given + if (clozeContent.length) { + self.answered = true; + } + + var cloze = self.clozes[ccIndex]; + cloze.setUserInput(clozeContent); + + // Handle instant feedback + if (self.params.behaviour.autoCheck) { + if (cloze.filledOut()) { + cloze.checkAnswer(); + } + else { + hasAllClozesFilled = false; + } + } + }); + + if (self.params.behaviour.autoCheck && hasAllClozesFilled) { + self.showEvaluation(); + self.toggleButtonVisibility(STATE_CHECKING); + } + }; + + /** + * Disables any active input. Useful for freezing the task and dis-allowing + * modification of wrong answers. + */ + Blanks.prototype.disableInput = function () { + this.$questions.find('input').attr('disabled', true); + }; + + Blanks.idCounter = 0; + + return Blanks; +})(H5P.jQuery, H5P.Question); + +/** + * Static utility method for parsing H5P.Blanks qestion into a format useful + * for creating reports. + * + * Example question: 'H5P content may be edited using a *browser/web-browser:something you use every day*.' + * + * Produces the following result: + * [ + * { + * type: 'text', + * content: 'H5P content may be edited using a ' + * }, + * { + * type: 'answer', + * correct: ['browser', 'web-browser'] + * }, + * { + * type: 'text', + * content: '.' + * } + * ] + * + * @param {string} question + */ +H5P.Blanks.parseText = function (question) { + var blank = new H5P.Blanks({ question: question }); + + /** + * Parses a text into an array where words starting and ending + * with an asterisk are separated from other text. + * e.g ["this", "*is*", " an ", "*example*"] + * + * @param {string} text + * + * @return {string[]} + */ + function tokenizeQuestionText(text) { + return text.split(/(\*.*?\*)/).filter(function (str) { + return str.length > 0; } + ); + } + + function startsAndEndsWithAnAsterisk(str) { + return str.substr(0,1) === '*' && str.substr(-1) === '*'; + } + + function replaceHtmlTags(str, value) { + return str.replace(/<[^>]*>/g, value); + } + + return tokenizeQuestionText(replaceHtmlTags(question, '')).map(function (part) { + return startsAndEndsWithAnAsterisk(part) ? + ({ + type: 'answer', + correct: blank.parseSolution(part.slice(1, -1)).solutions + }) : + ({ + type: 'text', + content: part + }); + }); +}; diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js new file mode 100644 index 0000000000..8a60a7b430 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js @@ -0,0 +1,238 @@ +(function ($, Blanks) { + + /** + * Simple private class for keeping track of clozes. + * + * @class H5P.Blanks.Cloze + * @param {string} answer + * @param {Object} behaviour Behavioral settings for the task from semantics + * @param {boolean} behaviour.acceptSpellingErrors - If true, answers will also count correct if they contain small spelling errors. + * @param {string} defaultUserAnswer + * @param {Object} l10n Localized texts + * @param {string} l10n.solutionLabel Assistive technology label for cloze solution + * @param {string} l10n.inputLabel Assistive technology label for cloze input + * @param {string} l10n.inputHasTipLabel Assistive technology label for input with tip + * @param {string} l10n.tipLabel Label for tip icon + */ + Blanks.Cloze = function (solution, behaviour, defaultUserAnswer, l10n) { + var self = this; + var $input, $wrapper; + var answers = solution.solutions; + var answer = answers.join('/'); + var tip = solution.tip; + var checkedAnswer = null; + var inputLabel = l10n.inputLabel; + + if (behaviour.caseSensitive !== true) { + // Convert possible solutions into lowercase + for (var i = 0; i < answers.length; i++) { + answers[i] = answers[i].toLowerCase(); + } + } + + /** + * Check if the answer is correct. + * + * @private + * @param {string} answered + */ + var correct = function (answered) { + if (behaviour.caseSensitive !== true) { + answered = answered.toLowerCase(); + } + for (var i = 0; i < answers.length; i++) { + // Damerau-Levenshtein comparison + if (behaviour.acceptSpellingErrors === true) { + var levenshtein = H5P.TextUtilities.computeLevenshteinDistance(answered, H5P.trim(answers[i]), true); + /* + * The correctness is temporarily computed by word length and number of number of operations + * required to change one word into the other (Damerau-Levenshtein). It's subject to + * change, cmp. https://github.com/otacke/udacity-machine-learning-engineer/blob/master/submissions/capstone_proposals/h5p_fuzzy_blanks.md + */ + if ((answers[i].length > 9) && (levenshtein <= 2)) { + return true; + } else if ((answers[i].length > 3) && (levenshtein <= 1)) { + return true; + } + } + // regular comparison + if (answered === H5P.trim(answers[i])) { + return true; + } + } + return false; + }; + + /** + * Check if filled out. + * + * @param {boolean} + */ + this.filledOut = function () { + var answered = this.getUserAnswer(); + // Blank can be correct and is interpreted as filled out. + return (answered !== '' || correct(answered)); + }; + + /** + * Check the cloze and mark it as wrong or correct. + */ + this.checkAnswer = function () { + checkedAnswer = this.getUserAnswer(); + var isCorrect = correct(checkedAnswer); + if (isCorrect) { + $wrapper.addClass('h5p-correct'); + $input.attr('disabled', true) + .attr('aria-label', inputLabel + '. ' + l10n.answeredCorrectly); + } + else { + $wrapper.addClass('h5p-wrong'); + $input.attr('aria-label', inputLabel + '. ' + l10n.answeredIncorrectly); + } + }; + + /** + * Disables input. + * @method disableInput + */ + this.disableInput = function () { + this.toggleInput(false); + }; + + /** + * Enables input. + * @method enableInput + */ + this.enableInput = function () { + this.toggleInput(true); + }; + + /** + * Toggles input enable/disable + * @method toggleInput + * @param {boolean} enabled True if input should be enabled, otherwise false + */ + this.toggleInput = function (enabled) { + $input.attr('disabled', !enabled); + }; + + /** + * Show the correct solution. + */ + this.showSolution = function () { + if (correct(this.getUserAnswer())) { + return; // Only for the wrong ones + } + + $('', { + 'aria-hidden': true, + 'class': 'h5p-correct-answer', + text: H5P.trim(answer.replace(/\s*\/\s*/g, '/')), + insertAfter: $wrapper + }); + $input.attr('disabled', true); + var ariaLabel = inputLabel + '. ' + + l10n.solutionLabel + ' ' + answer + '. ' + + l10n.answeredIncorrectly; + + $input.attr('aria-label', ariaLabel); + }; + + /** + * @returns {boolean} + */ + this.correct = function () { + return correct(this.getUserAnswer()); + }; + + /** + * Set input element. + * + * @param {H5P.jQuery} $element + * @param {function} afterCheck + * @param {function} afterFocus + * @param {number} clozeIndex Index of cloze + * @param {number} totalCloze Total amount of clozes in blanks + */ + this.setInput = function ($element, afterCheck, afterFocus, clozeIndex, totalCloze) { + $input = $element; + $wrapper = $element.parent(); + inputLabel = inputLabel.replace('@num', (clozeIndex + 1)) + .replace('@total', totalCloze); + + // Add tip if tip is set + if(tip !== undefined && tip.trim().length > 0) { + $wrapper.addClass('has-tip') + .append(H5P.JoubelUI.createTip(tip, { + tipLabel: l10n.tipLabel + })); + inputLabel += '. ' + l10n.inputHasTipLabel; + } + + $input.attr('aria-label', inputLabel); + + if (afterCheck !== undefined) { + $input.blur(function () { + if (self.filledOut()) { + // Check answers + if (!behaviour.enableRetry) { + self.disableInput(); + } + self.checkAnswer(); + afterCheck.apply(self); + } + }); + } + $input.keyup(function () { + if (checkedAnswer !== null && checkedAnswer !== self.getUserAnswer()) { + // The Answer has changed since last check + checkedAnswer = null; + $wrapper.removeClass('h5p-wrong'); + $input.attr('aria-label', inputLabel); + if (afterFocus !== undefined) { + afterFocus(); + } + } + }); + }; + + /** + * @returns {string} Cloze html + */ + this.toString = function () { + var extra = defaultUserAnswer ? ' value="' + defaultUserAnswer + '"' : ''; + var result = ''; + self.length = result.length; + return result; + }; + + /** + * @returns {string} Trimmed answer + */ + this.getUserAnswer = function () { + const trimmedAnswer = H5P.trim($input.val().replace(/\ /g, ' ')); + // Set trimmed answer + $input.val(trimmedAnswer); + if (behaviour.formulaEditor) { + // If fomula editor is enabled set trimmed text + $input.parent().find('.wiris-h5p-input').html(trimmedAnswer); + } + return trimmedAnswer; + }; + + /** + * @param {string} text New input text + */ + this.setUserInput = function (text) { + $input.val(text); + }; + + /** + * Resets aria label of input field + */ + this.resetAriaLabel = function () { + $input.attr('aria-label', inputLabel); + }; + }; + +})(H5P.jQuery, H5P.Blanks); diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json new file mode 100644 index 0000000000..dbee84de69 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json @@ -0,0 +1,214 @@ +{ + "semantics": [ + { + "label": "Medieelement", + "fields": [ + { + "label": "Type", + "description": "Valgfritt medieelement. Elementet vil bli plassert over spørsmålet." + }, + { + "label": "Deaktiver zoomingfunksjon." + } + ] + }, + { + "label": "Oppgavebeskrivelse", + "default": "Fyll inn teksten som mangler", + "description": "En tekst som forteller brukeren hvordan oppgaven skal løses" + }, + { + "label": "Tekstlinjer", + "entity": "tekstlinje", + "field": { + "label": "Tekstlinje", + "placeholder": "Oslo er hovedstaden i *Norge*", + "important": { + "description": "
  • Åpne felter merkes med en stjerne * foran og bak det korrekte ordet/uttrykket.
  • Alternativer angis med en skråstrek (/)
  • Tips angis med et kolon (:) før tipset
", + "example": "H5P content may be edited using a *browser/web-browser:Something you use every day*." + } + } + }, + { + "label": "Tilbakemelding på hele oppgava", + "fields": [ + { + "widgets": [ + { + "label": "Forhandsinnstilling" + } + ], + "label": "Opprett poengområder og legg inn tilbakemeldinger.", + "description": "Klikk på knappen \"Legg til poengområde\" og legg til så mange poengområder du trenger. Eksempel: 0–40 % Svakt resultat, 41–80 % Gjennomsnittlig resultat, 81–100 % Flott resultat!", + "entity": "Område", + "field": { + "fields": [ + { + "label": "Poengområde" + }, + {}, + { + "label": "Tilbakemelding for definert poengområde", + "placeholder": "Skriv inn tilbakemelding." + } + ] + } + } + ] + }, + { + "label": "Tekst for \"Vis svar\" knapp", + "default": "Vis svar" + }, + { + "label": "Tekst for \"Prøv igjen\" knapp", + "default": "Prøv igjen" + }, + { + "label": "Tekst for \"Sjekk\" knapp", + "default": "Sjekk" + }, + { + "label": "Text for \"Submit\" button", + "default": "Submit" + }, + { + "label": "Text for \"Not filled out\" message", + "default": "Please fill in all blanks to view solution" + }, + { + "label": "Tekst for \"':ans' er korrekt\"-melding", + "default": "':ans' er korrekt" + }, + { + "label": "Tekst for \"':ans' er feil\"-melding", + "default": "':ans' er feil" + }, + { + "label": "Tekst for \"Svar rett\"-melding", + "default": "Svar rett" + }, + { + "label": "Tekst for \"Svar feil\"-melding", + "default": "Svar feil" + }, + { + "label": "Merkelapp for løsning brukt av tekniske hjelpemidler", + "default": "Svar:" + }, + { + "label": "Merkelapp for inntastingsfelt brukt av tekniske hjelpemidler", + "description": "Bruk @num og @total for å bytte ut feltnummer og totalt antall felter", + "default": "Felt @num av @total" + }, + { + "label": "Merkelapp for å fortelle at et inntastingsfelt har et tips knyttet til seg", + "default": "Tips tilgjengelig" + }, + { + "label": "Merkelapp for tips ikon", + "default": "Tips" + }, + { + "label": "Innstillinger for oppgave-oppførsel", + "description": "Disse instillingene lar deg bestemme hvordan oppgavetypen skal oppføre seg.", + "fields": [ + { + "label": "Aktiver \"Prøv igjen\"-knapp" + }, + { + "label": "Aktiver \"Fasit\"-knapp" + }, + { + "label": "Enable \"Check\" button" + }, + { + "label": "Gi tilbakemelding med en gang brukeren har avgitt svar" + }, + { + "label": "Skill mellom store og små bokstaver", + "description": "Sørger for at svaret til brukeren må være nøyaktig det samme som i oppgaven." + }, + { + "label": "Krev at alle felter er besvart før fasit gis" + }, + { + "label": "Sett inntastingsfelt på egne linjer" + }, + { + "label": "Slå på bruker-bekreftelse for \"Fasit\"", + "description": "Denne innstillingen er ikke forenbar med \"Gi tilbakemelding med en gang brukeren har avgitt svar\" alternativet." + }, + { + "label": "Slå på bruker-bekreftelse for \"Prøv igjen\"" + }, + { + "label": "Accept minor spelling errors", + "description": "If activated, an answer will also count as correct with minor spelling errors (3-9 characters: 1 spelling error, more than 9 characters: 2 spelling errors)" + } + ] + }, + { + "label": "Bruker-bekreftelse før fasit-visning", + "fields": [ + { + "label": "Tittel", + "default": "Ferdig ?" + }, + { + "label": "Tekst", + "default": "Er du sikker på at du er ferdig?" + }, + { + "label": "Avbryt etikett", + "default": "Avbryt" + }, + { + "label": "Bekreftelse etikett", + "default": "Bekreft" + } + ] + }, + { + "label": "Prøv igjen bruker-bekreftelse", + "fields": [ + { + "label": "Tittel", + "default": "Prøv igjen ?" + }, + { + "label": "Tekst", + "default": "Er du sikker på at du vil prøve igjen?" + }, + { + "label": "Avbryt etikett", + "default": "Avbryt" + }, + { + "label": "Bekreft etikett", + "default": "Bekreft" + } + ] + }, + { + "label": "Textual representation of the score bar for those using a readspeaker", + "default": "You got :num out of :total points" + }, + { + "label": "Assistive technology description for \"Check\" button", + "default": "Check the answers. The responses will be marked as correct, incorrect, or unanswered." + }, + { + "label": "Assistive technology description for \"Show Solution\" button", + "default": "Show the solution. The task will be marked with its correct solution." + }, + { + "label": "Assistive technology description for \"Retry\" button", + "default": "Retry the task. Reset all responses and start the task over again." + }, + { + "label": "Assistive technology description for starting task", + "default": "Checking mode" + } + ] +} diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json new file mode 100644 index 0000000000..562429f120 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json @@ -0,0 +1,65 @@ +{ + "title": "Fill in the Blanks", + "description": "Test your users with fill in the blanks tasks(Cloze tests).", + "machineName": "H5P.Blanks", + "majorVersion": 1, + "minorVersion": 14, + "patchVersion": 6, + "runnable": 1, + "license": "MIT", + "author": "Joubel", + "embedTypes": [ + "iframe" + ], + "coreApi": { + "majorVersion": 1, + "minorVersion": 19 + }, + "preloadedCss": [ + { + "path": "css/blanks.css" + } + ], + "preloadedJs": [ + { + "path": "js/blanks.js" + }, + { + "path": "js/cloze.js" + } + ], + "preloadedDependencies": [ + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "H5P.Question", + "majorVersion": 1, + "minorVersion": 5 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.TextUtilities", + "majorVersion": 1, + "minorVersion": 3 + } + ], + "editorDependencies": [ + { + "machineName": "H5PEditor.RangeList", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5PEditor.ShowWhen", + "majorVersion": 1, + "minorVersion": 0 + } + ] +} \ No newline at end of file diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js new file mode 100644 index 0000000000..947c9340cc --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js @@ -0,0 +1,40 @@ +var H5PPresave = H5PPresave || {}; +/** + * Resolve the presave logic for the content type Fill in the Blanks + * + * @param {object} content + * @param finished + * @constructor + */ +H5PPresave['H5P.Blanks'] = function (content, finished) { + var presave = H5PEditor.Presave; + + if (isContentInvalid()) { + throw { + name: 'Invalid Fill in the blanks Error', + message: 'Could not find expected semantics in content.' + }; + } + + var score = content.questions + .map(function (question) { + var pattern = /\*[^\*]+\*/g; + var matches = question.match(pattern); + return Array.isArray(matches) ? matches.length : 0; + }) + .reduce(function (previous, current) { + return previous + current; + }, 0); + + presave.validateScore(score); + + finished({maxScore: score}); + + /** + * Check if required parameters is present + * @return {boolean} + */ + function isContentInvalid() { + return !presave.checkNestedRequirements(content, 'content.questions') || !Array.isArray(content.questions); + } +}; diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json new file mode 100644 index 0000000000..8cce012481 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json @@ -0,0 +1,489 @@ +[ + { + "name": "media", + "type": "group", + "label": "Media", + "importance": "medium", + "fields": [ + { + "name": "type", + "type": "library", + "label": "Type", + "options": [ + "H5P.Image 1.1", + "H5P.Video 1.6", + "H5P.Audio 1.5" + ], + "optional": true, + "description": "Optional media to display above the question." + }, + { + "name": "disableImageZooming", + "type": "boolean", + "label": "Disable image zooming", + "importance": "low", + "default": false, + "optional": true, + "widget": "showWhen", + "showWhen": { + "rules": [ + { + "field": "type", + "equals": "H5P.Image 1.1" + } + ] + } + } + ] + }, + { + "label": "Task description", + "importance": "high", + "name": "text", + "type": "text", + "widget": "html", + "default": "Fill in the missing words", + "description": "A guide telling the user how to answer this task.", + "enterMode": "p", + "tags": [ + "strong", + "em", + "u", + "a", + "ul", + "ol", + "h2", + "h3", + "hr", + "pre", + "code" + ] + }, + { + "name": "questions", + "type": "list", + "label": "Text blocks", + "importance": "high", + "entity": "text block", + "min": 1, + "max": 31, + "field": { + "name": "question", + "type": "text", + "widget": "html", + "label": "Line of text", + "importance": "high", + "placeholder": "Oslo is the capital of *Norway*.", + "description": "", + "important": { + "description": "
  • Blanks are added with an asterisk (*) in front and behind the correct word/phrase.
  • Alternative answers are separated with a forward slash (/).
  • You may add a textual tip, using a colon (:) in front of the tip.
", + "example": "H5P content may be edited using a *browser/web-browser:Something you use every day*." + }, + "enterMode": "p", + "tags": [ + "strong", + "em", + "del", + "u", + "code" + ] + } + }, + { + "name": "overallFeedback", + "type": "group", + "label": "Overall Feedback", + "importance": "low", + "expanded": true, + "fields": [ + { + "name": "overallFeedback", + "type": "list", + "widgets": [ + { + "name": "RangeList", + "label": "Default" + } + ], + "importance": "high", + "label": "Define custom feedback for any score range", + "description": "Click the \"Add range\" button to add as many ranges as you need. Example: 0-20% Bad score, 21-91% Average Score, 91-100% Great Score!", + "entity": "range", + "min": 1, + "defaultNum": 1, + "optional": true, + "field": { + "name": "overallFeedback", + "type": "group", + "importance": "low", + "fields": [ + { + "name": "from", + "type": "number", + "label": "Score Range", + "min": 0, + "max": 100, + "default": 0, + "unit": "%" + }, + { + "name": "to", + "type": "number", + "min": 0, + "max": 100, + "default": 100, + "unit": "%" + }, + { + "name": "feedback", + "type": "text", + "label": "Feedback for defined score range", + "importance": "low", + "placeholder": "Fill in the feedback", + "optional": true + } + ] + } + } + ] + }, + { + "label": "Text for \"Show solutions\" button", + "name": "showSolutions", + "type": "text", + "default": "Show solution", + "common": true + }, + { + "label": "Text for \"Retry\" button", + "importance": "low", + "name": "tryAgain", + "type": "text", + "default": "Retry", + "common": true, + "optional": true + }, + { + "label": "Text for \"Check\" button", + "importance": "low", + "name": "checkAnswer", + "type": "text", + "default": "Check", + "common": true, + "optional": true + }, + { + "label": "Text for \"Submit\" button", + "importance": "low", + "name": "submitAnswer", + "type": "text", + "default": "Submit", + "common": true, + "optional": true + }, + { + "label": "Text for \"Not filled out\" message", + "importance": "low", + "name": "notFilledOut", + "type": "text", + "default": "Please fill in all blanks to view solution", + "common": true, + "optional": true + }, + { + "label": "Text for \"':ans' is correct\" message", + "importance": "low", + "name": "answerIsCorrect", + "type": "text", + "default": "':ans' is correct", + "common": true, + "optional": true + }, + { + "label": "Text for \"':ans' is wrong\" message", + "importance": "low", + "name": "answerIsWrong", + "type": "text", + "default": "':ans' is wrong", + "common": true, + "optional": true + }, + { + "label": "Text for \"Answered correctly\" message", + "importance": "low", + "name": "answeredCorrectly", + "type": "text", + "default": "Answered correctly", + "common": true, + "optional": true + }, + { + "label": "Text for \"Answered incorrectly\" message", + "importance": "low", + "name": "answeredIncorrectly", + "type": "text", + "default": "Answered incorrectly", + "common": true, + "optional": true + }, + { + "label": "Assistive technology label for solution", + "importance": "low", + "name": "solutionLabel", + "type": "text", + "default": "Correct answer:", + "common": true, + "optional": true + }, + { + "label": "Assistive technology label for input field", + "importance": "low", + "name": "inputLabel", + "type": "text", + "description": "Use @num and @total to replace current cloze number and total cloze number", + "default": "Blank input @num of @total", + "common": true, + "optional": true + }, + { + "label": "Assistive technology label for saying an input has a tip tied to it", + "importance": "low", + "name": "inputHasTipLabel", + "type": "text", + "default": "Tip available", + "common": true, + "optional": true + }, + { + "label": "Tip icon label", + "importance": "low", + "name": "tipLabel", + "type": "text", + "default": "Tip", + "common": true, + "optional": true + }, + { + "name": "behaviour", + "type": "group", + "label": "Behavioural settings.", + "importance": "low", + "description": "These options will let you control how the task behaves.", + "optional": true, + "fields": [ + { + "label": "Enable \"Retry\"", + "importance": "low", + "name": "enableRetry", + "type": "boolean", + "default": true, + "optional": true + }, + { + "label": "Enable \"Show solution\" button", + "importance": "low", + "name": "enableSolutionsButton", + "type": "boolean", + "default": true, + "optional": true + }, + { + "name": "enableCheckButton", + "type": "boolean", + "label": "Enable \"Check\" button", + "widget": "none", + "importance": "low", + "default": true, + "optional": true + }, + { + "label": "Automatically check answers after input", + "importance": "low", + "name": "autoCheck", + "type": "boolean", + "default": false, + "optional": true + }, + { + "name": "caseSensitive", + "importance": "low", + "type": "boolean", + "default": true, + "label": "Case sensitive", + "description": "Makes sure the user input has to be exactly the same as the answer." + }, + { + "label": "Require all fields to be answered before the solution can be viewed", + "importance": "low", + "name": "showSolutionsRequiresInput", + "type": "boolean", + "default": true, + "optional": true + }, + { + "label": "Put input fields on separate lines", + "importance": "low", + "name": "separateLines", + "type": "boolean", + "default": false, + "optional": true + }, + { + "label": "Show confirmation dialog on \"Check\"", + "importance": "low", + "name": "confirmCheckDialog", + "type": "boolean", + "description": "This options is not compatible with the \"Automatically check answers after input\" option", + "default": false + }, + { + "label": "Show confirmation dialog on \"Retry\"", + "importance": "low", + "name": "confirmRetryDialog", + "type": "boolean", + "default": false + }, + { + "name": "acceptSpellingErrors", + "type": "boolean", + "label": "Accept minor spelling errors", + "importance": "low", + "description": "If activated, an answer will also count as correct with minor spelling errors (3-9 characters: 1 spelling error, more than 9 characters: 2 spelling errors)", + "default": false, + "optional": true + } + ] + }, + { + "label": "Check confirmation dialog", + "importance": "low", + "name": "confirmCheck", + "type": "group", + "common": true, + "fields": [ + { + "label": "Header text", + "importance": "low", + "name": "header", + "type": "text", + "default": "Finish ?" + }, + { + "label": "Body text", + "importance": "low", + "name": "body", + "type": "text", + "default": "Are you sure you wish to finish ?", + "widget": "html", + "enterMode": "p", + "tags": [ + "strong", + "em", + "del", + "u", + "code" + ] + }, + { + "label": "Cancel button label", + "importance": "low", + "name": "cancelLabel", + "type": "text", + "default": "Cancel" + }, + { + "label": "Confirm button label", + "importance": "low", + "name": "confirmLabel", + "type": "text", + "default": "Finish" + } + ] + }, + { + "label": "Retry confirmation dialog", + "importance": "low", + "name": "confirmRetry", + "type": "group", + "common": true, + "fields": [ + { + "label": "Header text", + "importance": "low", + "name": "header", + "type": "text", + "default": "Retry ?" + }, + { + "label": "Body text", + "importance": "low", + "name": "body", + "type": "text", + "default": "Are you sure you wish to retry ?", + "widget": "html", + "enterMode": "p", + "tags": [ + "strong", + "em", + "del", + "u", + "code" + ] + }, + { + "label": "Cancel button label", + "importance": "low", + "name": "cancelLabel", + "type": "text", + "default": "Cancel" + }, + { + "label": "Confirm button label", + "importance": "low", + "name": "confirmLabel", + "type": "text", + "default": "Confirm" + } + ] + }, + { + "name": "scoreBarLabel", + "type": "text", + "label": "Textual representation of the score bar for those using a readspeaker", + "default": "You got :num out of :total points", + "importance": "low", + "common": true + }, + { + "name": "a11yCheck", + "type": "text", + "label": "Assistive technology description for \"Check\" button", + "default": "Check the answers. The responses will be marked as correct, incorrect, or unanswered.", + "importance": "low", + "common": true + }, + { + "name": "a11yShowSolution", + "type": "text", + "label": "Assistive technology description for \"Show Solution\" button", + "default": "Show the solution. The task will be marked with its correct solution.", + "importance": "low", + "common": true + }, + { + "name": "a11yRetry", + "type": "text", + "label": "Assistive technology description for \"Retry\" button", + "default": "Retry the task. Reset all responses and start the task over again.", + "importance": "low", + "common": true + }, + { + "name": "a11yCheckingModeHeader", + "type": "text", + "label": "Assistive technology description for starting task", + "default": "Checking mode", + "importance": "low", + "common": true + } +] \ No newline at end of file diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js new file mode 100644 index 0000000000..bf3ea18e60 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js @@ -0,0 +1,124 @@ +var H5PUpgrades = H5PUpgrades || {}; + +H5PUpgrades['H5P.Blanks'] = (function () { + return { + 1: { + 1: { + contentUpgrade: function (parameters, finished) { + // Moved all behavioural settings into "behaviour" group. + parameters.behaviour = { + enableRetry: parameters.enableTryAgain === undefined ? true : parameters.enableRetry, + enableSolutionsButton: true, + autoCheck: parameters.autoCheck === undefined ? false : parameters.autoCheck, + caseSensitive: parameters.caseSensitive === undefined ? true : parameters.caseSensitive, + showSolutionsRequiresInput: parameters.showSolutionsRequiresInput === undefined ? true : parameters.showSolutionsRequiresInput, + separateLines: parameters.separateLines === undefined ? false : parameters.separateLines + }; + delete parameters.enableTryAgain; + delete parameters.enableShowSolution; + delete parameters.autoCheck; + delete parameters.caseSensitive; + delete parameters.showSolutionsRequiresInput; + delete parameters.separateLines; + delete parameters.changeAnswer; + + finished(null, parameters); + } + }, + + /** + * Asynchronous content upgrade hook. + * Upgrades content parameters to support Blanks 1.5. + * + * Converts task image into media object, adding support for video. + * + * @params {Object} parameters + * @params {function} finished + */ + 5: function (parameters, finished) { + + if (parameters.image) { + // Convert image field to media field + parameters.media = { + library: 'H5P.Image 1.0', + params: { + file: parameters.image + } + }; + + // Remove old image field + delete parameters.image; + } + + // Done + finished(null, parameters); + }, + + /** + * Asynchronous content upgrade hook. + * Upgrades content parameters to support Blanks 1.8 + * + * Move old feedback message to the new overall feedback system. + * + * @param {object} parameters + * @param {function} finished + */ + 8: function (parameters, finished) { + if (parameters && parameters.score) { + parameters.overallFeedback = [ + { + 'from': 0, + 'to': 100, + 'feedback': parameters.score + } + ]; + + delete parameters.score; + } + + finished(null, parameters); + }, + + /** + * Asynchronous content upgrade hook. + * + * @param {[type]} parameters + * @param {[type]} finished + * @param {[type]} extras + * @return {[type]} + */ + 11: function (parameters, finished, extras) { + // Move value from getTitle() to metadata title + if (parameters && parameters.text) { + extras = extras || {}; + extras.metadata = extras.metadata || {}; + extras.metadata.title = parameters.text.replace(/<[^>]*>?/g, ''); + } + finished(null, parameters, extras); + }, + /* + * Upgrades content parameters to support Blanks 1.9 + * + * Move disableImageZooming from behaviour to media + * + * @param {object} parameters + * @param {function} finished + */ + 12: function (parameters, finished) { + // If image has been used, move it down in the hierarchy and add disableImageZooming + if (parameters && parameters.media) { + parameters.media = { + type: parameters.media, + disableImageZooming: (parameters.behaviour && parameters.behaviour.disableImageZooming) ? parameters.behaviour.disableImageZooming : false + }; + } + + // Delete old disableImageZooming + if (parameters && parameters.behaviour) { + delete parameters.behaviour.disableImageZooming; + } + finished(null, parameters); + } + } + }; +})(); From c26c53bcbcd37dca0d06955673ab029a42a85845 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Fri, 7 Jul 2023 12:13:54 +0200 Subject: [PATCH 19/22] Fix unittest --- .../Integration/Http/Controllers/H5PControllerTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php index 8f0db74077..3d80f89e3e 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php @@ -118,10 +118,10 @@ public function testCreate(string $adapterMode, ?string $contentType): void public function provider_testCreate(): \Generator { - yield 'withoutContentType' => ['cerpus', null]; - yield 'withoutContentType' => ['ndla', null]; - yield 'withContentType' => ['cerpus', 'H5P.Toolbar 1.2']; - yield 'withContentType' => ['ndla', 'H5P.Toolbar 1.2']; + yield 'cerpus-withoutContentType' => ['cerpus', null]; + yield 'ndla-withoutContentType' => ['ndla', null]; + yield 'cerpus-withContentType' => ['cerpus', 'H5P.Toolbar 1.2']; + yield 'ndla-withContentType' => ['ndla', 'H5P.Toolbar 1.2']; } /** @dataProvider provider_adapterMode */ From adceeac67d65fc291e1ddfb57edf258d2345daef Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Wed, 20 Sep 2023 15:11:37 +0200 Subject: [PATCH 20/22] For libraries with patch version: Fix error switching language in H5P in editor. Fix libraries fail to load resources from own folder --- sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php | 2 +- .../apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php index b461aa8801..0e1a5e2731 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php @@ -102,7 +102,7 @@ public function getTranslations($libraries, $language_code) }) ->get() ->mapWithKeys(function ($library) { - return [$library->library->getLibraryString() => $library->translation]; + return [$library->library->getLibraryString(false) => $library->translation]; }) ->toArray(); } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php index 0aaee9ae4a..581d16c676 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php @@ -76,7 +76,7 @@ public function loadContent(int $id): static ); $this->contentConfig['exportUrl'] = route('content-download', ['h5p' => $this->content['id']]); - $this->contentConfig['library'] = $this->h5pCore->libraryToString($this->content['library']); + $this->contentConfig['library'] = $this->content['libraryFullVersionName']; $this->contentConfig['fullScreen'] = $this->content['library']['fullscreen']; $this->contentConfig['title'] = $this->content['title']; $this->contentConfig['metadata'] = $this->content['metadata']; From 7439d86ab830f435e197790a1c62e5b4b3a14b79 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Mon, 25 Sep 2023 09:24:22 +0200 Subject: [PATCH 21/22] CA tests: Remove excessive mocking, move misplaced test, add tests for previously modified code, fix some tests marked as incomplete --- .../Libraries/H5P/AjaxRequestTest.php | 60 ++++++--------- .../Libraries/H5P/EditorAjaxTest.php | 75 +++++++++++++++++++ .../Libraries/H5P/FrameworkTest.php | 30 ++++---- .../H5P/Storage/H5pCerpusStorageTest.php | 64 ++++++++++++---- .../Models}/H5PLibraryTest.php | 2 +- 5 files changed, 165 insertions(+), 66 deletions(-) create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php rename sourcecode/apis/contentauthor/tests/{Unit => Integration/Models}/H5PLibraryTest.php (99%) diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php index 572226184b..ba807412a1 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php @@ -3,10 +3,7 @@ namespace Tests\Integration\Libraries\H5P; use App\H5PLibrary; -use App\H5PLibraryLibrary; -use App\Libraries\ContentAuthorStorage; use App\Libraries\H5P\AjaxRequest; -use App\Libraries\H5P\Framework; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -25,58 +22,43 @@ public function test_libraryRebuild(): void /** @var H5PLibrary $edLib */ $edLib = H5PLibrary::factory()->create(['name' => 'FontOk', 'major_version' => 1, 'minor_version' => 3]); - H5PLibraryLibrary::create(['library_id' => $library->id, 'required_library_id' => $preLib->id, 'dependency_type' => 'preloaded']); - H5PLibraryLibrary::create(['library_id' => $library->id, 'required_library_id' => $dynLib->id, 'dependency_type' => 'dynamic']); - H5PLibraryLibrary::create(['library_id' => $library->id, 'required_library_id' => $edLib->id, 'dependency_type' => 'editor']); - - $storage = $this->createMock(ContentAuthorStorage::class); - $this->instance(ContentAuthorStorage::class, $storage); - $storage - ->expects($this->exactly(4)) - ->method('copyFolder'); - - $framework = $this->createMock(Framework::class); - $this->instance(Framework::class, $framework); - $framework->expects($this->exactly(4)) - ->method('deleteLibraryDependencies') - ->withConsecutive([$library->id], [$preLib->id], [$dynLib->id], [$edLib->id]); - $framework->expects($this->exactly(3)) - ->method('saveLibraryDependencies') - ->withConsecutive( - [$library->id, [['machineName' => 'player', 'majorVersion' => 3, 'minorVersion' => 14]], 'preloaded'], - [$library->id, [['machineName' => 'H5P.Dynamic', 'majorVersion' => 2, 'minorVersion' => 42]], 'dynamic'], - [$library->id, [['machineName' => 'FontOk', 'majorVersion' => 1, 'minorVersion' => 3]], 'editor'], - ); - $framework - ->expects($this->exactly(8)) - ->method('getH5pPath') - ->withConsecutive(['libraries'], ['libraries/H5P.Foobar-1.2']) - ->willReturnOnConsecutiveCalls('libraries', 'libraries/H5P.Foobar-1.2'); - - $core = $this->createMock(\H5PCore::class); - $this->instance(\H5PCore::class, $core); - $core->h5pF = $framework; - $core->expects($this->once())->method('mayUpdateLibraries')->willReturn(true); + $this->assertDatabaseEmpty('h5p_libraries_libraries'); $validator = $this->createMock(\H5PValidator::class); $this->instance(\H5PValidator::class, $validator); - $validator->h5pF = $framework; $validator ->expects($this->exactly(4)) ->method('getLibraryData') ->withConsecutive(['H5P.Foobar-1.2'], ['player-3.14'], ['H5P.Dynamic-2.42.3'], ['FontOk-1.3']) ->willReturnOnConsecutiveCalls([ - 'preloadedDependencies' => [['machineName' => 'player', 'majorVersion' => 3, 'minorVersion' => 14]], - 'dynamicDependencies' => [['machineName' => 'H5P.Dynamic', 'majorVersion' => 2, 'minorVersion' => 42]], - 'editorDependencies' => [['machineName' => 'FontOk', 'majorVersion' => 1, 'minorVersion' => 3]], + 'preloadedDependencies' => [$preLib->getLibraryH5PFriendly()], + 'dynamicDependencies' => [$dynLib->getLibraryH5PFriendly()], + 'editorDependencies' => [$edLib->getLibraryH5PFriendly()], ], [], [], []); $this + ->withSession(['isAdmin' => true]) ->post('/ajax', ['action' => AjaxRequest::LIBRARY_REBUILD, 'libraryId' => $library->id]) ->assertOk() ->assertJson([ 'success' => true, 'message' => 'Library rebuild', ]); + + $this->assertDatabaseHas('h5p_libraries_libraries', [ + 'library_id' => $library->id, + 'required_library_id' => $preLib->id, + 'dependency_type' => 'preloaded', + ]); + $this->assertDatabaseHas('h5p_libraries_libraries', [ + 'library_id' => $library->id, + 'required_library_id' => $dynLib->id, + 'dependency_type' => 'dynamic', + ]); + $this->assertDatabaseHas('h5p_libraries_libraries', [ + 'library_id' => $library->id, + 'required_library_id' => $edLib->id, + 'dependency_type' => 'editor', + ]); } } diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php new file mode 100644 index 0000000000..201ca0a7a6 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php @@ -0,0 +1,75 @@ +create(); + /** @var H5PLibrary $libTest */ + $libTest = H5PLibrary::factory()->create([ + 'name' => 'H5P.UnitTest', + 'major_version' => 3, + 'minor_version' => 14, + 'patch_version' => 42, + 'patch_version_in_folder_name' => 1, + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libFoo->id, + 'language_code' => 'nb', + 'translation' => json_encode(['lib' => $libFoo->getLibraryString(false), 'lang' => 'nb']), + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libTest->id, + 'language_code' => 'nb', + 'translation' => json_encode(['lib' => $libTest->getLibraryString(false), 'lang' => 'nb']), + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libFoo->id, + 'language_code' => 'nn', + 'translation' => json_encode(['lib' => $libFoo->getLibraryString(false), 'lang' => 'nn']), + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libTest->id, + 'language_code' => 'en', + 'translation' => json_encode(['lib' => $libTest->getLibraryString(false), 'lang' => 'en']), + ]); + + $translations = (new EditorAjax())->getTranslations( + [ + $libFoo->getLibraryString(false), + $libTest->getLibraryString(false), + ], + 'nb' + ); + + $this->assertIsArray($translations); + + $this->assertArrayHasKey($libFoo->getLibraryString(false), $translations); + $data = json_decode($translations[$libFoo->getLibraryString(false)], true, JSON_THROW_ON_ERROR); + $this->assertSame($libFoo->getLibraryString(false), $data['lib']); + $this->assertSame('nb', $data['lang']); + + $this->assertArrayHasKey($libFoo->getLibraryString(false), $translations); + $data = json_decode($translations[$libTest->getLibraryString(false)], true, JSON_THROW_ON_ERROR); + $this->assertSame($libTest->getLibraryString(false), $data['lib']); + $this->assertSame('nb', $data['lang']); + + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php index d3b2a8ec7a..2c35da2c6d 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php @@ -7,8 +7,8 @@ use App\H5PContent; use App\H5PLibrary; use App\H5PLibraryLibrary; +use App\Libraries\ContentAuthorStorage; use App\Libraries\H5P\Framework; -use App\Libraries\H5P\Storage\H5PCerpusStorage; use ArrayObject; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; @@ -16,6 +16,7 @@ use GuzzleHttp\Middleware; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Storage; use PDO; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -156,26 +157,29 @@ public function testLoadLibrary(): void /** @dataProvider provider_usePatch */ public function test_deleteLibrary($usePatch): void { + $disk = Storage::fake(); + $caStorage = App(ContentAuthorStorage::class); + $tmpDisk = Storage::fake($caStorage->getH5pTmpDiskName()); + /** @var H5PLibrary $library */ $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); + $path = 'libraries/' . $library->getFolderName(); + + $this->assertFalse($disk->exists($path)); + $disk->put($path . '/library.json', 'just testing'); + $this->assertTrue($disk->exists($path . '/library.json')); - $storage = $this->createMock(H5PCerpusStorage::class); - $this->instance(H5PCerpusStorage::class, $storage); - $storage - ->expects($this->once()) - ->method('deleteLibrary') - ->with([ - 'machineName' => $library->name, - 'majorVersion' => $library->major_version, - 'minorVersion' => $library->minor_version, - 'patchVersion' => $library->patch_version, - 'patchVersionInFolderName' => $library->patch_version_in_folder_name, - ]); + $this->assertFalse($tmpDisk->exists($path)); + $tmpDisk->put($path . '/library.json', 'just testing'); + $this->assertTrue($tmpDisk->exists($path . '/library.json')); $lib = ['id' => $library->id]; $this->assertDatabaseHas('h5p_libraries', $lib); $this->framework->deleteLibrary((object) $lib); $this->assertDatabaseMissing('h5p_libraries', $lib); + + $this->assertFalse($disk->exists($path)); + $this->assertFalse($tmpDisk->exists($path)); } /** @dataProvider provider_usePatch */ diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php index 8a631e8f4e..b40e010a0c 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php @@ -2,24 +2,23 @@ namespace Tests\Integration\Libraries\H5P\Storage; +use App\H5PLibrary; use App\Libraries\ContentAuthorStorage; +use App\Libraries\DataObjects\ContentStorageSettings; use App\Libraries\H5P\Storage\H5PCerpusStorage; use App\Libraries\H5P\Video\NullVideoAdapter; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Psr\Log\NullLogger; use Tests\TestCase; class H5pCerpusStorageTest extends TestCase { - protected function setUp(): void - { - $this->markTestIncomplete('Fix these later'); - } + use RefreshDatabase; public function test_correct_url_without_cdn_prefix() { - $disk = Storage::fake('fake'); - + $disk = Storage::fake('test'); $disk->put('test.txt', 'some content'); $cerpusStorage = new H5pCerpusStorage( @@ -28,27 +27,26 @@ public function test_correct_url_without_cdn_prefix() new NullVideoAdapter(), ); - $this->assertEquals("/test.txt", $cerpusStorage->getFileUrl('test.txt')); + $this->assertEquals("http://localhost/content/assets/test.txt", $cerpusStorage->getFileUrl('test.txt')); } public function test_correct_url_with_cdn_prefix() { - $disk = Storage::fake('fake'); - + $disk = Storage::fake('test'); $disk->put('test.txt', 'some content'); $cerpusStorage = new H5pCerpusStorage( - new ContentAuthorStorage(''), + new ContentAuthorStorage('https://not.localhost.test/prefix/'), new NullLogger(), new NullVideoAdapter(), ); - $this->assertEquals("http://test/aaa/test.txt", $cerpusStorage->getFileUrl('test.txt')); + $this->assertEquals("https://not.localhost.test/prefix/test.txt", $cerpusStorage->getFileUrl('test.txt')); } public function test_correct_url_when_file_not_found() { - $disk = Storage::fake('fake'); + Storage::fake('test'); $cerpusStorage = new H5pCerpusStorage( new ContentAuthorStorage(''), @@ -56,7 +54,47 @@ public function test_correct_url_when_file_not_found() new NullVideoAdapter(), ); - $this->assertEquals("", $cerpusStorage->getFileUrl('test.txt')); } + + /** @dataProvider provide_test_getUpdateScript */ + public function test_getUpgradeScript(array $libConfig): void + { + $disk = Storage::fake(); + + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create($libConfig); + $file = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $library->getFolderName()); + + $this->assertFalse($disk->exists($file)); + $disk->put($file, 'just testing'); + $this->assertTrue($disk->exists($file)); + + $cerpusStorage = new H5pCerpusStorage( + new ContentAuthorStorage(''), + new NullLogger(), + new NullVideoAdapter(), + ); + + $path = $cerpusStorage->getUpgradeScript($library->name, $library->major_version, $library->minor_version); + + $this->assertStringContainsString($file, $path); + } + + public function provide_test_getUpdateScript(): \Generator + { + yield 'withoutPatch' => [[ + 'name' => 'H5P.Blanks', + 'major_version' => 1, + 'minor_version' => 11, + ]]; + + yield 'withPatch' => [[ + 'name' => 'H5P.Blanks', + 'major_version' => 1, + 'minor_version' => 14, + 'patch_version' => 6, + 'patch_version_in_folder_name' => 1, + ]]; + } } diff --git a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php b/sourcecode/apis/contentauthor/tests/Integration/Models/H5PLibraryTest.php similarity index 99% rename from sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php rename to sourcecode/apis/contentauthor/tests/Integration/Models/H5PLibraryTest.php index dac20163b2..5484e64fef 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/H5PLibraryTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Models/H5PLibraryTest.php @@ -1,6 +1,6 @@ Date: Mon, 25 Sep 2023 07:39:58 +0000 Subject: [PATCH 22/22] Run php-cs-fixer --- .../tests/Integration/Libraries/H5P/EditorAjaxTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php index 201ca0a7a6..4be0882b9f 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php @@ -70,6 +70,5 @@ public function test_getTranslations(): void $data = json_decode($translations[$libTest->getLibraryString(false)], true, JSON_THROW_ON_ERROR); $this->assertSame($libTest->getLibraryString(false), $data['lib']); $this->assertSame('nb', $data['lang']); - } }