diff --git a/angular.json b/angular.json index 2f431657..fcba5834 100644 --- a/angular.json +++ b/angular.json @@ -29,10 +29,7 @@ "options": { "karmaConfig": "projects/ngx-coding-components/karma.conf.js", "tsConfig": "projects/ngx-coding-components/tsconfig.spec.json", - "polyfills": [ - "zone.js", - "zone.js/testing" - ] + "polyfills": [] } } } @@ -59,9 +56,7 @@ "outputPath": "dist/coding-components-serve", "index": "src/index.html", "main": "src/main.ts", - "polyfills": [ - "zone.js" - ], + "polyfills": [], "tsConfig": "tsconfig.app.json", "preserveSymlinks": true, "inlineStyleLanguage": "scss", @@ -129,10 +124,7 @@ "test": { "builder": "ngx-build-plus:karma", "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], + "polyfills": [], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", "assets": [ @@ -174,9 +166,7 @@ "index": "projects/schemer/src/index.html", "tsConfig": "projects/schemer/tsconfig.app.json", "inlineStyleLanguage": "scss", - "polyfills": [ - "zone.js" - ], + "polyfills": [], "assets": [ "projects/schemer/src/assets" ], diff --git a/docs/angular-21-migration-audit-2026-10-16.md b/docs/angular-21-migration-audit-2026-10-16.md index d10d40d0..1b908b8e 100644 --- a/docs/angular-21-migration-audit-2026-10-16.md +++ b/docs/angular-21-migration-audit-2026-10-16.md @@ -1,5 +1,10 @@ # Angular-21-Migration: Prüfung vom 16.10.2026 +**Späterer Stand:** Dieser Bericht beschreibt die damalige Teilstufe. Die vollständige +Responses-ESM-/Zoneless-Umstellung ist in +[responses-esm-zoneless-2026-10-16.md](responses-esm-zoneless-2026-10-16.md) +dokumentiert. + **Urteil:** Die bestätigten Migrationsfehler sind im lokalen Prüfstand behoben. Die unten aufgeführten Laufzeit-, Build- und Vertragstests bestehen. Dies ist keine Aussage über ein bereits veröffentlichtes Release oder Deployment. diff --git a/docs/responses-esm-zoneless-2026-10-16.json b/docs/responses-esm-zoneless-2026-10-16.json new file mode 100644 index 00000000..75d9b962 --- /dev/null +++ b/docs/responses-esm-zoneless-2026-10-16.json @@ -0,0 +1,48 @@ +{ + "date": "2026-10-16", + "status": "source_changes_for_pull_requests", + "versionBumps": false, + "responses": { + "installed": "5.3.1", + "installedNativeEsm": false, + "esmPullRequest": "https://github.com/iqb-berlin/responses/pull/138", + "esmSourceCommit": "8f77414", + "esmSourceBase": "565e7df", + "dependencyUpdatePendingRegularRelease": true + }, + "htmlBytes": { + "publishedComparison": 4845727, + "currentBranchWithPublishedResponses": 4270157, + "previousLocalEsmIntegrationExperiment": 3846981 + }, + "currentSavingsBytes": 575570, + "currentSavingsPercent": 11.88, + "zone": { + "rootPackageAbsent": true, + "bundleInputAbsent": true, + "browserGlobalAbsent": true, + "angularOptionalPeerMetadataRetained": true + }, + "tests": { + "responsesUnit": 366, + "responsesSolverPairs": 234, + "publishedReference": "5.3.1", + "goldenFixtures": 75, + "libraryUnit": 573, + "schemerUnit": 6, + "codebookContracts": 1287, + "developmentBrowser": 21, + "temporaryHtmlBrowser": 14 + }, + "lint": { + "errors": 0, + "existingWarnings": 20 + }, + "releaseCreated": false, + "releaseArtifactsCommitted": false, + "limits": [ + "Remote CI must be checked separately", + "No merge, registry publication or deployment", + "No new local .NET, Word, Safari or Firefox acceptance" + ] +} diff --git a/docs/responses-esm-zoneless-2026-10-16.md b/docs/responses-esm-zoneless-2026-10-16.md new file mode 100644 index 00000000..3c86f628 --- /dev/null +++ b/docs/responses-esm-zoneless-2026-10-16.md @@ -0,0 +1,150 @@ +# Responses ESM und vollständige Zoneless-Umstellung + +Prüfstand vom 16.10.2026. Die Quelländerungen enthalten keine Versionsanhebung +und keine Release-Dateien. Die bestehenden Versionsfelder bleiben unverändert. + +## Größe und getrennte Lieferwege + +| Prüfstand | HTML-Größe | +| --- | ---: | +| Veröffentlichtes Schemer 2.9.0-alpha.2 als Vergleich | 4.845.727 B | +| Dieser Zweig mit WOFF2, vollständiger Zoneless-Umstellung und Responses 5.3.1 aus npm | **4.270.157 B** | +| Vorheriger lokaler Integrationsversuch mit zusätzlichem Responses-ESM-Build | 3.846.981 B | + +Der aktuelle Zweig spart **575.570 B / 11,88 %**. Für die lokale HTML-Prüfung +wird ausschließlich eine temporäre Testdatei mit der Metadatenversion `0.0.0` +erzeugt. Sie ist kein Release-Artefakt und wird nicht eingecheckt. + +Der ESM-Einstieg ist separat in [Responses PR #138](https://github.com/iqb-berlin/responses/pull/138) +implementiert. Der frühere Integrationsversuch erreichte insgesamt 20,61 % +Ersparnis gegenüber dem veröffentlichten Vergleichsstand. Dieser Wert ist ein +zusätzliches Messergebnis und beschreibt nicht den normalen Build dieses Zweigs. +Er enthält auch den aktuellen Responses-Quellstand auf Basis von `565e7df`. +Im damaligen Vergleich bei noch aktiver Zone sparte der ESM-Schritt 388.880 B. + +coding-components installiert weiterhin das veröffentlichte Responses 5.3.1. +Ein Wechsel auf die neue ESM-Ausgabe benötigt deren spätere reguläre Freigabe +und eine anschließende Aktualisierung der Abhängigkeit. Es werden weder ein +vorläufiges Responses-Paket noch eine zusätzliche Alpha-Version vorausgesetzt. + +## Responses: zusätzliche Modulform ohne eingeschränkten Solver + +Die beiden Ausgaben werden aus denselben TypeScript-Engine-Quellen erzeugt. +`require` und klassische Unterpfade verwenden weiterhin die bisherigen +CommonJS-Dateien im Paketwurzelverzeichnis. `import` verwendet native ESM-Dateien +unter `esm/`. Die Ausgabe ergänzt explizite `.js`-Dateiendungen auch in den +Deklarationen, sodass NodeNext und Browser-Bundler ohne Sonderauflösung arbeiten. +Relative Verzeichnis-Einstiege wie `presentation` bleiben nutzbar. + +Das ESM-Solver-Modul importiert `evaluate` aus dem vollständigen mathjs. +Funktionen, Konfiguration und Toleranzen bleiben erhalten. Die kleinere Ausgabe +`mathjs/number` wurde anhand von Gegenbeispielen verworfen: Schon +`sum([1,2,3])` und `abs(complex(3,4))` liefern damit nicht mehr dieselben gültigen +numerischen Ergebnisse. Der Ansatz entspricht der dokumentierten +[ESM-Bündelung von mathjs](https://mathjs.org/docs/custom_bundling.html). + +Der Repository-Build von Responses basiert auf upstream `565e7df`; die ESM-Arbeit +ändert keine Dateien unter `src/`. Die ursprüngliche Responses-Unit-Suite +besteht mit ihrem gesperrten mathjs 15.1.0. Die frischen Consumer und der Schemer +verwenden mathjs 15.2.0, entsprechend dem bisherigen Schemer-Lockfile. + +Die Paketprüfung installiert einen echten Tarball und zusätzlich das +veröffentlichte Responses 5.3.1 unter einem reinen Testalias. Sie prüft: + +- ESM/CommonJS, öffentliche Exporte, Datei- und Verzeichnis-Unterpfade; +- TypeScript mit klassischer Node-Auflösung, NodeNext für ESM/CommonJS und Bundler; +- **234 Ableitungen** in beiden Modulformen und gegen veröffentlichtes 5.3.1: + 108 Ergebnisse `VALUE_CHANGED`, 65 `INVALID`, 61 `DERIVE_ERROR`; +- ausdrücklich erwartete Summen, komplexe Zwischenwerte, Einheiten und BigNumber; +- **75 bestehende Kodier-Golden-Fälle**, einschließlich Textdarstellung und + unveränderter erwarteter Kodierergebnisse. + +Diese Paketprüfungen bestehen auf **Node 18.20.8 und Node 22.23.1**. Die +unveränderte Engine-Suite besteht mit **366 Tests** und Coverage-Grenzen. + +**Korrektur zur früheren Probe:** In den ursprünglichen 180 synthetischen +Ableitungen fehlte `sourceParameters.processing`. Dadurch verglich diese Probe +zunächst nur dieselben Fehlerpfade. Der reproduzierbare Analyseskript setzt das +Pflichtfeld jetzt und verlangt ausdrücklich erfolgreiche Auswertungen und +Fehlerfälle. Der korrigierte Lauf hat 69 `VALUE_CHANGED`, 50 `INVALID` und +61 `DERIVE_ERROR`. Die oben genannte neue Paketprüfung umfasst darüber hinaus +konkrete erwartete Werte und den unabhängigen veröffentlichten Vergleich. + +Der zusätzliche Paketbuild und die Paketprüfungen liegen im Responses-PR. +Der ungenutzte direkte mathjs-12-Peer der Komponentenbibliothek ist entfernt; +Responses verwaltet seine mathjs-Abhängigkeit selbst. + +## Zone.js: entfernte Anforderungen und geprüfte Aktualisierungswege + +Beide App-Einstiegspunkte verwenden ausdrücklich `provideZonelessChangeDetection()`. +Zone.js-Imports, Polyfills für Build und Test, das ungenutzte Polyfills-Quellmodul, +direkte Abhängigkeit, installierter Lockfile-Paketeintrag und Bibliotheks-Peer +sind entfernt. Der HTML-Wrapper erwartet keine entfernte `polyfills.js` mehr. +Die Test-Initialisierung verwendet `BrowserTestingModule`/`platformBrowserTesting`. +Die drei `fakeAsync`-Fälle warten jetzt auf echte Observable- oder Timer-Ergebnisse. + +| Bereich | Aktualisierungsweg und Nachweis | +| --- | --- | +| Studio/Verona-Start und spätere Host-Nachrichten | Variablen, Schema und Rolle im Wrapper sind Signals, die das Template liest. Native Nachrichten erscheinen ohne vorbereitenden Klick. Leere Listen entfernen die alte Ansicht. | +| Rollenwechsel | Eine ausdrückliche neue Rolle `super` hebt einen vorherigen Gastmodus auf. Eine Nachricht ohne neue Rolle erhält dagegen die bestehende Berechtigung. Beide Fälle laufen im Browser. | +| Schemer-Listen, Import, Dialogrückgaben, 300-ms-Debounce | `markForCheck()` an den asynchronen Grenzen und der zentralen Listenaktualisierung; bestehender `AsyncPipe` für die Auswahl bleibt erhalten. | +| Variablen-/Code-Editor und 500-ms-Debounce | Eigene Benachrichtigung nach verzögerten Änderungen sowie nach Bestätigung/Abbruch der Material-Dialoge. | +| Quell-, Array-/Fragment- und Verarbeitungsdialoge | Material-/Formularereignisse bleiben gebunden; Rückgaben benachrichtigen die zuständige Ansicht. Gültige Position 0 und leere Anweisungen bleiben zulässig. | +| Dateilisten und Antwortschema aus FileReader | Menü-Ausgaben benachrichtigen ihre gebundenen Besitzer; native Dateiimporte ersetzen die sichtbare Liste in Schemer und Demo. | +| Rich Text, Bild und Formel | Explizite Benachrichtigung nach Bildimport und Dialogrückgabe. Die vorhandene Tiptap-Direktive benachrichtigt bereits bei Inhalts-/Auswahländerungen. Native Auswahl aktualisiert die Format-Schaltfläche im echten Custom Element. | +| Verzögerte Variablenauswahl | Der 500-ms-Rückruf meldet die Änderung an Angular. Bestätigung wird ohne nachträglichen Klick oder erzwungene Test-Aktualisierung aktiv. | +| Solver-Cursor | Der verzögerte Rückruf verändert nur Fokus und DOM-Auswahl; die davor geänderten Formulardaten werden durch den gebundenen Menü-Listener gemeldet. | +| Codebook-Oberfläche | Eingaben über Angular-Inputs/`ngOnChanges`, gebundene Ereignisse und Material-Datenquelle; keine eigene native asynchrone Zustandsquelle. | +| Datei-/Verona-Dienste | Sie liefern Promise-/Observable-Ergebnisse. Die benutzenden Ansichten übernehmen die Benachrichtigung; der Nachrichtendienst behält den bestehenden Verona-Vertrag. | + +Die Umstellung folgt den [Zoneless-Anforderungen von Angular](https://angular.dev/guide/zoneless): +Signals, `markForCheck()`, gebundene Ereignisse und `setInput()` melden Änderungen. +Die neuen Render-Tests verwenden `whenStable()` und erzwingen keine +`detectChanges()`-Aufrufe, um fehlende Benachrichtigungen nicht zu verdecken. + +Im Lockfile bleiben ausschließlich Angulars **optionale Peer-Metadaten** zu +Zone.js stehen. Sie stammen aus dem Angular-Paket; es gibt keinen installierten +Zone.js-Paketeintrag. Ein frisches `npm ci`, die frische Bibliotheksinstallation, +die Bundle-Eingaben und die Browserprüfung bestätigen die tatsächliche Abwesenheit. + +## Erfolgreiche lokale Prüfung + +| Prüfung | Ergebnis | +| --- | --- | +| Responses | 366 Unit-Tests; frische Tarball-Consumer auf Node 18 und 22 einschließlich veröffentlichtem 5.3.1-Vergleich | +| Bibliothek | **573 Tests** mit Coverage, einschließlich drei zusätzlicher echter Zoneless-Renderfälle | +| Standalone-Schemer | **6 Tests** mit Coverage | +| Codebook-Generator und Verträge | **1.287 Tests** bestanden | +| Entwicklungsbuild | **21 Browserfälle**, einschließlich 320–1440 px und Container-Resizing | +| Temporäres HTML mit veröffentlichtem Responses 5.3.1 | **14 iframe-Fälle**, einschließlich der Originalvariablen aus dem lokalen Studio-Export und sechs Solver-Fällen | +| Native Demo/Elements, vorheriger Integrationsversuch | Kein `window.Zone`, alle elf Tags registriert; 20 Anfangsvariablen, Dateiimport, Format-Schaltfläche nach nativer Auswahl und Editor-Ausgabe geprüft; keine Laufzeitfehler | +| Builds | Bibliothek mit allen Einstiegspunkten, eigenständiger Schemer samt Verpackung und Demo/Elements erfolgreich | +| Weitere Gates | Lint ohne Fehler bei 20 bestehenden Warnungen; Schema- und Ajv-Prüfungen bestanden | +| Audit des vorherigen Integrationsversuchs | Repository und Consumer ohne hohe/kritische Befunde; ein niedriger KaTeX-Befund, beim Consumer zusätzlich ein moderater Tiptap-Befund. | + +Die Browserläufe nutzen das installierte Chrome. `PLAYWRIGHT_CHANNEL=chrome` +wählt es im Repository aus; ohne diese Option bleibt der normale Chromium-Pfad +der CI erhalten. Beispiel für eine temporäre HTML-Testdatei: + +```sh +PLAYWRIGHT_CHANNEL=chrome \ +SCHEMER_ARTIFACT=/tmp/schemer-zoneless-test.html \ +npm run test:schemer:responsive +``` + +## Grenzen und Auslieferungsstand + +Die lokalen Prüfungen belegen keinen Merge, keine Registry-Veröffentlichung und +kein Deployment auf iqb-studio.de. Die entfernte CI ist gesondert an den PRs +zu prüfen. Der reale Studio-Vertrag wird lokal als iframe mit +Start-/Änderungsnachrichten geprüft; die produktive Aufgabe benötigt eine Anmeldung. +Die externen Studio-/Kodierbox-Anwendungen wurden nicht geändert. + +Die .NET-Quellen in Responses wurden nicht verändert. Ihre Suite wurde mangels +lokalem .NET-SDK nicht erneut ausgeführt. Microsoft Word, Safari und Firefox +wurden für diesen Stand ebenfalls nicht erneut abgenommen. Die früheren +Host-/Word-Nachweise gelten weiterhin nur für ihre bezeichneten Artefakte. + +Codebook bleibt im Bibliothekspaket enthalten. Ein zusätzlicher Codebook-Dialog +im Schemer und die neue Partial-Credit-Erweiterung für leere Teilantworten sind +weiterhin nicht Bestandteil dieser Änderungen. diff --git a/docs/schemer-bundle-size-2026-10-16.md b/docs/schemer-bundle-size-2026-10-16.md new file mode 100644 index 00000000..5470ecf3 --- /dev/null +++ b/docs/schemer-bundle-size-2026-10-16.md @@ -0,0 +1,110 @@ +# Schemer: Dateigröße und mathjs-Prüfung vom 16.10.2026 + +**Späterer Stand:** Dieser Bericht beschreibt die damalige Teilstufe. Die vollständige +Responses-ESM-/Zoneless-Umstellung ist in +[responses-esm-zoneless-2026-10-16.md](responses-esm-zoneless-2026-10-16.md) +dokumentiert. + +## Umgesetzte Optimierung + +Verglichen wird das unveränderte veröffentlichte `Schemer_v2.9.0-alpha.2` +(`7ce5cacb41a2e545160fabaa3bb28331933c208c`) mit demselben Produktionsbuild nach +zwei Änderungen: NunitoSans als WOFF2 und Zone.js ausschließlich über die +Angular-Polyfills. `provideZoneChangeDetection()` bleibt aktiv, damit unter +Angular 21 die Studio-Startnachricht und verzögerte Rückgaben sichtbar werden. + +| Messung | Vorher | Nachher | Einsparung | +| --- | ---: | ---: | ---: | +| Gepacktes, eigenständiges HTML | 4.845.727 B | 4.327.681 B | **518.046 B / 10,69 %** | +| Hauptskript | 3.777.295 B | 3.741.547 B | 35.748 B | +| Eingebetteter Nunito-CSS-Block einschließlich Base64 | 759.244 B | 276.946 B | 482.298 B | +| Schriftdatei vor Base64 | 569.372 B | 207.636 B | 361.736 B | +| Gesamtes HTML mit gzip, rein lokale Vergleichsmessung | 1.431.181 B | 1.309.675 B | 121.506 B | + +Die Schrift bleibt vollständig: 1.100 Glyphen, 939 Unicode-Zuordnungen, +identische Konturen und Metriken sowie unveränderte Achsen `wght`, `wdth`, +`opsz`, `YTLC`. Es wurde kein Zeichensatz oder Schriftstil entfernt. Die +TTF-Quelldatei bleibt im Demo unter `src/assets/fonts/` erhalten; die redundante +Schemer-Kopie wurde durch WOFF2 ersetzt. Ihre SHA-256 vor der Konvertierung: +`585d07756a7f5cca34de3e622a23f5e98d60efc96ac9c9e7c6bc251fea040854`. + +Erzeugung mit Python, fontTools 4.67.0 und Brotli 1.2.0: + +```python +from fontTools.ttLib import TTFont + +font = TTFont('src/assets/fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf', + recalcTimestamp=False) +font.flavor = 'woff2' +font.save('projects/schemer/src/assets/fonts/' + 'NunitoSans-VariableFont_YTLC,opsz,wdth,wght.woff2') +``` + +## Lokale Prüfung + +- Produktionsbuild mit `--stats-json` und vollständige HTML-Verpackung erfolgreich. + Der Build enthält Zone.js nur noch in `polyfills.js`, ohne zweite Kopie in `main.js`. +- Sechs Schemer-Unit-Tests mit Coverage bestanden; Projekt-Lint ohne Fehler, + weiterhin 20 bestehende Warnungen. +- 15 Browserfälle am Entwicklungsbuild und acht iframe-Fälle am gepackten HTML + bestanden, einschließlich der Originalvariablen aus dem bereitgestellten + Studio-Export. Studio-Start, spätere Nachrichten, verzögerte Rückgabe, + Rich Text mit Bild/Formel und Dialogabbrüche funktionieren. +- Zusätzlicher A/B-Browservergleich im frischen Chrome-Kontext: beide + Schriftdateien tatsächlich geladen; identische Textbreiten bei Gewicht + 200/400/700/1000 für `ÄÖÜäöüß Größe 0123456789`. Die Screenshots der + eingebetteten Schemer-Oberfläche sind bytegleich. + +Die Version wurde nicht angehoben. Das optimierte HTML ist ein lokaler +Prüfstand; das veröffentlichte alpha.2-Artefakt wurde nicht ersetzt. + +## mathjs: gemessene Optionen + +Installiert sind Responses 5.3.1 und mathjs 15.2.0. Der Browser erreicht mathjs +über `@iqb/responses/derive/derive-value.js`, das `require('mathjs')` verwendet +und daraus ausschließlich `evaluate` aufruft. Der CommonJS-Einstieg bindet +zahlreiche weitere Exporte ein. Die normale ESM-Ausgabe stellt weiterhin die +volle Ausdruckssprache zur Verfügung und lässt sich besser optimieren. + +Die isolierten Messungen verwenden denselben esbuild 0.28.1 mit `bundle`, +`minify`, Browserplattform, ESM-Ausgabe und Ziel ES2022. Sie messen keine bereits +umgesetzte Ersparnis im vollständigen Angular-HTML: + +| Isolierter Probe-Build | Größe | +| --- | ---: | +| mathjs, bisheriger CommonJS-Zugriff | 1.075.263 B | +| mathjs, ESM-Import von `evaluate` mit vollem Funktionsumfang | 669.432 B | +| mathjs/number | 377.727 B | +| Responses einschließlich bisherigem mathjs-Import | 1.132.292 B | +| Responses mit ausschließlich für den Versuch auf ESM umgeleitetem Math-Import | 747.973 B | + +Im Responses-Versuch spart ESM **384.319 B / 33,94 %**. Die 35 direkten +Ausdrucksfälle liefern gleiche Ergebnisse und Fehler für CommonJS und ESM. +Die ursprüngliche Probe der 180 Ableitungen enthielt kein +`sourceParameters.processing` und verglich deshalb nur Fehlerpfade. Der +inzwischen korrigierte Analyseskript setzt das Pflichtfeld und verlangt +ausdrücklich sowohl erfolgreiche Ableitungen als auch Fehlerfälle. Geprüft werden Rechenfunktionen, +sehr kleine Zahlen, Rundung, bedingte Ausdrücke, Arrays, komplexe Zwischenwerte, +BigNumber/Brüche, Einheiten, Syntaxfehler, leere/nichtnumerische Quellwerte und +drei Antwortstatus. Dies ist eine gezielte Stichprobe, keine vollständige +Kompatibilitätsfreigabe einer neuen Responses-Ausgabe. + +`mathjs/number` ist kein kompatibler Ersatz: Beispielsweise liefern +`sum([1,2,3])`, `mean([1,2,3])` und `abs(complex(3,4))` mit dem vollständigen +mathjs gültige numerische Ergebnisse, mit der reduzierten Ausgabe Fehler. +Auch bei ausschließlich numerischen SOLVER-Endergebnissen werden diese +Funktionen benötigt. + +Reproduktion ohne Änderung installierter Pakete oder Produktionskonfiguration: + +```sh +node scripts/analyze-mathjs-bundle.mjs +``` + +Der nächste saubere Schritt ist ein zusätzlicher ESM-Einstieg in Responses mit +ESM-Import von mathjs, bei weiter verfügbarem CommonJS-Einstieg für Node-Hosts. +Dazu gehören die dortigen Engine-Tests und ein erneuter Vergleich des gesamten +Schemer-Bundles. Die Math-Instanz, Toleranzen und unterstützten Funktionen +sollten dabei erhalten bleiben. Eine alleinige Importänderung in Schemer +behebt den `require` innerhalb des veröffentlichten Responses-Pakets nicht. +Der Produktionsimport bleibt in dieser Optimierung unverändert. diff --git a/docs/schemer-embedded-start-2026-10-16.md b/docs/schemer-embedded-start-2026-10-16.md index f44c55ad..4a1ff122 100644 --- a/docs/schemer-embedded-start-2026-10-16.md +++ b/docs/schemer-embedded-start-2026-10-16.md @@ -1,5 +1,10 @@ # Schemer-Alpha: Basisvariablen im eingebetteten Betrieb +**Späterer Stand:** Dieser Bericht beschreibt die damalige Teilstufe. Die vollständige +Responses-ESM-/Zoneless-Umstellung ist in +[responses-esm-zoneless-2026-10-16.md](responses-esm-zoneless-2026-10-16.md) +dokumentiert. + ## Fehler und Ursache Das veröffentlichte Schemer-HTML 2.9.0-alpha.1 bleibt nach einer gültigen diff --git a/e2e/schemer-embedded.spec.js b/e2e/schemer-embedded.spec.js index e6a795b8..ad491a01 100644 --- a/e2e/schemer-embedded.spec.js +++ b/e2e/schemer-embedded.spec.js @@ -18,6 +18,8 @@ test.afterEach(async ({ page }) => { requestAnimationFrame(() => requestAnimationFrame(resolve)); })); expect(runtimeErrors.get(page)).toEqual([]); + const zones = await Promise.all(page.frames().map(frame => frame.evaluate(() => typeof window.Zone))); + expect(zones.every(value => value === 'undefined')).toBe(true); }); const makeVariable = id => ({ @@ -92,6 +94,26 @@ test('embedded Schemer renders a later host update and its read-only role', asyn await expect(frame.locator('.var-list-entry')).toHaveText(['replacement']); await expect(frame.locator('.var-list-buttons button').first()).toBeDisabled(); await expect(frame.locator('var-coding .variable-details input').first()).toBeDisabled(); + + const withoutRole = makeStart([makeVariable('refresh_without_role')]); + delete withoutRole.schemerConfig; + await page.evaluate(message => { + document.querySelector('iframe').contentWindow.postMessage(message, '*'); + }, withoutRole); + await expect(frame.locator('.var-list-entry')).toHaveText(['refresh_without_role']); + await expect(frame.locator('var-coding .variable-details input').first()).toBeDisabled(); + + await page.evaluate(message => { + document.querySelector('iframe').contentWindow.postMessage(message, '*'); + }, makeStart([makeVariable('replacement')], 'super')); + await expect(frame.locator('.var-list-buttons button').first()).toBeEnabled(); + await expect(frame.locator('var-coding .variable-details input').first()).toBeEnabled(); + + await page.evaluate(message => { + document.querySelector('iframe').contentWindow.postMessage(message, '*'); + }, makeStart([])); + await expect(frame.locator('.var-list-entry')).toHaveCount(0); + await expect(frame.getByText('Es sind keine Basis-Variablen definiert.', { exact: false })).toBeVisible(); }); test('embedded Schemer reports edited coding data back to the host session', async ({ page }) => { @@ -258,3 +280,32 @@ test('embedded Schemer saves source selections in the migrated Material dialog', return coding ? { sourceType: coding.sourceType, sources: coding.deriveSources } : null; })).toEqual({ sourceType: 'SUM_SCORE', sources: ['response_1', 'response_2'] }); }); + +// SOLVER references deliberately use literal ${...} tokens. +/* eslint-disable no-template-curly-in-string */ +[ + { expression: 'sum([${response_1},1,2])', value: '3', result: '6' }, + { expression: 'abs(complex(${response_1},4))', value: '3', result: '5' }, + { expression: 'number(unit(${response_1},"cm"),"mm")', value: '3', result: '30' }, + { expression: 'number(bignumber("0.1")+bignumber("0.2"))', value: '3', result: '0.3' }, + { expression: 'ceil(${response_1})', value: '0.000000000000001', result: '0' }, + { expression: '${response_1}==0 ? 1:0', value: '0.000000000000001', result: '1' } +].forEach(({ expression, value, result }) => { + test(`embedded Schemer preserves full SOLVER semantics: ${expression}`, async ({ page }) => { + const frame = await loadEmbeddedSchemer(page); + await expect(frame.locator('.var-list-entry')).toHaveCount(2); + await frame.locator('.var-list-buttons button').first().click(); + const dialog = frame.getByRole('dialog'); + await dialog.getByLabel('Name', { exact: true }).fill('SolverCheck'); + await dialog.getByRole('combobox', { name: 'Quell-Variablen' }).click(); + await frame.getByRole('option', { name: 'response_1', exact: true }).click(); + await page.keyboard.press('Escape'); + await dialog.getByRole('combobox', { name: 'Ableitungsmethode' }).click(); + await frame.getByRole('option', { name: 'Mathematischer Ausdruck (Solver)', exact: true }).click(); + await dialog.getByLabel('Ausdruck für Solver', { exact: true }).fill(expression); + await dialog.locator('.solver-test-values').getByLabel('response_1', { exact: true }).fill(value); + await dialog.getByRole('button', { name: 'Testen', exact: true }).click(); + await expect(dialog.locator('.solver-test-result-ok')).toHaveText(`Ergebnis: ${result}`); + }); +}); +/* eslint-enable no-template-curly-in-string */ diff --git a/package-lock.json b/package-lock.json index c00ae728..8061ec16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,7 @@ "mathjs": "^15.2.0", "prosemirror-state": "^1.4.4", "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.15.0" + "tslib": "^2.3.0" }, "devDependencies": { "@angular-devkit/build-angular": "^21.2.24", @@ -21138,12 +21137,6 @@ "peerDependencies": { "zod": "^3.25.28 || ^4" } - }, - "node_modules/zone.js": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", - "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" } } } diff --git a/package.json b/package.json index 2525290d..b7aecfb9 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,7 @@ "mathjs": "^15.2.0", "prosemirror-state": "^1.4.4", "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.15.0" + "tslib": "^2.3.0" }, "peerDependencies": { "@tiptap/core": "^2.23.1", diff --git a/playwright.schemer.config.cjs b/playwright.schemer.config.cjs index 708459d2..5b2a7da3 100644 --- a/playwright.schemer.config.cjs +++ b/playwright.schemer.config.cjs @@ -8,6 +8,7 @@ module.exports = defineConfig({ workers: 2, use: { browserName: 'chromium', + channel: process.env.PLAYWRIGHT_CHANNEL, headless: !!process.env.CI, baseURL: 'http://127.0.0.1:4210', screenshot: 'only-on-failure', diff --git a/projects/ngx-coding-components/package.json b/projects/ngx-coding-components/package.json index 0ffbdebe..58e0f590 100644 --- a/projects/ngx-coding-components/package.json +++ b/projects/ngx-coding-components/package.json @@ -17,7 +17,6 @@ }, "homepage": "https://github.com/iqb-berlin/coding-components#readme", "peerDependencies": { - "mathjs": "^12.4.2", "@angular/animations": "^21.0.0", "@angular/cdk": "^21.0.0", "@angular/common": "^21.0.0", @@ -60,7 +59,6 @@ "ngx-tiptap": "^12.0.0", "prosemirror-state": "^1.3.4", "rxjs": "~7.8.0", - "zone.js": "~0.15.0", "@iqb/responses": "^5.3.1" }, "devDependencies": { diff --git a/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.spec.ts b/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.spec.ts index 50bf2944..53ff6723 100644 --- a/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.spec.ts @@ -15,7 +15,9 @@ describe('SelectVariableDialogComponent', () => { ...overrides }; - const component = new SelectVariableDialogComponent(selectData, dialogRef); + const component = new SelectVariableDialogComponent( + selectData, dialogRef, jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']) + ); return { component, dialogRef, selectData }; }; diff --git a/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.ts b/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.ts index 8a253de2..b11a56c7 100644 --- a/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.ts +++ b/projects/ngx-coding-components/src/lib/dialogs/select-variable-dialog.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, OnInit, Inject, ViewChild } from '@angular/core'; import { @@ -69,7 +70,8 @@ export class SelectVariableDialogComponent implements OnInit { constructor( @Inject(MAT_DIALOG_DATA) public selectData: SelectVariableDialogData, - public dialogRef: MatDialogRef + public dialogRef: MatDialogRef, + private changeDetector: ChangeDetectorRef ) {} ngOnInit(): void { @@ -87,6 +89,7 @@ export class SelectVariableDialogComponent implements OnInit { }); setTimeout(() => { this.setSelectedValues(); + this.changeDetector.markForCheck(); }, 500); } diff --git a/projects/ngx-coding-components/src/lib/dialogs/zoneless-rendering.spec.ts b/projects/ngx-coding-components/src/lib/dialogs/zoneless-rendering.spec.ts new file mode 100644 index 00000000..894f9640 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/dialogs/zoneless-rendering.spec.ts @@ -0,0 +1,79 @@ +import { TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { TranslateModule } from '@ngx-translate/core'; +import { Subject } from 'rxjs'; +import { CodeInstructionComponent } from '../var-coding/code-instruction.component'; +import { CodesTitleComponent } from '../var-coding/codes-title.component'; +import { EditProcessingDialogData } from '../var-coding/dialogs/edit-processing-dialog.component'; +import { SelectVariableDialogComponent, SelectVariableDialogData } from './select-variable-dialog.component'; + +describe('native zoneless rendering boundaries', () => { + it('renders an asynchronously returned instruction without forced change detection', async () => { + expect('Zone' in window).toBeFalse(); + const closed = new Subject(); + await TestBed.configureTestingModule({ + imports: [CodeInstructionComponent, TranslateModule.forRoot()], + providers: [{ provide: MatDialog, useValue: { open: () => ({ afterClosed: () => closed }) } }] + }).compileComponents(); + const fixture = TestBed.createComponent(CodeInstructionComponent); + fixture.componentRef.setInput('code', { + id: 0, type: 'FULL_CREDIT', score: 0, label: '', manualInstruction: '

Before

' + }); + await fixture.whenStable(); + expect(fixture.nativeElement.textContent).toContain('Before'); + + fixture.componentInstance.editTextDialog_manualInstruction(); + await Promise.resolve(); + closed.next('

Asynchronous instruction

'); + await fixture.whenStable(); + expect(fixture.nativeElement.textContent).toContain('Asynchronous instruction'); + expect(fixture.nativeElement.textContent).not.toContain('Before'); + closed.complete(); + }); + + it('renders processing and fragments after an asynchronous dialog result', async () => { + const closed = new Subject(); + await TestBed.configureTestingModule({ + imports: [CodesTitleComponent, TranslateModule.forRoot()], + providers: [{ provide: MatDialog, useValue: { open: () => ({ afterClosed: () => closed }) } }] + }).compileComponents(); + const fixture = TestBed.createComponent(CodesTitleComponent); + fixture.componentRef.setInput('processing', []); + await fixture.whenStable(); + fixture.componentInstance.editProcessingAndFragments(); + await Promise.resolve(); + closed.next({ fragmenting: '(fragment)', processing: ['IGNORE_CASE'] }); + await fixture.whenStable(); + expect(fixture.nativeElement.textContent).toContain('processing.IGNORE_CASE'); + expect(fixture.nativeElement.textContent).toContain('(fragment)'); + closed.complete(); + }); + + it('renders the delayed selection and enables confirmation without Zone.js', async () => { + const variable = { id: 'v', alias: 'AliasV', sourceType: 'BASE' as const }; + const data: SelectVariableDialogData = { + title: 'Select', + prompt: '', + variables: [variable], + selectedVariable: variable, + codingStatus: {}, + okButtonLabel: 'Confirm' + }; + await TestBed.configureTestingModule({ + imports: [SelectVariableDialogComponent, TranslateModule.forRoot()], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: data }, + { provide: MatDialogRef, useValue: { close: () => undefined } } + ] + }).compileComponents(); + const fixture = TestBed.createComponent(SelectVariableDialogComponent); + await fixture.whenStable(); + // The component deliberately initializes its selection after 500 ms. + await new Promise(resolve => { setTimeout(resolve, 550); }); + await fixture.whenStable(); + expect(fixture.componentInstance.getSelected()).toEqual(['AliasV']); + const button = fixture.nativeElement.querySelector('button'); + expect(button.disabled).toBeFalse(); + expect(fixture.nativeElement.querySelector('mat-list-option').getAttribute('aria-selected')).toBe('true'); + }); +}); diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.spec.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.spec.ts index 2dc210ff..5ccefa29 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.spec.ts @@ -57,7 +57,9 @@ describe('RichTextEditorComponent', () => { const matDialog = { open: jasmine.createSpy('open').and.returnValue(dialogRef) } as unknown as MatDialog; - const component = new RichTextEditorComponent(matDialog); + const component = new RichTextEditorComponent( + matDialog, jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']) + ); const commandSpyHost: CommandSpyHost = { focus: jasmine.createSpy('focus'), diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts index f270a81a..d952e597 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, EventEmitter, Input, Output, AfterViewInit, OnChanges, SimpleChanges } from '@angular/core'; @@ -112,7 +113,10 @@ export class RichTextEditorComponent implements AfterViewInit, OnChanges { extensions: this.defaultExtensions }); - constructor(private editFormulaDialog: MatDialog) {} + constructor( + private editFormulaDialog: MatDialog, + private changeDetector: ChangeDetectorRef + ) {} ngAfterViewInit(): void { this.editor.commands.focus(); @@ -246,6 +250,7 @@ export class RichTextEditorComponent implements AfterViewInit, OnChanges { this.editor.commands.insertBlockImage({ src: mediaSrc }); } } + this.changeDetector.markForCheck(); } toggleBlockquote(): void { @@ -263,6 +268,7 @@ export class RichTextEditorComponent implements AfterViewInit, OnChanges { }); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (typeof dialogResult === 'string' && normalizeFormulaLatex(dialogResult)) { this.editor.chain() .insertIqbMathFormula(normalizeFormulaLatex(dialogResult)) @@ -288,6 +294,7 @@ export class RichTextEditorComponent implements AfterViewInit, OnChanges { }); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (typeof dialogResult === 'string' && normalizeFormulaLatex(dialogResult)) { const transaction = this.editor.state.tr.setNodeMarkup(position, null, { latex: normalizeFormulaLatex(dialogResult) diff --git a/projects/ngx-coding-components/src/lib/schemer/schemer-host.integration.spec.ts b/projects/ngx-coding-components/src/lib/schemer/schemer-host.integration.spec.ts index 167378f8..b8046dc7 100644 --- a/projects/ngx-coding-components/src/lib/schemer/schemer-host.integration.spec.ts +++ b/projects/ngx-coding-components/src/lib/schemer/schemer-host.integration.spec.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -12,7 +12,7 @@ import { SchemerComponent } from './schemer.component'; @Component({ template: ` - @if (showSchemer) { + @if (showSchemer()) { { }).compileComponents(); fixture = TestBed.createComponent(SchemerHostComponent); - fixture.detectChanges(); + await fixture.whenStable(); }); const getSchemer = (): SchemerComponent => fixture.debugElement @@ -65,18 +65,18 @@ describe('Schemer host lifecycle integration', () => { .query(By.directive(VarCodingComponent)) .componentInstance as VarCodingComponent; - it('should display a selected coding immediately after rebuilding its host', () => { + it('should display a selected coding immediately after rebuilding its host', async () => { const firstSchemer = getSchemer(); expect(firstSchemer.selectedCoding$.getValue()?.id).toBe('base'); expect(getVarCoding().varCoding?.id).toBe('base'); - fixture.componentInstance.showSchemer = false; - fixture.detectChanges(); + fixture.componentInstance.showSchemer.set(false); + await fixture.whenStable(); expect(fixture.debugElement.query(By.directive(SchemerComponent))) .toBeNull(); - fixture.componentInstance.showSchemer = true; - fixture.detectChanges(); + fixture.componentInstance.showSchemer.set(true); + await fixture.whenStable(); const rebuiltSchemer = getSchemer(); expect(rebuiltSchemer).not.toBe(firstSchemer); diff --git a/projects/ngx-coding-components/src/lib/schemer/schemer.component.spec.ts b/projects/ngx-coding-components/src/lib/schemer/schemer.component.spec.ts index 73c961a3..bf83f844 100644 --- a/projects/ngx-coding-components/src/lib/schemer/schemer.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/schemer/schemer.component.spec.ts @@ -1,10 +1,9 @@ /* eslint-disable max-classes-per-file */ -import { fakeAsync, tick } from '@angular/core/testing'; import { CodingSchemeFactory, CodingSchemeProblem } from '@iqb/responses'; import { CodingFactory } from '@iqb/responses/coding-factory'; import { VariableCodingData } from '@iqbspecs/coding-scheme/coding-scheme.interface'; import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; -import { of, Subject } from 'rxjs'; +import { firstValueFrom, of, Subject } from 'rxjs'; import { MessageDialogComponent } from '../dialogs/message-dialog.component'; import { FileService } from '../services/file.service'; import { VarCodingComponent } from '../var-coding/var-coding.component'; @@ -94,7 +93,8 @@ describe('SchemerComponent', () => { dialog as unknown as never, selectVariableDialog as unknown as never, editSourceParametersDialog as unknown as never, - inputDialog as unknown as never + inputDialog as unknown as never, + jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']) ); }); @@ -305,8 +305,8 @@ describe('SchemerComponent', () => { expect(component.selectedCoding$.getValue()).toBeNull(); }); - it('varCodingElement setter should subscribe when var-coding appears after conflict recovery', fakeAsync(() => { - const emitSpy = spyOn(component.codingSchemeChanged, 'emit'); + it('varCodingElement setter should subscribe when var-coding appears after conflict recovery', async () => { + const emitSpy = spyOn(component.codingSchemeChanged, 'emit').and.callThrough(); const changes$ = new Subject(); schemerService.setCodingScheme({ variableCodings: [] } as unknown as never); @@ -316,13 +316,14 @@ describe('SchemerComponent', () => { varCodingChanged: changes$.asObservable() } as unknown as VarCodingComponent; + const changed = firstValueFrom(component.codingSchemeChanged); changes$.next(null); - tick(300); + await changed; expect(schemerFacade.tryResolveIdentifierConflicts).toHaveBeenCalled(); expect(emitSpy).toHaveBeenCalledWith(schemerService.codingScheme); changes$.complete(); - })); + }); it('ngOnDestroy should reset duplicate resolution state', () => { component.ngOnDestroy(); diff --git a/projects/ngx-coding-components/src/lib/schemer/schemer.component.ts b/projects/ngx-coding-components/src/lib/schemer/schemer.component.ts index c3faf20c..4b65f594 100644 --- a/projects/ngx-coding-components/src/lib/schemer/schemer.component.ts +++ b/projects/ngx-coding-components/src/lib/schemer/schemer.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, EventEmitter, Input, @@ -99,6 +100,7 @@ export class SchemerComponent implements OnDestroy { this.varCodingChangedSubscription = value.varCodingChanged .pipe(debounceTime(300)) .subscribe(() => { + this.changeDetector.markForCheck(); this.updateVariableLists(); if (!this.hasIdentifierConflict) { this.codingSchemeChanged.emit(this.schemerService.codingScheme); @@ -155,7 +157,8 @@ export class SchemerComponent implements OnDestroy { private showCodingSchemeDialog: MatDialog, private selectVariableDialog: MatDialog, private editSourceParametersDialog: MatDialog, - private inputDialog: MatDialog + private inputDialog: MatDialog, + private changeDetector: ChangeDetectorRef ) {} private tr( @@ -172,6 +175,7 @@ export class SchemerComponent implements OnDestroy { } updateVariableLists() { + this.changeDetector.markForCheck(); const previousSelectedId = this.selectedCoding$.value?.id; if (this.schemerFacade.tryResolveIdentifierConflicts()) { @@ -491,6 +495,7 @@ export class SchemerComponent implements OnDestroy { } ); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (dialogResult && typeof dialogResult === 'object' && this.schemerService.codingScheme) { const timestamp = new Date().getTime(); const dialogResultTyped: EditSourceParametersDialogData = @@ -559,6 +564,7 @@ export class SchemerComponent implements OnDestroy { } ); dialogRef.afterClosed().subscribe((variables: string[]) => { + this.changeDetector.markForCheck(); let changed = false; if (variables && variables.length > 0) { if (this.schemerService && this.schemerService.codingScheme) { @@ -600,6 +606,7 @@ export class SchemerComponent implements OnDestroy { data: dialogData }); dialogRef.afterClosed().subscribe(result => { + this.changeDetector.markForCheck(); if (typeof result === 'string') { const variableCodings = this.schemerService.codingScheme?.variableCodings; @@ -662,6 +669,7 @@ export class SchemerComponent implements OnDestroy { } ); dialogRef.afterClosed().subscribe((variables: string[]) => { + this.changeDetector.markForCheck(); let changed = false; if (variables && variables.length > 0) { variables.forEach(v => { @@ -739,6 +747,7 @@ export class SchemerComponent implements OnDestroy { } ); dialogRef.afterClosed().subscribe((variables: string[]) => { + this.changeDetector.markForCheck(); let changed = false; if (variables && variables.length > 0) { variables.forEach(v => { diff --git a/projects/ngx-coding-components/src/lib/var-coding/code-instruction.component.ts b/projects/ngx-coding-components/src/lib/var-coding/code-instruction.component.ts index 8df98607..76c676da 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/code-instruction.component.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/code-instruction.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; @@ -116,7 +117,8 @@ export class CodeInstructionComponent { constructor( private sanitizer: DomSanitizer, public translateService: TranslateService, - public editTextDialog: MatDialog + public editTextDialog: MatDialog, + private changeDetector: ChangeDetectorRef ) {} editTextDialog_manualInstruction(): void { @@ -134,6 +136,7 @@ export class CodeInstructionComponent { autoFocus: false }); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (typeof dialogResult === 'string' && this.code) { this.code.manualInstruction = dialogResult; this.codeDataChanged.emit(this.code); diff --git a/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rule-list.component.ts b/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rule-list.component.ts index 8efa1dbd..883e8bc6 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rule-list.component.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rule-list.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -81,7 +82,8 @@ export class CodeRuleListComponent { public schemerService: SchemerService, public editRuleReferenceDialog: MatDialog, private iconRegistry: MatIconRegistry, - private sanitizer: DomSanitizer + private sanitizer: DomSanitizer, + private changeDetector: ChangeDetectorRef ) { const icons = [ { name: 'match-word', svg: asSvg(MATCH_WORD_SVG) }, @@ -158,6 +160,7 @@ export class CodeRuleListComponent { autoFocus: false }); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (dialogResult !== false && dialogResult !== undefined && dialogResult !== null) { rule.fragment = dialogResult; this.setCodeRulesChanged(); diff --git a/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rules.component.ts b/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rules.component.ts index 979dffa8..832cd470 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rules.component.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/code-rules/code-rules.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, EventEmitter, Input, @@ -60,7 +61,8 @@ export class CodeRulesComponent { constructor( public schemerService: SchemerService, - public editRuleReferenceDialog: MatDialog + public editRuleReferenceDialog: MatDialog, + private changeDetector: ChangeDetectorRef ) {} setCodeRulesChanged() { @@ -105,6 +107,7 @@ export class CodeRulesComponent { } ); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (dialogResult !== false && dialogResult !== undefined && dialogResult !== null) { ruleSet.valueArrayPos = dialogResult; this.setCodeRulesChanged(); diff --git a/projects/ngx-coding-components/src/lib/var-coding/codes-title.component.ts b/projects/ngx-coding-components/src/lib/var-coding/codes-title.component.ts index 0051489a..359226e6 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/codes-title.component.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/codes-title.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -107,7 +108,8 @@ export class CodesTitleComponent { constructor( public schemerService: SchemerService, - private editProcessingDialog: MatDialog + private editProcessingDialog: MatDialog, + private changeDetector: ChangeDetectorRef ) {} sortCodes(event: MouseEvent) { @@ -129,6 +131,7 @@ export class CodesTitleComponent { } ); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (dialogResult && typeof dialogResult === 'object') { const dialogResultTyped: EditProcessingDialogData = dialogResult; if (this.fragmenting !== dialogResultTyped.fragmenting) { diff --git a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts index 252527e3..a4ecf8da 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts @@ -1,7 +1,6 @@ import { SourceType, VariableSourceParameters, SourceProcessingType } from '@iqbspecs/coding-scheme/coding-scheme.interface'; import { TranslateService } from '@ngx-translate/core'; -import { fakeAsync, tick } from '@angular/core/testing'; import { EditSourceParametersDialog, EditSourceParametersDialogData } from './edit-source-parameters-dialog.component'; import { SchemerService } from '../../services/schemer.service'; @@ -404,7 +403,7 @@ describe('EditSourceParametersDialog', () => { expect(dialog.solverSourceWarning).toBeNull(); }); - it('insertSolverSourceReference should insert the selected source at the cursor', fakeAsync(() => { + it('insertSolverSourceReference should insert the selected source at the cursor', async () => { const dialog = createDialog({ selfAlias: 'D', sourceType: 'SOLVER', @@ -416,16 +415,16 @@ describe('EditSourceParametersDialog', () => { input.setSelectionRange(6, 6); dialog.insertSolverSourceReference({ id: 'v1', label: 'V1' }, input); - tick(); + await new Promise(resolve => { setTimeout(resolve); }); expect(dialog.data.sourceParameters.solverExpression).toBe( `round(${solverRef('V1')} + 1)` ); expect(input.selectionStart).toBe(11); expect(input.selectionEnd).toBe(11); - })); + }); - it('insertSolverSourceReference should replace the selected expression range', fakeAsync(() => { + it('insertSolverSourceReference should replace the selected expression range', async () => { const dialog = createDialog({ selfAlias: 'D', sourceType: 'SOLVER', @@ -437,13 +436,13 @@ describe('EditSourceParametersDialog', () => { input.setSelectionRange(4, 15); dialog.insertSolverSourceReference({ id: 'v2', label: 'V2' }, input); - tick(); + await new Promise(resolve => { setTimeout(resolve); }); expect(dialog.data.sourceParameters.solverExpression).toBe( `1 + ${solverRef('V2')}` ); expect(dialog.solverTestResult).toBeNull(); - })); + }); it('getSolverSourceReference should expose the inserted reference syntax', () => { const dialog = createDialog({ diff --git a/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts b/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts index 730d9806..551101fc 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Component, EventEmitter, Input, @@ -137,13 +138,15 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { private confirmDialog: MatDialog, private generateCodingDialog: MatDialog, private messageDialog: MatDialog, - private editSourceParametersDialog: MatDialog + private editSourceParametersDialog: MatDialog, + private changeDetector: ChangeDetectorRef ) {} ngOnInit() { this.lastChangeFromSubscription = this.lastChangeFrom$ .pipe(debounceTime(500)) .subscribe(source => { + this.changeDetector.markForCheck(); if (source !== 'init') { this.updateHasResidualAutoCode(); this.updateHasIntendedIncompleteAutoCode(); @@ -204,6 +207,7 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { autoFocus: false }); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (typeof dialogResult === 'string' && this.varCoding) { this.varCoding.manualInstruction = dialogResult; this.varCodingChanged.emit(this.varCoding); @@ -232,6 +236,7 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { } }); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if (typeof dialogResult === 'string') { this.schemerService.setCodingToTextMode( dialogResult as CodingToTextMode @@ -261,6 +266,7 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { } }); dialogRef.afterClosed().subscribe(result => { + this.changeDetector.markForCheck(); if (result === true && this.schemerService.codingScheme) { if (this.varCoding && this.varCoding.sourceType === 'BASE') { this.schemerService.codingScheme.variableCodings = @@ -313,6 +319,7 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { } ); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if ( typeof dialogResult !== 'undefined' && dialogResult !== false && @@ -396,6 +403,7 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { } }); dialogRef.afterClosed().subscribe(result => { + this.changeDetector.markForCheck(); if (result === true) this.performPasteSingleCode(); }); return; @@ -494,6 +502,7 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { } ); dialogRef.afterClosed().subscribe(dialogResult => { + this.changeDetector.markForCheck(); if ( dialogResult && typeof dialogResult === 'object' && this.varCoding diff --git a/projects/schemer/src/app/app.component.ts b/projects/schemer/src/app/app.component.ts index b5ec4fa5..c68ffbf0 100644 --- a/projects/schemer/src/app/app.component.ts +++ b/projects/schemer/src/app/app.component.ts @@ -1,4 +1,6 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + Component, OnDestroy, OnInit, signal +} from '@angular/core'; import { Subject, takeUntil } from 'rxjs'; import { CodingFactory } from '@iqb/responses/coding-factory'; @@ -24,16 +26,16 @@ import { VeronaAPIService, VosStartCommand } from './verona-api.service'; template: ` @if (isStandalone) { (); isStandalone: boolean = window === window.parent; - varList: VariableInfo[] = []; - codings: CodingScheme | null = null; - userRole: UserRoleType = 'RW_MAXIMAL'; + readonly varList = signal([]); + readonly codings = signal(null); + readonly userRole = signal('RW_MAXIMAL'); title = 'schemer'; constructor( private veronaAPIService: VeronaAPIService, @@ -82,7 +84,8 @@ export class AppComponent implements OnInit, OnDestroy { this.veronaAPIService.vosStartCommand .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((message: VosStartCommand) => { - this.varList = message.variables; + this.varList.set(message.variables); + let codings: CodingScheme; if (message.codingScheme) { const compareVersionResult = CodingScheme.checkVersion(message.codingScheme); if (compareVersionResult !== 'OK') { @@ -106,59 +109,64 @@ export class AppComponent implements OnInit, OnDestroy { } }); } - this.codings = new CodingScheme(message.codingScheme); + codings = new CodingScheme(message.codingScheme); + this.codings.set(codings); if (compareVersionResult !== 'OK') { this.emitCodingSchemeChanged(); } } else { - this.codings = new CodingScheme([]); + codings = new CodingScheme([]); + this.codings.set(codings); } - this.varList.forEach(vi => { + message.variables.forEach(vi => { let codingForBaseVariable; - if (this.codings && this.codings.variableCodings) { - codingForBaseVariable = this.codings.variableCodings.find(v => v.id === vi.id); + if (codings.variableCodings) { + codingForBaseVariable = codings.variableCodings.find(v => v.id === vi.id); } if (codingForBaseVariable) { codingForBaseVariable.alias = vi.alias || vi.id; - } else if (this.codings) { - if (vi.type === 'no-value') { - const newNoValueBaseVar = CodingFactory.createBaseCodingVariable(vi.id, 'BASE_NO_VALUE'); - if (vi.alias) newNoValueBaseVar.alias = vi.alias; - this.codings.variableCodings.push(newNoValueBaseVar); - } else { - const newBaseVar = CodingFactory.createCodingVariable(vi.id); - if (vi.alias) newBaseVar.alias = vi.alias; - this.codings.variableCodings.push(newBaseVar); - } + } else if (vi.type === 'no-value') { + const newNoValueBaseVar = CodingFactory.createBaseCodingVariable(vi.id, 'BASE_NO_VALUE'); + if (vi.alias) newNoValueBaseVar.alias = vi.alias; + codings.variableCodings.push(newNoValueBaseVar); + } else { + const newBaseVar = CodingFactory.createCodingVariable(vi.id); + if (vi.alias) newBaseVar.alias = vi.alias; + codings.variableCodings.push(newBaseVar); } }); - if (message.schemerConfig && message.schemerConfig.role && message.schemerConfig.role !== 'super') { - this.userRole = ['guest', 'commenter'].includes(message.schemerConfig.role) ? 'RO' : 'RW_MINIMAL'; + const role = message.schemerConfig?.role; + if (role) { + let userRole: UserRoleType = 'RW_MAXIMAL'; + if (role !== 'super') { + userRole = ['guest', 'commenter'].includes(role) ? 'RO' : 'RW_MINIMAL'; + } + this.userRole.set(userRole); } }); } emitCodingSchemeChanged() { - this.veronaAPIService.sendVosSchemeChangedNotification(this.codings); + this.veronaAPIService.sendVosSchemeChangedNotification(this.codings()); } setNewVarlist(varList: VariableInfo[] | null) { if (varList) { - this.varList = varList; + this.varList.set(varList); const variableCodings: VariableCodingData[] = []; - this.varList.forEach(c => { + varList.forEach(c => { variableCodings.push(CodingFactory.createCodingVariable(c.id)); }); - this.codings = new CodingScheme(variableCodings); + this.codings.set(new CodingScheme(variableCodings)); } } setNewCodingScheme(codings: CodingScheme | null) { - if (codings) this.codings = codings; + if (codings) this.codings.set(codings); } setUserRole(newRole: UserRoleType) { - this.userRole = newRole; + this.userRole.set(newRole); } ngOnDestroy(): void { diff --git a/projects/schemer/src/assets/fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf b/projects/schemer/src/assets/fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf deleted file mode 100644 index 8a034686..00000000 Binary files a/projects/schemer/src/assets/fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf and /dev/null differ diff --git a/projects/schemer/src/assets/fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.woff2 b/projects/schemer/src/assets/fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.woff2 new file mode 100644 index 00000000..09d857a5 Binary files /dev/null and b/projects/schemer/src/assets/fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.woff2 differ diff --git a/projects/schemer/src/assets/nunitosans.css b/projects/schemer/src/assets/nunitosans.css index 62a2e01c..5b8b1742 100644 --- a/projects/schemer/src/assets/nunitosans.css +++ b/projects/schemer/src/assets/nunitosans.css @@ -1,4 +1,4 @@ @font-face { font-family: 'NunitoSans'; - src: url('./fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf'); + src: url('./fonts/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.woff2') format('woff2'); } diff --git a/projects/schemer/src/environments/environment.ts b/projects/schemer/src/environments/environment.ts index f56ff470..71e75c9b 100644 --- a/projects/schemer/src/environments/environment.ts +++ b/projects/schemer/src/environments/environment.ts @@ -5,12 +5,3 @@ export const environment = { production: false }; - -/* - * For easier debugging in development mode, you can import the following file - * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. - * - * This import should be commented out in production mode because it will have a negative impact - * on performance if an error is thrown. - */ -// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/projects/schemer/src/html_wrapper/index.html b/projects/schemer/src/html_wrapper/index.html index 18a71ea8..d8076c03 100644 --- a/projects/schemer/src/html_wrapper/index.html +++ b/projects/schemer/src/html_wrapper/index.html @@ -36,7 +36,6 @@ - diff --git a/projects/schemer/src/main.ts b/projects/schemer/src/main.ts index 74410fa3..be768730 100644 --- a/projects/schemer/src/main.ts +++ b/projects/schemer/src/main.ts @@ -1,5 +1,4 @@ -import 'zone.js'; -import { enableProdMode, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; +import { enableProdMode, importProvidersFrom, provideZonelessChangeDetection } from '@angular/core'; import { createApplication } from '@angular/platform-browser'; import { provideHttpClient } from '@angular/common/http'; import { createCustomElement } from '@angular/elements'; @@ -17,9 +16,7 @@ if (environment.production) { (async () => { const app = await createApplication({ providers: [ - // Host messages and debounced edits still rely on Zone.js. Angular 21 otherwise - // defaults to zoneless rendering, leaving embedded start commands invisible. - provideZoneChangeDetection(), + provideZonelessChangeDetection(), provideAnimations(), provideHttpClient(), importProvidersFrom( diff --git a/projects/schemer/src/polyfills.ts b/projects/schemer/src/polyfills.ts deleted file mode 100644 index aa09a9ff..00000000 --- a/projects/schemer/src/polyfills.ts +++ /dev/null @@ -1 +0,0 @@ -import 'zone.js'; diff --git a/projects/schemer/src/test.ts b/projects/schemer/src/test.ts index e63a8760..14348dca 100644 --- a/projects/schemer/src/test.ts +++ b/projects/schemer/src/test.ts @@ -1,15 +1,13 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js'; -import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; + BrowserTestingModule, + platformBrowserTesting +} from '@angular/platform-browser/testing'; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() + BrowserTestingModule, + platformBrowserTesting() ); diff --git a/projects/schemer/tsconfig.spec.json b/projects/schemer/tsconfig.spec.json index b66a2f0b..9e91291d 100644 --- a/projects/schemer/tsconfig.spec.json +++ b/projects/schemer/tsconfig.spec.json @@ -1,4 +1,3 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../../tsconfig.json", "compilerOptions": { @@ -8,8 +7,7 @@ ] }, "files": [ - "src/test.ts", - "src/polyfills.ts" + "src/test.ts" ], "include": [ "src/**/*.spec.ts", diff --git a/scripts/analyze-mathjs-bundle.mjs b/scripts/analyze-mathjs-bundle.mjs new file mode 100644 index 00000000..f9616cb8 --- /dev/null +++ b/scripts/analyze-mathjs-bundle.mjs @@ -0,0 +1,115 @@ +// Run with: node scripts/analyze-mathjs-bundle.mjs +// Probes are isolated; installed packages and production build settings stay unchanged. +import assert from 'node:assert/strict'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const directory = await mkdtemp(join(tmpdir(), 'schemer-mathjs-')); +const sizes = {}; +const responsesManifest = createRequire(import.meta.url)('@iqb/responses/package.json'); + +async function bundle(name, contents, plugins = []) { + const result = await build({ + stdin: { contents, resolveDir: root, sourcefile: `${name}.js` }, + bundle: true, minify: true, platform: 'browser', format: 'esm', + target: 'es2022', write: false, plugins + }); + sizes[name] = result.outputFiles[0].contents.length; + const path = join(directory, `${name}.mjs`); + await writeFile(path, result.outputFiles[0].contents); + return import(pathToFileURL(path).href); +} + +const expressions = [ + '1+2*3', '2^10', 'round(1.2345,2)', 'ceil(1e-15)', 'floor(-1.2)', + 'sqrt(3^2+4^2)', 'sin(pi/2)', 'cos(0)', 'log(100,10)', 'exp(1)', + 'abs(-5)', 'min(3,1,2)', 'max(3,1,2)', 'sum([1,2,3])', 'mean([1,2,3])', + 'median([1,2,9])', 'std([1,2,3])', 'factorial(5)', 'combinations(5,2)', + 'gcd(24,18)', 'mod(-5,3)', '1e-15==0 ? 1:0', '3>2 ? 7:0', 'null', + '1/0', 'sqrt(-4)', 'abs(complex(3,4))', 're(sqrt(-4))', + 'number(bignumber("0.1")+bignumber("0.2"))', 'number(fraction(1,3))', + 'number(unit(2,"cm"),"mm")', 'det([[1,2],[3,4]])', 'size([1,2,3],1)', + 'sin(', 'unknownFunction(2)' +]; + +function evaluate(evaluator, expression) { + try { + const value = evaluator(expression); + return JSON.stringify({ type: typeof value, value }); + } catch (error) { + return JSON.stringify({ error: error.message }); + } +} + +try { + const commonjs = await bundle('commonjs', 'const { evaluate } = require("mathjs"); export { evaluate };'); + const esm = await bundle('esm', 'export { evaluate } from "mathjs";'); + const number = await bundle('number', 'export { evaluate } from "mathjs/number";'); + const numberDifferences = []; + for (const expression of expressions) { + const expected = evaluate(commonjs.evaluate, expression); + assert.equal(evaluate(esm.evaluate, expression), expected, expression); + const reduced = evaluate(number.evaluate, expression); + if (reduced !== expected) numberDifferences.push({ expression, expected, reduced }); + } + + const entry = 'const { CodingSchemeFactory } = require("@iqb/responses"); export { CodingSchemeFactory };'; + const original = await bundle('responsesCommonjs', entry); + const native = await bundle('responsesNativeImport', 'export { CodingSchemeFactory } from "@iqb/responses";'); + let redirects = 0; + const redirected = await bundle('responsesEsmMath', entry, [{ + name: 'probe-esm-math', + setup(builder) { + builder.onResolve({ filter: /^mathjs$/ }, args => + args.importer.replaceAll('\\', '/').endsWith('/@iqb/responses/derive/derive-value.js') + ? { path: 'mathjs-evaluate', namespace: 'probe' } : undefined); + builder.onLoad({ filter: /.*/, namespace: 'probe' }, () => { + redirects += 1; + return { contents: 'export { evaluate } from "mathjs";', resolveDir: root, loader: 'js' }; + }); + } + }]); + assert.equal(redirects, 1, 'probe must redirect precisely the installed SOLVER math import'); + + const source = { id: 'a', alias: 'A', sourceType: 'BASE', codes: [] }; + const solverExpressions = [ + '${A}+1', 'round(${A}/3,2)', 'ceil(${A})', '${A}==0 ? 1:0', + 'abs(complex(${A},4))', 'number(unit(${A},"cm"),"mm")', + 'sum([${A},1,2])', 'null', '${A}/0', '${A}+' + ]; + let responseDerivationPairs = 0; + const statuses = {}; + for (const expression of solverExpressions) { + for (const value of [0, 1e-15, 3, -5, null, 'abc']) { + for (const status of ['VALUE_CHANGED', 'CODING_COMPLETE', 'INVALID']) { + const target = { id: 'd', alias: 'D', sourceType: 'SOLVER', deriveSources: ['a'], + sourceParameters: { processing: [], solverExpression: expression } }; + const args = [[source, target], target, [{ id: 'a', status, value }]]; + const result = redirected.CodingSchemeFactory.deriveValue(...structuredClone(args)); + statuses[result.status] = (statuses[result.status] || 0) + 1; + assert.deepEqual( + result, + original.CodingSchemeFactory.deriveValue(...structuredClone(args)), + JSON.stringify({ expression, value, status }) + ); + assert.deepEqual(native.CodingSchemeFactory.deriveValue(...structuredClone(args)), + result, `native ESM: ${expression}`); + responseDerivationPairs += 1; + } + } + } + assert.ok(statuses.VALUE_CHANGED > 0); + assert.ok(statuses.DERIVE_ERROR > 0); + console.log(JSON.stringify({ + responses: { version: responsesManifest.version, + nativeEsm: Boolean(responsesManifest.exports?.['.']?.import) }, + sizes, expressionParity: expressions.length, responseDerivationPairs, statuses, numberDifferences + }, null, 2)); +} finally { + await rm(directory, { recursive: true, force: true }); +} diff --git a/src/main.ts b/src/main.ts index d9a84511..c2913f49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { importProvidersFrom } from '@angular/core'; +import { importProvidersFrom, provideZonelessChangeDetection } from '@angular/core'; import { createApplication } from '@angular/platform-browser'; import { provideHttpClient } from '@angular/common/http'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; @@ -7,11 +7,11 @@ import { provideAnimations } from '@angular/platform-browser/animations'; import { SchemerTranslateLoader } from '@ngx-coding-components/translations/schemer-translate-loader'; import { registerNgxCodingComponentsElements } from '@ngx-coding-components/elements/register-elements'; import { AppComponent } from './app/app.component'; -import 'zone.js'; (async () => { const app = await createApplication({ providers: [ + provideZonelessChangeDetection(), provideAnimations(), provideHttpClient(), TranslateService, diff --git a/src/test.ts b/src/test.ts index 76dfad3f..9915b2ef 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,12 +1,10 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js'; -import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; + BrowserTestingModule, + platformBrowserTesting +} from '@angular/platform-browser/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { @@ -17,8 +15,8 @@ declare const require: { // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() + BrowserTestingModule, + platformBrowserTesting() ); // Then we find all the tests.