Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c873a4069 | ||
|
|
09bb98235b |
@@ -17,6 +17,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||
|
||||
### Sicherheit
|
||||
|
||||
## [0.20.2] – 2026-10-03
|
||||
|
||||
### Behoben
|
||||
- **Das Seitenmenü ließ sich auf dem Handy nicht schließen.** Es gab weder eine Fläche zum Antippen noch ein Schließen beim Navigieren – das Markup stammt von AdminLTE, dessen JavaScript wir bewusst nicht einbinden (#034)
|
||||
- Die Dashboard-Tabelle war auf dem Telefon rechts abgeschnitten; sie zeigt dort jetzt Host und Ablauf, den Kunden klein darunter (#034)
|
||||
|
||||
### Hinzugefügt
|
||||
- Schließen-Knopf im Menükopf, Schließen per Escape, und der Inhalt hinter dem offenen Menü scrollt nicht mehr mit (#034)
|
||||
|
||||
## [0.20.1] – 2026-10-03
|
||||
|
||||
### Behoben
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Zeitübersicht
|
||||
|
||||
**Gesamt:** 16 h 58 min
|
||||
**Gesamt:** 17 h 38 min
|
||||
|
||||
| Issue | Zeit |
|
||||
|---|---|
|
||||
@@ -39,6 +39,7 @@
|
||||
| #031 Update für beide Varianten | 0 h 45 min |
|
||||
| #032 LXC-Installation korrigiert | 0 h 20 min |
|
||||
| #033 Weiterleitung auf Lausch-Adresse | 0 h 30 min |
|
||||
| #034 Bedienung auf dem Handy | 0 h 40 min |
|
||||
|
||||
| Meilenstein | Zeit |
|
||||
|---|---|
|
||||
@@ -61,7 +62,7 @@
|
||||
|
||||
## 2026-09-17 · Sitzung 1 · 19:36–laufend
|
||||
**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0)
|
||||
**Version:** 0.20.1
|
||||
**Version:** 0.20.2
|
||||
|
||||
### Erledigt
|
||||
- Auftrag gelesen, Rückfragen gestellt
|
||||
@@ -179,6 +180,9 @@
|
||||
- #033 war hausgemacht: Mein `HOSTNAME=0.0.0.0` aus #031 hat den Healthcheck repariert und dafür alle Weiterleitungen aus Server Actions auf `0.0.0.0` gelenkt. Vorher stand dort der Containername – derselbe Fehler, nur unauffälliger
|
||||
- Lehre: Weiterleitungen gehören aus der konfigurierten Adresse gebaut, nie aus dem, was der Server über sich selbst weiß
|
||||
|
||||
- Mobile Bedienung (#034): Das Menü ließ sich auf dem Telefon nicht schließen – AdminLTE-Markup ohne AdminLTE-JavaScript. Eigener Zustand, abdunkelnde Fläche, Schließen beim Navigieren und mit Escape
|
||||
- Lehre: Die Mobilansicht war mit Screenshots geprüft, nicht mit den Fingern. Ein Bild zeigt nicht, dass etwas nicht wieder weggeht
|
||||
|
||||
### Nächster Schritt
|
||||
- #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline
|
||||
- Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023)
|
||||
|
||||
@@ -473,6 +473,11 @@ Server-ID stehen in der Detailansicht unter *Subscription-Historie*.
|
||||
|
||||
## Anhang: dunkles Design und Mobilansicht
|
||||
|
||||
Auf dem Telefon öffnet das Symbol links oben das Menü; es schließt sich durch Antippen der
|
||||
abgedunkelten Fläche, über das Kreuz im Menükopf, mit Escape – und von selbst, sobald ein Eintrag
|
||||
gewählt wurde. Die Tabellen zeigen auf schmalen Bildschirmen nur die wichtigsten Spalten; die
|
||||
übrigen Angaben stehen auf der jeweiligen Detailseite.
|
||||
|
||||
Das Dashboard folgt der Einstellung des Betriebssystems und lässt sich über das Mondsymbol in der
|
||||
Kopfleiste umschalten.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 252 KiB |
@@ -0,0 +1,28 @@
|
||||
# #034 Bedienung auf dem Handy
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.20.2
|
||||
**Labels:** ui, bug
|
||||
**Geschätzt:** 2 h **Tatsächlich:** 0 h 40 min
|
||||
|
||||
## Auslöser
|
||||
Florian im produktiven Einsatz auf dem Telefon: „Das Menü geht nicht weg.“ Dazu eine Tabelle, von der
|
||||
rechts die Hälfte abgeschnitten war.
|
||||
|
||||
## Ursache
|
||||
Das Markup stammt von AdminLTE, dessen JavaScript binden wir bewusst nicht ein. Die Klasse
|
||||
`sidebar-open` ließ sich damit setzen – aber nichts nahm sie wieder weg: keine Fläche zum Antippen,
|
||||
kein Schließen beim Navigieren, kein Escape. Auf dem Telefon blieb das Menü über dem Inhalt stehen.
|
||||
|
||||
## Behebung
|
||||
- Eigener, winziger Zustand (`menuZustand.ts`), auf den Kopfzeile und Menü gemeinsam hören
|
||||
- Abdunkelnde Fläche hinter dem Menü – antippen schließt (ein `button`, damit auch per Tastatur erreichbar)
|
||||
- Schließen-Knopf im Menükopf, nur auf schmalen Bildschirmen
|
||||
- Schließt beim Navigieren und mit Escape
|
||||
- Der Inhalt dahinter scrollt nicht mehr mit, solange das Menü offen ist
|
||||
- Dashboard-Tabelle: auf dem Telefon nur Host und Ablauf, der Kunde steht klein unter dem Hostnamen.
|
||||
Key und Rechnungsstände erscheinen ab Tablet-Breite – die Host-Tabelle machte das schon vorher so.
|
||||
- Fünf Tests für die Zustandslogik
|
||||
|
||||
## Nicht geändert
|
||||
Die abdunkelnde Fläche gilt nur unter 992 px. Auf breiten Bildschirmen bleibt es beim bisherigen
|
||||
Verhalten: Das Menü klappt zu Symbolen zusammen, statt sich über den Inhalt zu legen.
|
||||
@@ -37,4 +37,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
||||
| [#031](031-update-skript-beide-varianten.md) | Update- und Sicherungsskript für beide Varianten | erledigt | v0.20.0 | 2 h | 0 h 45 min |
|
||||
| [#032](032-lxc-installation-korrekturen.md) | LXC-Installation: Herkunft der Skripte und Speicher | erledigt | v0.20.1 | 1 h | 0 h 20 min |
|
||||
| [#033](033-weiterleitung-lauschadresse.md) | Abmelden führte auf die Lausch-Adresse | erledigt | v0.20.1 | 1 h | 0 h 30 min |
|
||||
| | **Summe** | | | **151 h** | **16 h 58 min** |
|
||||
| [#034](034-mobile-bedienung.md) | Bedienung auf dem Handy | erledigt | v0.20.2 | 2 h | 0 h 40 min |
|
||||
| | **Summe** | | | **153 h** | **17 h 38 min** |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proxledger",
|
||||
"version": "0.20.1",
|
||||
"version": "0.20.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -121,12 +121,22 @@ export default async function DashboardSeite() {
|
||||
<table className="table table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
{/* Auf dem Handy bleiben Host und Ablauf; der Rest käme ohnehin nur
|
||||
per Seitwärtsscrollen ins Bild (#034). */}
|
||||
<th scope="col">Host</th>
|
||||
<th scope="col">Kunde</th>
|
||||
<th scope="col">Key</th>
|
||||
<th scope="col" className="d-none d-md-table-cell">
|
||||
Kunde
|
||||
</th>
|
||||
<th scope="col" className="d-none d-lg-table-cell">
|
||||
Key
|
||||
</th>
|
||||
<th scope="col">Ablauf</th>
|
||||
<th scope="col">Proxmox</th>
|
||||
<th scope="col">Kundenrechnung</th>
|
||||
<th scope="col" className="d-none d-sm-table-cell">
|
||||
Proxmox
|
||||
</th>
|
||||
<th scope="col" className="d-none d-sm-table-cell">
|
||||
Kundenrechnung
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -137,8 +147,12 @@ export default async function DashboardSeite() {
|
||||
{host.hostname}
|
||||
</Link>
|
||||
{host.meldetNicht ? <KeineMeldungBadge /> : null}
|
||||
{/* Auf schmalen Bildschirmen steht der Kunde hier mit darunter. */}
|
||||
<div className="small text-body-secondary d-md-none">
|
||||
{host.kunde ? host.kunde.name : "Nicht zugeordnet"}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<td className="d-none d-md-table-cell">
|
||||
{host.kunde ? (
|
||||
<Link href={`/kunden/${host.kunde.id}`} className="text-decoration-none">
|
||||
{host.kunde.name}
|
||||
@@ -152,16 +166,16 @@ export default async function DashboardSeite() {
|
||||
</Link>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<td className="d-none d-lg-table-cell">
|
||||
<CopyableCode wert={host.subKey} />
|
||||
</td>
|
||||
<td>
|
||||
<AblaufBadge datum={host.nextduedate?.toISOString() ?? null} heute={heute} />
|
||||
</td>
|
||||
<td>
|
||||
<td className="d-none d-sm-table-cell">
|
||||
<EinkaufBadge bezahlt={host.einkaufBezahlt} />
|
||||
</td>
|
||||
<td>
|
||||
<td className="d-none d-sm-table-cell">
|
||||
<VerkaufBadge erstellt={host.verkaufErstellt} bezahlt={host.verkaufBezahlt} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -186,6 +186,19 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fläche hinter dem geöffneten Menü auf schmalen Bildschirmen. Ein Knopf statt eines div,
|
||||
* damit sie auch per Tastatur erreichbar ist.
|
||||
*/
|
||||
.pl-menu-hintergrund {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1037; /* unter dem Menü (1038), über allem anderen */
|
||||
padding: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* ---------- Inhaltsbereich ---------- */
|
||||
|
||||
.app-content-header {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { SearchDialog, type SuchEintrag } from "./SearchDialog";
|
||||
import type { Rolle } from "@/generated/prisma/enums";
|
||||
import { ROLLEN_TEXT } from "@/server/auth/rollen";
|
||||
import { menuUmschalten } from "./menuZustand";
|
||||
|
||||
export interface KopfleistenAlert {
|
||||
id: string;
|
||||
@@ -58,16 +59,9 @@ export function Header({
|
||||
return () => window.removeEventListener("keydown", tasten);
|
||||
}, []);
|
||||
|
||||
/** Sidebar umschalten: auf schmalen Bildschirmen als Overlay, sonst eingeklappt. */
|
||||
/** Seitenmenü umschalten – der Zustand liegt in menuZustand.ts, damit das Menü selbst ihn kennt. */
|
||||
const sidebarUmschalten = useCallback(() => {
|
||||
const body = document.body;
|
||||
if (window.innerWidth < 992) {
|
||||
body.classList.toggle("sidebar-open");
|
||||
body.classList.remove("sidebar-collapse");
|
||||
} else {
|
||||
body.classList.toggle("sidebar-collapse");
|
||||
body.classList.remove("sidebar-open");
|
||||
}
|
||||
menuUmschalten();
|
||||
}, []);
|
||||
|
||||
function themaUmschalten() {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LogoMitSchriftzug } from "@/components/Logo";
|
||||
import type { Rolle } from "@/generated/prisma/enums";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { menuSchliessen, useMenuOffen } from "./menuZustand";
|
||||
|
||||
interface MenuEintrag {
|
||||
href: string;
|
||||
@@ -30,6 +31,21 @@ export function Sidebar({
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [suche, setSuche] = useState("");
|
||||
const menuOffen = useMenuOffen();
|
||||
|
||||
// Nach dem Navigieren hat das Menü seinen Zweck erfüllt – auf dem Handy verdeckt es sonst alles.
|
||||
useEffect(() => {
|
||||
menuSchliessen();
|
||||
}, [pathname]);
|
||||
|
||||
// Escape schließt, wie bei jedem anderen Overlay auch.
|
||||
useEffect(() => {
|
||||
function taste(ereignis: KeyboardEvent) {
|
||||
if (ereignis.key === "Escape") menuSchliessen();
|
||||
}
|
||||
window.addEventListener("keydown", taste);
|
||||
return () => window.removeEventListener("keydown", taste);
|
||||
}, []);
|
||||
|
||||
const haupt: MenuEintrag[] = [
|
||||
{ href: "/", titel: "Dashboard", icon: "bi-speedometer2" },
|
||||
@@ -105,11 +121,30 @@ export function Sidebar({
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="app-sidebar shadow" data-bs-theme="dark">
|
||||
<div className="sidebar-brand">
|
||||
<>
|
||||
{/* Fläche hinter dem Menü: antippen schließt. Ohne sie bleibt das Menü auf dem Handy stehen. */}
|
||||
{menuOffen ? (
|
||||
<button
|
||||
type="button"
|
||||
className="pl-menu-hintergrund d-lg-none"
|
||||
aria-label="Menü schließen"
|
||||
onClick={menuSchliessen}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<aside className="app-sidebar shadow" data-bs-theme="dark">
|
||||
<div className="sidebar-brand d-flex align-items-center justify-content-between">
|
||||
<Link href="/" className="brand-link">
|
||||
<LogoMitSchriftzug variante="sidebar" groesse={34} />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link text-white d-lg-none pl-touch-target me-2"
|
||||
aria-label="Menü schließen"
|
||||
onClick={menuSchliessen}
|
||||
>
|
||||
<i className="bi bi-x-lg" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="pl-sidebar-user">
|
||||
@@ -147,6 +182,7 @@ export function Sidebar({
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
/**
|
||||
* Zustand des Seitenmenüs auf schmalen Bildschirmen (#034).
|
||||
*
|
||||
* AdminLTE bringt dafür eigenes JavaScript mit, das wir bewusst nicht einbinden – die Klasse
|
||||
* `sidebar-open` am `body` allein lässt sich aber nicht wieder loswerden: Kein Hintergrund zum
|
||||
* Antippen, kein Schließen beim Navigieren. Deshalb ein eigener, winziger Zustand, auf den
|
||||
* Kopfzeile und Menü gemeinsam hören.
|
||||
*/
|
||||
|
||||
const SCHMAL = 992;
|
||||
|
||||
let offen = false;
|
||||
const zuhoerer = new Set<() => void>();
|
||||
|
||||
function melden() {
|
||||
for (const ruf of zuhoerer) ruf();
|
||||
}
|
||||
|
||||
/** Setzt die Klassen, die das AdminLTE-Stylesheet auswertet. */
|
||||
function klassenSetzen() {
|
||||
const body = document.body;
|
||||
body.classList.toggle("sidebar-open", offen);
|
||||
// Auf schmalen Bildschirmen liegt das Menü über dem Inhalt; dahinter soll nichts mitscrollen.
|
||||
body.style.overflow = offen && window.innerWidth < SCHMAL ? "hidden" : "";
|
||||
}
|
||||
|
||||
export function menuUmschalten() {
|
||||
if (window.innerWidth >= SCHMAL) {
|
||||
// Breiter Bildschirm: Das Menü bleibt stehen und wird nur schmaler.
|
||||
document.body.classList.toggle("sidebar-collapse");
|
||||
document.body.classList.remove("sidebar-open");
|
||||
return;
|
||||
}
|
||||
|
||||
offen = !offen;
|
||||
document.body.classList.remove("sidebar-collapse");
|
||||
klassenSetzen();
|
||||
melden();
|
||||
}
|
||||
|
||||
export function menuSchliessen() {
|
||||
if (!offen) return;
|
||||
offen = false;
|
||||
klassenSetzen();
|
||||
melden();
|
||||
}
|
||||
|
||||
function abonnieren(ruf: () => void) {
|
||||
zuhoerer.add(ruf);
|
||||
return () => {
|
||||
zuhoerer.delete(ruf);
|
||||
};
|
||||
}
|
||||
|
||||
export function useMenuOffen(): boolean {
|
||||
// Auf dem Server ist das Menü nie offen – sonst käme es beim ersten Rendern zu einem Unterschied.
|
||||
return useSyncExternalStore(
|
||||
abonnieren,
|
||||
() => offen,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* Prüft das Öffnen und Schließen des Seitenmenüs. Statt eines echten Browsers genügen hier
|
||||
* Attrappen für `document.body` und `window` – geprüft wird, welche Klassen gesetzt werden.
|
||||
*/
|
||||
|
||||
const klassen = new Set<string>();
|
||||
let breite = 390;
|
||||
|
||||
function aufraeumen() {
|
||||
klassen.clear();
|
||||
breite = 390;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
aufraeumen();
|
||||
vi.resetModules();
|
||||
|
||||
const body = {
|
||||
classList: {
|
||||
toggle(name: string, an?: boolean) {
|
||||
const soll = an ?? !klassen.has(name);
|
||||
if (soll) klassen.add(name);
|
||||
else klassen.delete(name);
|
||||
},
|
||||
remove(name: string) {
|
||||
klassen.delete(name);
|
||||
},
|
||||
contains: (name: string) => klassen.has(name),
|
||||
},
|
||||
style: { overflow: "" },
|
||||
};
|
||||
|
||||
vi.stubGlobal("document", { body });
|
||||
vi.stubGlobal("window", {
|
||||
get innerWidth() {
|
||||
return breite;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("Seitenmenü auf schmalen Bildschirmen", () => {
|
||||
it("öffnet und schließt wieder", async () => {
|
||||
const { menuUmschalten, menuSchliessen } = await import("@/components/layout/menuZustand");
|
||||
|
||||
menuUmschalten();
|
||||
expect(klassen.has("sidebar-open")).toBe(true);
|
||||
|
||||
menuSchliessen();
|
||||
expect(klassen.has("sidebar-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("sperrt den Inhalt hinter dem offenen Menü gegen Scrollen", async () => {
|
||||
const { menuUmschalten, menuSchliessen } = await import("@/components/layout/menuZustand");
|
||||
|
||||
menuUmschalten();
|
||||
expect((document.body as unknown as { style: { overflow: string } }).style.overflow).toBe("hidden");
|
||||
|
||||
menuSchliessen();
|
||||
expect((document.body as unknown as { style: { overflow: string } }).style.overflow).toBe("");
|
||||
});
|
||||
|
||||
it("schließt auch beim zweiten Antippen des Knopfes", async () => {
|
||||
const { menuUmschalten } = await import("@/components/layout/menuZustand");
|
||||
|
||||
menuUmschalten();
|
||||
menuUmschalten();
|
||||
expect(klassen.has("sidebar-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("macht ein zweites Schließen zum Nichts", async () => {
|
||||
const { menuSchliessen } = await import("@/components/layout/menuZustand");
|
||||
|
||||
menuSchliessen();
|
||||
expect(klassen.has("sidebar-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("klappt auf breiten Bildschirmen nur ein, statt zu überlagern", async () => {
|
||||
breite = 1400;
|
||||
const { menuUmschalten } = await import("@/components/layout/menuZustand");
|
||||
|
||||
menuUmschalten();
|
||||
expect(klassen.has("sidebar-collapse")).toBe(true);
|
||||
expect(klassen.has("sidebar-open")).toBe(false);
|
||||
});
|
||||
});
|
||||