diff --git a/.distignore b/.distignore index 6d56a32..cba0493 100644 --- a/.distignore +++ b/.distignore @@ -74,6 +74,7 @@ sftp-config.json AGENTS.md EXELEARNING_CHANGES.md playwright.config.* +vitest.config.* scripts cloudflare-worker mkdocs.yml diff --git a/.gitattributes b/.gitattributes index 1c98209..27e481e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -48,6 +48,7 @@ sftp-config.json export-ignore sonar-project.properties export-ignore test.php export-ignore tests/ export-ignore +vitest.config.* export-ignore # eXeLearning submodule (built editor is in dist/static/) exelearning/ export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f924c3..508349b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,10 @@ jobs: - name: Run code linting (PHPCS) run: ./vendor/bin/phpcs --standard=.phpcs.xml.dist -q . + # === JS UNIT TESTS (no WordPress runtime needed; fail fast before wp-env) === + - name: Run JavaScript unit tests + run: npm run test:js + # === START ENVIRONMENT (with Xdebug for coverage) === - name: Start wp-env with coverage run: npx wp-env start --xdebug=coverage diff --git a/assets/css/exelearning.css b/assets/css/exelearning.css index 1285c74..db94fd6 100644 --- a/assets/css/exelearning.css +++ b/assets/css/exelearning.css @@ -291,6 +291,59 @@ max-width: 100%; } +/* Loading state for the embedded package. + The package is fetched, parsed and laid out inside the iframe before anything + paints, so without this the visitor stares at an empty frame. The spinner is + hidden unless the enqueued loader adds `is-loading`, so with JS disabled + nothing is shown that JS would never be able to clear. */ +.exelearning-embed-loader { + position: relative; +} + +.exelearning-embed-loader__spinner { + display: none; + position: absolute; + inset: 0; + align-items: center; + justify-content: center; + gap: 0.6em; + background: #fff; + border: 1px solid #ddd; + border-radius: 4px; + color: #50575e; + font-size: 0.875rem; +} + +/* Only the visibility depends on the state. */ +.exelearning-embed-loader.is-loading .exelearning-embed-loader__spinner { + display: flex; +} + +.exelearning-embed-loader__spinner::before { + content: ""; + width: 1.25em; + height: 1.25em; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: exelearning-embed-spin 0.8s linear infinite; +} + +@keyframes exelearning-embed-spin { + to { + transform: rotate(360deg); + } +} + +/* A slower spin is still a spin. Someone who asked for reduced motion gets none: the + ring stays as a static shape, and the "Loading content…" text with role="status" + carries the message on its own. */ +@media (prefers-reduced-motion: reduce) { + .exelearning-embed-loader__spinner::before { + animation: none; + } +} + .exelearning-screenshot-img { display: block; width: 100%; diff --git a/assets/js/exelearning-embed-loader.js b/assets/js/exelearning-embed-loader.js new file mode 100644 index 0000000..c6b622f --- /dev/null +++ b/assets/js/exelearning-embed-loader.js @@ -0,0 +1,140 @@ +/** + * Loading state for embedded eXeLearning packages. + * + * The iframe has to download, parse and lay out a whole package before anything + * paints, so without this the visitor stares at an empty bordered frame with no + * clue whether it is loading or broken. + * + * Binds every `.exelearning-embed-loader` on the page in one pass, so a page + * with several embeds costs one cached script instead of one inline copy per + * block. The spinner is revealed from here, never server-side: a visitor + * without JavaScript then never gets an overlay that nothing could clear. + */ +( function () { + 'use strict'; + + /** Give up after this long so a failed embed cannot spin forever. */ + var BACKSTOP_MS = 20000; + + /** + * How far ahead of the viewport to arm the spinner. + * + * It has to be at least the browser's own lazy-loading fetch distance, which is + * far larger than "nearly visible" -- Chrome uses a viewport-distance threshold + * in the low thousands of pixels. A margin smaller than that arms the spinner + * only after the frame has already been fetched. + */ + var FETCH_MARGIN = '1500px'; + + /** + * Whether the frame finished loading before this script got a chance to listen. + * + * The loader is enqueued in the footer and binds no earlier than DOMContentLoaded, + * so an embed near the top of a long page -- a cached one especially -- can fire + * its load event before bind() ever runs. `load` is not re-emitted, so without + * asking the frame directly it would look unloaded forever and the spinner would + * cover finished content until the backstop expired. + * + * The embed is same-origin (the REST content proxy on this host) and keeps + * allow-same-origin in its sandbox, so its document is readable. The about:blank + * test is what makes reading it safe: a lazy frame whose fetch has NOT started + * yet also reports readyState 'complete', on the placeholder document it was + * created with. Treating that as loaded would suppress the spinner in exactly the + * case it exists for. + * + * @param {HTMLIFrameElement} iframe The embed frame. + * @return {boolean} True only when a real document has finished loading. + */ + function hasAlreadyLoaded( iframe ) { + try { + var doc = iframe.contentDocument; + return !! doc && + 'complete' === doc.readyState && + !! doc.location && + 'about:blank' !== doc.location.href; + } catch ( e ) { + // Cross-origin, so unreadable. Fall through to the normal event flow. + return false; + } + } + + function bind( wrap ) { + var iframe = wrap.querySelector( '.exelearning-iframe' ); + if ( ! iframe || wrap.dataset.exeLoaderBound ) { + return; + } + wrap.dataset.exeLoaderBound = '1'; + + // Once the frame has painted -- or failed, or outlived the backstop -- the + // spinner must never appear again. The margin below is a heuristic about + // someone else's fetch scheduling; this is the invariant that does not + // depend on getting that heuristic right. + var settled = false; + + var clear = function () { + settled = true; + wrap.classList.remove( 'is-loading' ); + }; + // `load` fires when the package finished downloading, but the eXeLearning + // theme still builds its navigation right after, so clearing immediately + // can uncover a frame that is still blank. + var clearSoon = function () { + setTimeout( clear, 150 ); + }; + + var start = function () { + if ( settled ) { + return; + } + wrap.classList.add( 'is-loading' ); + setTimeout( clear, BACKSTOP_MS ); + }; + + iframe.addEventListener( 'load', function () { + // Settled now, not in 150 ms: the frame is done, and a scroll landing in + // between must not be able to drop a spinner over the loaded content. + settled = true; + clearSoon(); + } ); + iframe.addEventListener( 'error', clear ); + + // A frame that finished before we got here will never re-emit `load`, so ask + // it directly instead of waiting for an event that already happened. + if ( hasAlreadyLoaded( iframe ) ) { + settled = true; + } + + // The iframe is `loading="lazy"`, so the browser owns the fetch schedule and + // starts it long before the frame is on screen. Arming the spinner at parse + // time would leave it over a frame nothing is fetching yet; arming it only + // once the frame is nearly visible would leave it over a frame that already + // finished. The margin covers the browser's fetch distance, and `settled` + // covers whatever the margin gets wrong. + if ( ! ( 'IntersectionObserver' in window ) ) { + start(); + return; + } + var observer = new IntersectionObserver( + function ( entries ) { + if ( ! entries.some( function ( entry ) { return entry.isIntersecting; } ) ) { + return; + } + observer.disconnect(); + start(); + }, + { rootMargin: FETCH_MARGIN } + ); + observer.observe( iframe ); + } + + function bindAll() { + var wraps = document.querySelectorAll( '.exelearning-embed-loader' ); + Array.prototype.forEach.call( wraps, bind ); + } + + if ( 'loading' === document.readyState ) { + document.addEventListener( 'DOMContentLoaded', bindAll ); + } else { + bindAll(); + } +}() ); diff --git a/includes/class-elp-upload-block.php b/includes/class-elp-upload-block.php index 3ef0813..95f3733 100644 --- a/includes/class-elp-upload-block.php +++ b/includes/class-elp-upload-block.php @@ -26,7 +26,12 @@ public function __construct() { } /** - * Enqueue frontend styles. + * Enqueue frontend styles and register the shared embed-loader behavior. + * + * The loader is registered here but not enqueued: a page with no eXeLearning + * block has nothing for it to bind, and enqueueing from this hook would put the + * request on every frontend page of the site. render_block_preview() enqueues it + * at the point it emits a wrapper for the loader to find. */ public function enqueue_frontend_styles() { wp_enqueue_style( @@ -35,6 +40,15 @@ public function enqueue_frontend_styles() { array(), EXELEARNING_VERSION ); + // Binds every `.exelearning-embed-loader` on the page at once, so the + // spinner costs one cached file instead of an inline copy per block. + wp_register_script( + 'exelearning-embed-loader', + plugins_url( '../assets/js/exelearning-embed-loader.js', __FILE__ ), + array(), + EXELEARNING_VERSION, + true + ); } /** @@ -300,6 +314,15 @@ private function render_block_preview( $data, $download_html ) { $html .= '
'; } + // The package is downloaded, parsed and laid out inside the iframe before + // anything paints, which reads as a blank frame for a noticeable moment. + // Wrap it so the loader script can cover that gap with a spinner, and pull + // the script in here so it only ships on pages that actually have a wrapper. + // It is a footer script enqueued while the content renders, which still + // prints; in a REST render the handle was never registered and this is a + // harmless no-op. + wp_enqueue_script( 'exelearning-embed-loader' ); + $html .= '', + esc_html__( 'Loading content…', 'exelearning' ) + ); + $html .= ''; $html .= ''; diff --git a/languages/exelearning-ca.po b/languages/exelearning-ca.po index 4b3e3d7..5e720dc 100644 --- a/languages/exelearning-ca.po +++ b/languages/exelearning-ca.po @@ -459,26 +459,30 @@ msgstr "Aquest no és un fitxer eXeLearning (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Error en crear el directori per als fitxers extrets." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Error: contingut d'eXeLearning no trobat" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Descarrega el fitxer" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Aquest contingut d'eXeLearning és un fitxer font i no es pot previsualitzar directament." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Veure a pantalla completa" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "S'està carregant el contingut…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-ca_valencia.po b/languages/exelearning-ca_valencia.po index 576000d..f3d8481 100644 --- a/languages/exelearning-ca_valencia.po +++ b/languages/exelearning-ca_valencia.po @@ -459,26 +459,30 @@ msgstr "Este no és un fitxer eXeLearning (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Error en crear el directori per als fitxers extrets." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Error: contingut d'eXeLearning no trobat" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Descarregar el fitxer" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Este contingut d'eXeLearning és un fitxer font i no es pot previsualitzar directament." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Veure a pantalla completa" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "S'està carregant el contingut…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-de_DE.po b/languages/exelearning-de_DE.po index b7500bc..607224d 100644 --- a/languages/exelearning-de_DE.po +++ b/languages/exelearning-de_DE.po @@ -459,26 +459,30 @@ msgstr "Dies ist keine eXeLearning-Datei (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Fehler beim Erstellen des Verzeichnisses für extrahierte Dateien." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Fehler: eXeLearning-Inhalt nicht gefunden" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Datei herunterladen" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Dieser eXeLearning-Inhalt ist eine Quelldatei und kann nicht direkt in der Vorschau angezeigt werden." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Vollbild anzeigen" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "Inhalt wird geladen…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-eo.po b/languages/exelearning-eo.po index 747deb7..b0a19d2 100644 --- a/languages/exelearning-eo.po +++ b/languages/exelearning-eo.po @@ -459,26 +459,30 @@ msgstr "Ĉi tio ne estas eXeLearning-dosiero (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Malsukcesis krei dosierujon por ĉerpitaj dosieroj." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Eraro: eXeLearning-enhavo ne trovita" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Elŝuti dosieron" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Ĉi tiu eXeLearning-enhavo estas fontdosiero kaj ne povas esti antaŭrigardata rekte." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Vidi tutekrane" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "Enhavo estas ŝargata…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-es_ES.po b/languages/exelearning-es_ES.po index 01f8b1b..c1f6f37 100644 --- a/languages/exelearning-es_ES.po +++ b/languages/exelearning-es_ES.po @@ -459,26 +459,30 @@ msgstr "Este no es un archivo eXeLearning (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Error al crear el directorio para los archivos extraídos." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Error: contenido de eXeLearning no encontrado" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Descargar archivo" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Este contenido de eXeLearning es un archivo fuente y no puede previsualizarse directamente." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Ver a pantalla completa" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "Cargando contenido…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-eu.po b/languages/exelearning-eu.po index 93239f8..628558b 100644 --- a/languages/exelearning-eu.po +++ b/languages/exelearning-eu.po @@ -459,26 +459,30 @@ msgstr "Hau ez da eXeLearning fitxategia (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Errorea ateratako fitxategien direktorioa sortzean." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Errorea: eXeLearning edukia ez da aurkitu" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Deskargatu fitxategia" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "eXeLearning eduki hau iturburu fitxategia da eta ezin da zuzenean aurreikusi." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Ikusi pantaila osoan" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "Edukia kargatzen…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-gl_ES.po b/languages/exelearning-gl_ES.po index 770becc..1a0933c 100644 --- a/languages/exelearning-gl_ES.po +++ b/languages/exelearning-gl_ES.po @@ -459,26 +459,30 @@ msgstr "Este non é un ficheiro eXeLearning (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Erro ao crear o directorio para os ficheiros extraídos." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Erro: contido de eXeLearning non atopado" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Descargar ficheiro" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Este contido de eXeLearning é un ficheiro fonte e non se pode previsualizar directamente." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Ver a pantalla completa" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "Cargando o contido…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-it_IT.po b/languages/exelearning-it_IT.po index 16e6814..9886f76 100644 --- a/languages/exelearning-it_IT.po +++ b/languages/exelearning-it_IT.po @@ -459,26 +459,30 @@ msgstr "Questo non è un file eXeLearning (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Errore nella creazione della directory per i file estratti." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Errore: contenuto eXeLearning non trovato" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Scarica file" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Questo contenuto eXeLearning è un file sorgente e non può essere visualizzato in anteprima direttamente." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Visualizza a schermo intero" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "Caricamento del contenuto…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-pt_PT.po b/languages/exelearning-pt_PT.po index 2ffcf8b..21af4a4 100644 --- a/languages/exelearning-pt_PT.po +++ b/languages/exelearning-pt_PT.po @@ -459,26 +459,30 @@ msgstr "Este não é um ficheiro eXeLearning (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Erro ao criar o diretório para os ficheiros extraídos." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Erro: conteúdo eXeLearning não encontrado" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Descarregar ficheiro" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Este conteúdo eXeLearning é um ficheiro fonte e não pode ser pré-visualizado diretamente." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Ver em ecrã inteiro" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "A carregar o conteúdo…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning-ro_RO.po b/languages/exelearning-ro_RO.po index 30cb7be..2dfd927 100644 --- a/languages/exelearning-ro_RO.po +++ b/languages/exelearning-ro_RO.po @@ -459,26 +459,30 @@ msgstr "Acesta nu este un fișier eXeLearning (.elpx)." msgid "Failed to create directory for extracted files." msgstr "Eroare la crearea directorului pentru fișierele extrase." -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "Eroare: conținutul eXeLearning nu a fost găsit" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "Descarcă fișierul" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "Acest conținut eXeLearning este un fișier sursă și nu poate fi previzualizat direct." -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "Vizualizare pe ecran complet" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "Se încarcă conținutul…" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/languages/exelearning.pot b/languages/exelearning.pot index 4e342b1..4bafdd6 100644 --- a/languages/exelearning.pot +++ b/languages/exelearning.pot @@ -458,26 +458,30 @@ msgstr "" msgid "Failed to create directory for extracted files." msgstr "" -#: includes/class-elp-upload-block.php:167 +#: includes/class-elp-upload-block.php:181 msgid "Error: eXeLearning content not found" msgstr "" -#: includes/class-elp-upload-block.php:258 +#: includes/class-elp-upload-block.php:272 #: public/class-shortcodes.php:204 msgid "Download file" msgstr "" -#: includes/class-elp-upload-block.php:271 +#: includes/class-elp-upload-block.php:285 msgid "This eXeLearning content is a source file and cannot be previewed directly." msgstr "" -#: includes/class-elp-upload-block.php:295 +#: includes/class-elp-upload-block.php:309 #: public/class-shortcodes.php:336 #: assets/js/elp-upload.js:501 #: assets/js/elp-upload.js:502 msgid "View fullscreen" msgstr "" +#: includes/class-elp-upload-block.php:342 +msgid "Loading content…" +msgstr "" + #: includes/class-exelearning-editor.php:82 #: includes/class-exelearning-editor.php:83 msgid "eXeLearning Editor" diff --git a/package-lock.json b/package-lock.json index 44d9184..a1677fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,46 @@ "devDependencies": { "@playwright/test": "^1.62.0", "@wordpress/env": "^11.11.0", - "nunjucks": "^3.2.4" + "happy-dom": "^20.10.2", + "nunjucks": "^3.2.4", + "vitest": "^4.1.8" + } + }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@inquirer/ansi": { @@ -442,6 +481,13 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -484,6 +530,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", + "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^2.0.0-alpha.3", + "@emnapi/runtime": "^2.0.0-alpha.3" + } + }, "node_modules/@nodable/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", @@ -1057,6 +1125,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@php-wasm/cli-util": { "version": "3.1.13", "resolved": "https://registry.npmjs.org/@php-wasm/cli-util/-/cli-util-3.1.13.tgz", @@ -1385,6 +1463,322 @@ "node": ">=20" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@simple-git/args-pathspec": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", @@ -1415,6 +1809,13 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -1428,6 +1829,17 @@ "node": ">=10" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aws-lambda": { "version": "8.10.161", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz", @@ -1455,6 +1867,31 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -1469,45 +1906,175 @@ "dev": true, "license": "MIT", "dependencies": { - "@types/ms": "*", - "@types/node": "*" + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@types/node": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", - "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@wordpress/env": { @@ -1879,6 +2446,16 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-lock": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", @@ -1985,6 +2562,19 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2074,6 +2664,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2282,6 +2882,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2442,6 +3049,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/diff3": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.4.tgz", @@ -2528,6 +3145,19 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2548,6 +3178,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2602,6 +3239,16 @@ "node": ">=4" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2612,6 +3259,16 @@ "node": ">= 0.6" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/express/-/express-4.22.0.tgz", @@ -2705,6 +3362,24 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -2964,6 +3639,47 @@ "dev": true, "license": "ISC" }, + "node_modules/happy-dom": { + "version": "20.11.1", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.1.tgz", + "integrity": "sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3304,25 +4020,298 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lodash.includes": { @@ -3476,6 +4465,16 @@ "node": "18 >=18.20 || 20 || >=22" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3642,6 +4641,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3704,6 +4722,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/octokit": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.2.tgz", @@ -3900,6 +4932,33 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -3952,6 +5011,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", + "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4147,6 +5235,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4391,6 +5513,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4494,6 +5623,16 @@ "dev": true, "license": "MIT" }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4501,6 +5640,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4511,6 +5657,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4644,6 +5797,50 @@ "node": ">=8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -4689,6 +5886,14 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4800,6 +6005,189 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/wasm-feature-detect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", @@ -4817,6 +6205,16 @@ "defaults": "^1.0.3" } }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4855,6 +6253,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index cffa8bd..6d058dc 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,16 @@ "devDependencies": { "@playwright/test": "^1.62.0", "@wordpress/env": "^11.11.0", - "nunjucks": "^3.2.4" + "happy-dom": "^20.10.2", + "nunjucks": "^3.2.4", + "vitest": "^4.1.8" }, "scripts": { "wp-env": "wp-env", "composer": "wp-env run cli --env-cwd=wp-content/plugins/exelearning composer", "composer:tests-cli": "wp-env run tests-cli --env-cwd=wp-content/plugins/exelearning composer", "test:unit": "wp-env run tests-cli --env-cwd=wp-content/plugins/exelearning ./vendor/bin/phpunit", + "test:js": "vitest run", "test:e2e": "playwright test", "build": "echo 'Use make build-editor instead for static editor build'", "package": "npm run package:zip", diff --git a/tests/js/exelearning_embed_loader.test.js b/tests/js/exelearning_embed_loader.test.js new file mode 100644 index 0000000..490994d --- /dev/null +++ b/tests/js/exelearning_embed_loader.test.js @@ -0,0 +1,191 @@ +// Unit tests for assets/js/exelearning-embed-loader.js. +// +// The loader is enqueued in the footer and binds no earlier than DOMContentLoaded, so +// it routinely arrives after an embed has already finished: `load` has fired and will +// not fire again. Everything here is about that ordering, because getting it wrong is +// worse than having no spinner at all -- the overlay lands on content the visitor is +// already reading and stays for the whole backstop. +// +// The loader is an IIFE with no exports, so each test imports a fresh copy under a +// cache-busting URL and drives it through the DOM. + +const path = require( 'path' ); +const { pathToFileURL } = require( 'url' ); + +const LOADER = pathToFileURL( + path.resolve( __dirname, '../../assets/js/exelearning-embed-loader.js' ) +).href; + +let loadCount = 0; +let observers = []; + +/** + * Install a controllable IntersectionObserver so intersection is a test decision + * rather than a layout accident. + */ +function installIntersectionObserver() { + observers = []; + window.IntersectionObserver = class { + constructor( callback, options ) { + this.callback = callback; + this.options = options; + this.targets = []; + observers.push( this ); + } + observe( element ) { + this.targets.push( element ); + } + disconnect() { + this.disconnected = true; + } + /** Test hook: report every observed target as intersecting. */ + intersect() { + this.callback( this.targets.map( ( target ) => ( { target, isIntersecting: true } ) ) ); + } + }; +} + +/** + * Build the wrapper markup the block renders, with a controllable contentDocument. + * + * @param {Object} frame What the embed frame should report. + * @param {string} [frame.readyState] Document readyState, or null for no document. + * @param {string} [frame.href] Document location.href. + * @param {boolean} [frame.throws] Whether reading contentDocument throws (cross-origin). + * @return {HTMLElement} The loader wrapper. + */ +function buildEmbed( frame ) { + document.body.innerHTML = + ''; + + const iframe = document.querySelector( '.exelearning-iframe' ); + Object.defineProperty( iframe, 'contentDocument', { + configurable: true, + get() { + if ( frame.throws ) { + throw new Error( 'cross-origin' ); + } + if ( ! frame.readyState ) { + return null; + } + return { readyState: frame.readyState, location: { href: frame.href } }; + }, + } ); + + return document.querySelector( '.exelearning-embed-loader' ); +} + +/** Import a fresh copy of the loader, which binds on import. */ +async function runLoader() { + await import( /* @vite-ignore */ `${ LOADER }?load=${ ++loadCount }` ); +} + +beforeEach( () => { + installIntersectionObserver(); +} ); + +afterEach( () => { + document.body.innerHTML = ''; + observers = []; +} ); + +describe( 'exelearning-embed-loader: a frame that finished before the loader bound', () => { + it( 'never covers an embed whose document already loaded', async () => { + const wrap = buildEmbed( { readyState: 'complete', href: 'http://example.test/embed.html' } ); + + await runLoader(); + observers[ 0 ].intersect(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( false ); + } ); + + it( 'still covers a lazy embed whose fetch has not started', async () => { + // A lazy frame the browser has not fetched yet reports readyState 'complete' on + // its about:blank placeholder. Reading that as "loaded" would suppress the + // spinner in the one case it exists for. + const wrap = buildEmbed( { readyState: 'complete', href: 'about:blank' } ); + + await runLoader(); + observers[ 0 ].intersect(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( true ); + } ); + + it( 'still covers an embed that is mid-flight', async () => { + const wrap = buildEmbed( { readyState: 'loading', href: 'http://example.test/embed.html' } ); + + await runLoader(); + observers[ 0 ].intersect(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( true ); + } ); + + it( 'falls back to the event flow when the document is unreadable', async () => { + // Cross-origin: we cannot know, so behave as before rather than guess. + const wrap = buildEmbed( { throws: true } ); + + await runLoader(); + observers[ 0 ].intersect(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( true ); + } ); + + it( 'still covers a frame that reports no document at all', async () => { + const wrap = buildEmbed( { readyState: null } ); + + await runLoader(); + observers[ 0 ].intersect(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( true ); + } ); +} ); + +describe( 'exelearning-embed-loader: without IntersectionObserver', () => { + it( 'arms the spinner immediately rather than never', async () => { + delete window.IntersectionObserver; + const wrap = buildEmbed( { readyState: 'loading', href: 'about:blank' } ); + + await runLoader(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( true ); + } ); + + it( 'still leaves a frame that already loaded uncovered', async () => { + delete window.IntersectionObserver; + const wrap = buildEmbed( { readyState: 'complete', href: 'http://example.test/embed.html' } ); + + await runLoader(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( false ); + } ); +} ); + +describe( 'exelearning-embed-loader: the normal ordering still works', () => { + it( 'covers the frame on intersection and clears it on load', async () => { + const wrap = buildEmbed( { readyState: 'loading', href: 'about:blank' } ); + const iframe = document.querySelector( '.exelearning-iframe' ); + + await runLoader(); + observers[ 0 ].intersect(); + expect( wrap.classList.contains( 'is-loading' ) ).toBe( true ); + + iframe.dispatchEvent( new window.Event( 'load' ) ); + await new Promise( ( resolve ) => setTimeout( resolve, 250 ) ); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( false ); + } ); + + it( 'does not arm the spinner after a load that arrived first', async () => { + const wrap = buildEmbed( { readyState: 'loading', href: 'about:blank' } ); + const iframe = document.querySelector( '.exelearning-iframe' ); + + await runLoader(); + iframe.dispatchEvent( new window.Event( 'load' ) ); + observers[ 0 ].intersect(); + + expect( wrap.classList.contains( 'is-loading' ) ).toBe( false ); + } ); +} ); diff --git a/tests/unit/ElpUploadBlockTest.php b/tests/unit/ElpUploadBlockTest.php index 24ca35c..3597023 100644 --- a/tests/unit/ElpUploadBlockTest.php +++ b/tests/unit/ElpUploadBlockTest.php @@ -752,6 +752,86 @@ public function test_block_iframe_has_shared_class() { $this->assertStringContainsString( 'exelearning-iframe', $result ); } + /** + * The preview iframe is wrapped in a loader that covers the blank frame + * while the package downloads and lays out. + */ + public function test_block_preview_has_loading_spinner() { + $attachment_id = $this->create_previewable_attachment( str_repeat( 'f', 40 ) ); + + $result = $this->block->render_block( array( 'attachmentId' => $attachment_id ) ); + + $this->assertStringContainsString( 'exelearning-embed-loader', $result ); + $this->assertStringContainsString( 'exelearning-embed-loader__spinner', $result ); + // Announced to assistive tech rather than silently spinning. + $this->assertStringContainsString( 'role="status"', $result ); + $this->assertStringContainsString( 'aria-live="polite"', $result ); + // The loader ships as one enqueued asset, so no copy of its behavior is + // inlined into the block. Asserted against its own distinctive tokens + // rather than "contains no