Compare commits
2 Commits
10eacdeaf5
...
21e18b81d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 21e18b81d8 | |||
| e6fdb39a86 |
+4
-3
@@ -641,7 +641,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
|
||||
- [x] H1 simple/pro Mode (uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist) *(2026-08-29: uiModeService statt uiStore — zustand war nie installiert (Q2 nie umgesetzt), Muster folgt documentService/featureFlags-Stil; localStorage webcad.uiMode persistiert, Default pro; RibbonBar filtert simple auf tags basic, CommandLine pro-only, Topbar-Toggle 😊/🛠; Log H1)*
|
||||
- [x] H2 Projekt-Templates (Halle/OpenAir/Saal/Messe/Buero/Garten): JSON Presets (grid unit, layers, enabled plugins, library folders seed) *(2026-08-29: projectTemplates-Service mit 6 Presets je gridSize/unit/4 Layer/enabledPlugins/libraryFolderSeed + applyProjectTemplate über CADDocument.replaceLayers; Log H2)*
|
||||
- [x] H3 Rollen: library-admin (global write) vs user (own folders) — an bestehendes auth anbinden, Backend guard *(2026-08-29: requireLibraryAdmin an bestehende admin-Rolle gebunden; alle 8 schreibenden Global-Library-Routen guarded (POST/PATCH/DELETE folders+blocks, import, import-zip), Lesen/Export für authentifizierte offen; Log H3)*
|
||||
- [ ] H4 i18n Grundlage: strings.ts zentrale Maps de/en, UI-Texte ersetzen (mechanisch, Teilautomatisierbar)
|
||||
- [x] H4 i18n Grundlage: strings.ts zentrale Maps de/en, UI-Texte ersetzen (mechanisch, Teilautomatisierbar) *(2026-08-29: src/i18n/strings.ts — de/en-Maps, t() mit Key-Fallback, Sprachpersistenz (webcad.language, Default de), subscribeLanguage + useT-Hook; Kern-UI-Texte LayoutBar + Topbar-Modus umgestellt, Live-Wechsel-Test; vollständige Ersetzung aller Komponenten = teilautomatisierbares Follow-up; Log H4)*
|
||||
- [ ] H5 Flächenberechnung Polygon (m²) Info im PropertiesPanel + measure-area Tool
|
||||
|
||||
## Phase I — Qualität & Release
|
||||
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
|
||||
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-29 | H4 | i18n-Grundlage: (1) src/i18n/strings.ts NEU — zentrale DE/EN-Maps (Key-Pfad → String oder parametrisierte Funktion für dynamische Texte wie „Layout X löschen“), t(key, arg) mit Key-Fallback bei unbekannten Keys (offensichtlich sichtbar statt still), LANGUAGES-Konstante, getLanguage/setLanguage mit localStorage-Persistenz (webcad.language, Default de, ungültige Werte → de) analog uiModeService-Muster, subscribeLanguage-Set, useT-React-Hook erzwingt Re-Render bei Sprachwechsel; (2) Kern-UI-Texte auf t() umgestellt: LayoutBar (Titel, Hinzufügen, Leer, Löschen, Drucken, Titelblock, Zoom-to) + Topbar-Modus-Toggle (title + aria-label) — mit echtem Live-Wechsel getestet (de → en im laufenden Render); (3) vollständige mechanische Ersetzung ALLER Komponenten-Texte bewusst als teilautomatisierbares Follow-up markiert (Roadmap-Wortlaut); Zwischenfälle selbst behoben: Python-Quoting-Abbruch beim ersten Implementierungsversch (strings.ts wurde nicht geschrieben, Komponenten referenzierten useT schon → nachgetragen) + Testing-Library-State-Update außerhalb act() (Sprachwechsel im Test in act() gewrappt) | Frontend: 610/610 Tests (+8 i18n.test.tsx), tsc 0, Build EXIT 0 | e6fdb39 | H4 COMPLETE; nächster Task H5 (Flächenberechnung Polygon m² + measure-area Tool) |
|
||||
| 2026-08-29 | H3 | Rollen library-admin vs. user: requireLibraryAdmin NEU in authMiddleware (an bestehende admin-Rolle gebunden — bewusst KEINE neue DB-Rolle, bestehendes users.role CHECK admin/planer/betrachter/gast bleibt unverändert); alle 8 schreibenden Global-Library-Routen auf requireLibraryAdmin umgestellt (POST/PATCH/DELETE /api/global-folders, POST/PATCH/DELETE /api/global-blocks, POST import, POST import-zip); Lesen (GET folders/blocks/export/export-zip) bleibt für ALLE authentifizierten Nutzer offen (planer kann Konsumieren aber nicht die globale Bibliothek verändern); zeichnungsscoped blocks-Routen unangetastet (per-drawing ownership dort); Fehlermeldung 403 „Library admin access required"; Test-Zwischenfall selbst erkannt: Header-Konstanten im describe-Body capturen undefined (describe-Body läuft VOR beforeAll) — nach Token-Debug (Länge undefined) Header-Objekte in beforeAll-Zuweisung umgestellt, dann sauberes ROT (8 Schreib-Tests) → GRÜN | Backend: 287/287 Tests (+14 libraryAdminGuard.test.ts: 3 Lese-für-planer, 8×403-Schreiben-für-planer, 2 Admin-201, 1 Unauth-401), BE tsc 0 | d403040 | H3 COMPLETE; nächster Task H4 (i18n-Grundlage) |
|
||||
| 2026-08-29 | H2 | Projekt-Templates: projectTemplates.ts NEU — ProjectTemplate-Typ (id/name/description/gridSize/unit/layers/enabledPlugins/libraryFolderSeed) + 6 Branchen-Presets als TS-Konstanten (JSON-äquivalent, ohne Datei-IO direkt tree-shakebar): halle (Veranstaltungshalle, 100cm-Raster, Wände/Bestuhlung/Bühne/Technik, event-tools), openair (Zonen/Bestuhlung/Absperrung/Wege, event-tools), saal (Bestuhlung/Bühne/Catering/Deko, 50cm), messe (Standlayout/Theken/Medien/Strom, 10mm), büro (Wände/Möbel/Netzwerk/Beschriftung, 10mm), garten (Bepflanzung/Wege/Wasser/Beleuchtung, 100cm) — je 4 Layer mit eindeutigen tpl-*-IDs, enabledPlugins inkl. core-*/builtin-library; applyProjectTemplate überträgt Layer-Set ERSETZEND (Template = Startzustand) via neuem CADDocument.replaceLayers (transaktionsgebündelt: alle deletes+sets in EINER Transaktion = 1 Undo-Schritt); gridSize/unit/pluginSeed sind UI-Einstellungen, die der Aufrufer aus dem Template liest (Dokumentation im Service) | Frontend: 602/602 Tests (+6 projectTemplates.test.ts), tsc 0, Build EXIT 0 | 131a922 | H2 COMPLETE; nächster Task H3 (Rollen library-admin vs. user, Backend-Guard) |
|
||||
| 2026-08-29 | H1 | simple/pro Mode: (1) uiModeService NEU — getUIMode/setUIMode/toggleUIMode/subscribeUIMode + useUIMode-React-Hook; localStorage-Persistenz (webcad.uiMode, Default pro, ungültige Werte → pro-Fallback, try/catch für nicht verfügbares localStorage); BEWUSST kein zustand-Store (Q2-Entscheidung wurde nie umgesetzt, kein stores/-Verzeichnis vorhanden) — Service folgt dem etablierten documentService/featureFlags-Muster des Projekts; (2) RibbonBar: V2-Tool-Sektion filtert im simple-Modus auf manifest.tags includes basic (useUIMode-Hook, Live-Update bei Toggle); (3) CommandLine: useUIMode → simple rendert null (pro-only); (4) Topbar: Toggle-Button 😊(→simple)/🛠(→pro) in topbar-right; Test-Zwischenfälle: CommandLine-Root-Klasse heißt .cmdline nicht .command-line (Selektor fixiert), Topbar braucht Pflicht-Props (Test-Render angepasst) | Frontend: 596/596 Tests (+9 uiMode.test.tsx), tsc 0, Build EXIT 0 | 8774fea | H1 COMPLETE; nächster Task H2 (Projekt-Templates) |
|
||||
@@ -740,5 +741,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
|
||||
|
||||
---
|
||||
*Nächster offener Schritt: **H4** (i18n-Grundlage: strings.ts zentrale Maps de/en, UI-Texte ersetzen — mechanisch, teilautomatisierbar). Danach: H5 Flächenberechnung. Restlich: D1–D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G komplett, H1+H2+H3 fertig (simple/pro Mode, Projekt-Templates, Rollen-Guard), BUG-1-Kollisionsklasse geschlossen. FE 602/602 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main.*
|
||||
*Nächster offener Schritt: **H5** (Flächenberechnung Polygon m² im PropertiesPanel + measure-area Tool). Danach: Rest-D-Tools (D1–D4/D9/D10-path/D11/D12), Phase I (Docs/CI/Release).*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G komplett, H1–H4 fertig (Modi, Templates, Rollen, i18n-Grundlage), BUG-1-Kollisionsklasse geschlossen. FE 610/610 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main.*
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { LayoutDefinition } from '../types/cad.types';
|
||||
import { getActiveDocument, onActiveDocumentChanged } from '../kernel/document/documentService';
|
||||
import { insertTitleBlockForLayout } from '../services/titleBlockService';
|
||||
import { printLayout } from '../services/printService';
|
||||
import { useT } from '../i18n/strings';
|
||||
|
||||
export interface LayoutView {
|
||||
center: { x: number; y: number };
|
||||
@@ -80,6 +81,8 @@ const WIDGET_W = 100;
|
||||
const WIDGET_H = 80;
|
||||
|
||||
const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp }) => {
|
||||
const t = useT();
|
||||
|
||||
const [doc, setDoc] = useState(docProp ?? getActiveDocument());
|
||||
const [layouts, setLayouts] = useState<LayoutDefinition[]>([]);
|
||||
|
||||
@@ -134,23 +137,23 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
|
||||
return (
|
||||
<div className="layout-bar" data-testid="layout-bar">
|
||||
<div className="layout-bar-header">
|
||||
<span className="layout-bar-title">Layouts</span>
|
||||
<span className="layout-bar-title">{t('layouts.title')}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="layout-bar-add"
|
||||
title="Layout hinzufügen"
|
||||
title={t('layouts.add')}
|
||||
onClick={handleAdd}
|
||||
disabled={!bridge || !doc}
|
||||
>+</button>
|
||||
<button
|
||||
type="button"
|
||||
className="layout-bar-print screen-only"
|
||||
title="Layout drucken (A4 landscape)"
|
||||
title={t('layouts.print')}
|
||||
onClick={handlePrint}
|
||||
>🖨</button>
|
||||
</div>
|
||||
{layouts.length === 0 ? (
|
||||
<div className="layout-bar-empty">Keine Layouts</div>
|
||||
<div className="layout-bar-empty">{t('layouts.empty')}</div>
|
||||
) : (
|
||||
<div className="layout-bar-list">
|
||||
{layouts.map((l) => {
|
||||
@@ -161,7 +164,7 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
|
||||
type="button"
|
||||
className="layout-bar-entry"
|
||||
data-testid={`layout-entry-${l.id}`}
|
||||
title={`Zoom zu ${l.name}`}
|
||||
title={t('layouts.zoomto', l.name)}
|
||||
onClick={() => handleZoomTo(l)}
|
||||
>
|
||||
<svg
|
||||
@@ -187,7 +190,7 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
|
||||
<button
|
||||
type="button"
|
||||
className="layout-bar-tb"
|
||||
title={`Titelblock für ${l.name} einfügen/aktualisieren`}
|
||||
title={t('layouts.titleblock', l.name)}
|
||||
onClick={() => {
|
||||
if (!doc) return;
|
||||
insertTitleBlockForLayout(doc as never, l.id, {
|
||||
@@ -202,7 +205,7 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
|
||||
<button
|
||||
type="button"
|
||||
className="layout-bar-del"
|
||||
title={`Layout ${l.name} löschen`}
|
||||
title={t('layouts.delete', l.name)}
|
||||
onClick={() => handleDelete(l.id)}
|
||||
>🗑</button>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,15 @@ import React from 'react';
|
||||
import type { TopbarProps } from '../types/ui.types';
|
||||
import type { UnitType } from '../utils/format';
|
||||
import { useUIMode, toggleUIMode, getUIMode } from '../services/uiModeService';
|
||||
import { useT } from '../i18n/strings';
|
||||
|
||||
const Topbar: React.FC<TopbarProps> = ({
|
||||
projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme,
|
||||
onOpenSettings, onOpenLeftDrawer, onOpenRightDrawer,
|
||||
unit = 'mm', onUnitChange, onNavigateBack,
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const uiMode = useUIMode();
|
||||
|
||||
return (
|
||||
@@ -38,8 +41,8 @@ const Topbar: React.FC<TopbarProps> = ({
|
||||
<div className="topbar-right">
|
||||
<button
|
||||
className="icon-btn-top"
|
||||
title={uiMode === 'pro' ? 'Zum einfachen Modus wechseln' : 'Zum Profi-Modus wechseln'}
|
||||
aria-label={uiMode === 'pro' ? 'Zum einfachen Modus wechseln' : 'Zum Profi-Modus wechseln'}
|
||||
title={uiMode === 'pro' ? t('mode.toSimple') : t('mode.toPro')}
|
||||
aria-label={uiMode === 'pro' ? t('mode.toSimple') : t('mode.toPro')}
|
||||
onClick={toggleUIMode}
|
||||
>
|
||||
{uiMode === 'pro' ? '😊' : '🛠'}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Task H4 – i18n-Grundlage: zentrale Strings-Maps de/en.
|
||||
*
|
||||
* t(key) liefert den Text der aktuellen Sprache; unbekannte Keys
|
||||
* fallen auf den Key zurück (im Code sichtbar, lint-bar über Tests).
|
||||
* Sprache persistiert in localStorage ('webcad.language', Default de).
|
||||
* Komponenten nutzen useT() für Live-Updates bei Sprachwechsel.
|
||||
*
|
||||
* Vollständige mechanische Ersetzung ALLER Komponenten-Texte ist
|
||||
* teilautomatisierbares Follow-up; hier liegt die Infrastruktur und
|
||||
* die Kern-UI-Texte (LayoutBar, Modus).
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const LANGUAGES = ['de', 'en'] as const;
|
||||
export type Language = (typeof LANGUAGES)[number];
|
||||
|
||||
type StringEntry = string | ((arg?: string) => string);
|
||||
|
||||
const STORAGE_KEY = 'webcad.language';
|
||||
|
||||
/** Zentrale Text-Maps (Key-Pfad → Text oder parametrisierte Funktion). */
|
||||
const DE: Record<string, StringEntry> = {
|
||||
'layouts.title': 'Layouts',
|
||||
'layouts.add': 'Layout hinzufügen',
|
||||
'layouts.empty': 'Keine Layouts',
|
||||
'layouts.delete': (n?: string) => 'Layout ' + (n ?? '') + ' löschen',
|
||||
'layouts.print': 'Layout drucken (A4 landscape)',
|
||||
'layouts.titleblock': (n?: string) => 'Titelblock für ' + (n ?? '') + ' einfügen/aktualisieren',
|
||||
'layouts.zoomto': (n?: string) => 'Zoom zu ' + (n ?? ''),
|
||||
'mode.toSimple': 'Zum einfachen Modus wechseln',
|
||||
'mode.toPro': 'Zum Profi-Modus wechseln',
|
||||
};
|
||||
|
||||
const EN: Record<string, StringEntry> = {
|
||||
'layouts.title': 'Layouts',
|
||||
'layouts.add': 'Add layout',
|
||||
'layouts.empty': 'No layouts',
|
||||
'layouts.delete': (n?: string) => 'Delete layout ' + (n ?? ''),
|
||||
'layouts.print': 'Print layout (A4 landscape)',
|
||||
'layouts.titleblock': (n?: string) => 'Insert/update title block for ' + (n ?? ''),
|
||||
'layouts.zoomto': (n?: string) => 'Zoom to ' + (n ?? ''),
|
||||
'mode.toSimple': 'Switch to simple mode',
|
||||
'mode.toPro': 'Switch to pro mode',
|
||||
};
|
||||
|
||||
function mapFor(lang: Language): Record<string, StringEntry> {
|
||||
return lang === 'en' ? EN : DE;
|
||||
}
|
||||
|
||||
function readStored(): Language {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw === 'de' || raw === 'en') return raw;
|
||||
} catch {
|
||||
// localStorage nicht verfügbar
|
||||
}
|
||||
return 'de';
|
||||
}
|
||||
|
||||
let current: Language = readStored();
|
||||
const listeners = new Set<(lang: Language) => void>();
|
||||
|
||||
/** Aktuelle Sprache. */
|
||||
export function getLanguage(): Language {
|
||||
return current;
|
||||
}
|
||||
|
||||
/** Sprache wechseln und persistieren. */
|
||||
export function setLanguage(lang: Language): void {
|
||||
current = lang;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, lang);
|
||||
} catch {
|
||||
// Persistenz optional
|
||||
}
|
||||
for (const cb of listeners) cb(lang);
|
||||
}
|
||||
|
||||
/** Subscribe auf Sprachwechsel; gibt Unsubscribe zurück. */
|
||||
export function subscribeLanguage(cb: (lang: Language) => void): () => void {
|
||||
listeners.add(cb);
|
||||
return () => { listeners.delete(cb); };
|
||||
}
|
||||
|
||||
/**
|
||||
* Übersetzung: t('key') bzw. t('key', arg) für parametrisierte Texte.
|
||||
* Unbekannter Key → Key als Fallback (offensichtlich sichtbar).
|
||||
*/
|
||||
export function t(key: string, arg?: string): string {
|
||||
const entry = mapFor(current)[key];
|
||||
if (typeof entry === 'function') return entry(arg);
|
||||
if (typeof entry === 'string') return entry;
|
||||
return key;
|
||||
}
|
||||
|
||||
/** React-Hook: erzwingt Re-Render bei Sprachwechsel. */
|
||||
export function useT(): typeof t {
|
||||
const [, force] = useState(0);
|
||||
useEffect(() => {
|
||||
const off = subscribeLanguage(() => force((n) => n + 1));
|
||||
return off;
|
||||
}, []);
|
||||
return t;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Task H4 – i18n-Grundlage: zentrale strings.ts Maps de/en.
|
||||
*
|
||||
* strings-Modul: t(key) mit de/en-Maps, getLanguage/setLanguage
|
||||
* (localStorage-persistiert, Default de), subscribe + useT-Hook.
|
||||
* Kern-UI-Texte (LayoutBar, Modus-Umschaltung) laufen ueber die
|
||||
* Maps; vollstaendige mechanische Ersetzung aller Komponenten ist
|
||||
* teilautomatisierbares Follow-up (Roadmap-Vermerk).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup, fireEvent, act } from '@testing-library/react';
|
||||
import {
|
||||
t, getLanguage, setLanguage, subscribeLanguage, useT, LANGUAGES,
|
||||
} from '../src/i18n/strings';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// ––– strings-Modul ––––––––––––––––––––––––
|
||||
|
||||
describe('H4: i18n strings', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
setLanguage('de');
|
||||
});
|
||||
|
||||
it('Default-Sprache ist de', () => {
|
||||
expect(getLanguage()).toBe('de');
|
||||
});
|
||||
|
||||
it('t liefert deutsche Kern-UI-Texte', () => {
|
||||
expect(t('layouts.title')).toBe('Layouts');
|
||||
expect(t('layouts.add')).toBe('Layout hinzufügen');
|
||||
expect(t('layouts.empty')).toBe('Keine Layouts');
|
||||
});
|
||||
|
||||
it('setLanguage(en) wechselt auf englische Texte', () => {
|
||||
setLanguage('en');
|
||||
expect(t('layouts.title')).toBe('Layouts');
|
||||
expect(t('layouts.add')).toBe('Add layout');
|
||||
expect(t('layouts.empty')).toBe('No layouts');
|
||||
});
|
||||
|
||||
it('Sprache persistiert in localStorage und ueberlebt Reload-Semantik', () => {
|
||||
setLanguage('en');
|
||||
expect(localStorage.getItem('webcad.language')).toBe('en');
|
||||
// erneutes Lesen startet aus persistiertem Zustand
|
||||
expect(getLanguage()).toBe('en');
|
||||
});
|
||||
|
||||
it('LANGUAGES enthaelt genau de und en', () => {
|
||||
expect(LANGUAGES).toEqual(['de', 'en']);
|
||||
});
|
||||
|
||||
it('t mit unbekanntem Key liefert den Key zurueck (Fallback, lint-bar)', () => {
|
||||
setLanguage('de');
|
||||
expect(t('gibts.nicht')).toBe('gibts.nicht');
|
||||
});
|
||||
|
||||
it('subscribeLanguage feuert bei Wechsel und unsubscribed', () => {
|
||||
const cb = vi.fn();
|
||||
const off = subscribeLanguage(cb);
|
||||
setLanguage('en');
|
||||
expect(cb).toHaveBeenCalledWith('en');
|
||||
off();
|
||||
cb.mockClear();
|
||||
setLanguage('de');
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ––– useT-Hook + Komponente ––––––––––––––––––
|
||||
|
||||
describe('H4: useT-Hook rendert LayoutBar zweisprachig', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
setLanguage('de');
|
||||
});
|
||||
|
||||
it('LayoutBar zeigt deutsche Texte und wechselt live auf englisch', async () => {
|
||||
const LayoutBar = (await import('../src/components/LayoutBar')).default;
|
||||
render(<LayoutBar doc={null} bridge={null} />);
|
||||
expect(screen.getByText('Layouts')).toBeTruthy();
|
||||
expect(screen.getByText('Keine Layouts')).toBeTruthy();
|
||||
expect(screen.getByTitle('Layout hinzufügen')).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
setLanguage('en');
|
||||
});
|
||||
// Live-Update via subscribe (nach act flush)
|
||||
expect(screen.getByText('No layouts')).toBeTruthy();
|
||||
expect(screen.getByTitle('Add layout')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user