diff --git a/05_react/unterricht/tag51/01_react-app-with-tests/src/components/lists/ListUser.jsx b/05_react/unterricht/tag51/01_react-app-with-tests/src/components/lists/ListUser.jsx
index 66edae2..442d44f 100644
--- a/05_react/unterricht/tag51/01_react-app-with-tests/src/components/lists/ListUser.jsx
+++ b/05_react/unterricht/tag51/01_react-app-with-tests/src/components/lists/ListUser.jsx
@@ -5,7 +5,7 @@ const ListUser = (props) => {
// const { } = props;
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
- // const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // 2019
@@ -18,7 +18,8 @@ const ListUser = (props) => {
})
.then(({ data }) => {
console.log(data);
- setUsers(data?.users);
+ setUsers(data?.users ?? []);
+ setError(null);
})
.catch((err) => {
//
@@ -27,6 +28,7 @@ const ListUser = (props) => {
console.error('Request abgebrochen:', err.message);
} else {
console.error('Error:', err);
+ setError(err);
}
})
.finally(() => {
@@ -47,6 +49,15 @@ const ListUser = (props) => {
return
is loading users...
;
}
+ if (error) {
+ const status = error.response?.status;
+ return (
+
+ {status ? `Failed to load users (HTTP ${status})` : 'Failed to load users'}
+
+ );
+ }
+
return (
{users.length > 0 &&
diff --git a/05_react/unterricht/tag51/01_react-app-with-tests/src/test/ListUser.jsx b/05_react/unterricht/tag51/01_react-app-with-tests/src/test/ListUser.jsx
deleted file mode 100644
index 48917d6..0000000
--- a/05_react/unterricht/tag51/01_react-app-with-tests/src/test/ListUser.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import { describe, it, afterEach } from 'vitest';
-
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import ListUser from '../components/lists/ListUser';
-
-// Create a new instance of the MockAdapter
-const mock = new MockAdapter(axios);
-
-// Clean up after each test case to prevent interference between tests
-afterEach(() => {
- mock.reset();
-});
-
-describe('ListUser', () => {
- it('should load and display user data', async () => {
- render();
- const mockData = { users: [{ id: 1, firstName: 'John' }] };
- // Configure the mock: When this specific URL is requested, return 200 and our mockData
- // This intercepts the network call, so no real internet request happens
- mock.onGet('https://dummyjson.com/users?limit=3').reply(200, mockData);
-
- const userElement = await screen.findByText(/John/i);
-
- expect(userElement).toBeInTheDocument();
- });
-});
diff --git a/05_react/unterricht/tag51/01_react-app-with-tests/src/test/ListUser.test.jsx b/05_react/unterricht/tag51/01_react-app-with-tests/src/test/ListUser.test.jsx
new file mode 100644
index 0000000..9b9fd80
--- /dev/null
+++ b/05_react/unterricht/tag51/01_react-app-with-tests/src/test/ListUser.test.jsx
@@ -0,0 +1,91 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, beforeEach, afterEach, vi } from 'vitest';
+
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import ListUser from '../components/lists/ListUser';
+
+const USERS_URL = 'https://dummyjson.com/users?limit=100';
+
+const mockUsersResponse = {
+ users: [
+ {
+ id: 1,
+ firstName: 'John',
+ lastName: 'Doe',
+ age: 28,
+ email: 'john.doe@example.com',
+ },
+ ],
+ total: 1,
+ skip: 0,
+ limit: 100,
+};
+
+const mock = new MockAdapter(axios);
+
+beforeEach(() => {
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ vi.spyOn(console, 'error').mockImplementation(() => {});
+});
+
+afterEach(() => {
+ mock.reset();
+ vi.restoreAllMocks();
+});
+
+describe('ListUser', () => {
+ it('should load and display user data on HTTP 200', async () => {
+ mock.onGet(USERS_URL).reply(200, mockUsersResponse);
+
+ render();
+
+ expect(screen.getByText(/is loading users/i)).toBeInTheDocument();
+
+ expect(await screen.findByText(/John/i)).toBeInTheDocument();
+ expect(screen.getByText(/Doe/i)).toBeInTheDocument();
+ expect(screen.getByText(/28/)).toBeInTheDocument();
+ expect(screen.getByText(/john.doe@example.com/i)).toBeInTheDocument();
+ expect(screen.queryByText(/is loading users/i)).not.toBeInTheDocument();
+ });
+
+ it('should show an empty list on HTTP 200 when no users are returned', async () => {
+ mock.onGet(USERS_URL).reply(200, { users: [], total: 0, skip: 0, limit: 100 });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByText(/is loading users/i)).not.toBeInTheDocument();
+ });
+
+ expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ it.each([
+ [400, 'Bad Request'],
+ [401, 'Unauthorized'],
+ [403, 'Forbidden'],
+ [404, 'Not Found'],
+ [500, 'Internal Server Error'],
+ [503, 'Service Unavailable'],
+ ])('should show an error for HTTP %s (%s)', async (status) => {
+ mock.onGet(USERS_URL).reply(status);
+
+ render();
+
+ expect(await screen.findByRole('alert')).toHaveTextContent(`Failed to load users (HTTP ${status})`);
+ expect(screen.queryByText(/John/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/is loading users/i)).not.toBeInTheDocument();
+ });
+
+ it('should show a generic error on network failure', async () => {
+ mock.onGet(USERS_URL).networkError();
+
+ render();
+
+ expect(await screen.findByRole('alert')).toHaveTextContent('Failed to load users');
+ expect(screen.queryByText(/HTTP/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/John/i)).not.toBeInTheDocument();
+ });
+});
diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/CategorySection.test.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/CategorySection.test.tsx
index e2c55c0..a9fd0d9 100644
--- a/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/CategorySection.test.tsx
+++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/CategorySection.test.tsx
@@ -30,7 +30,7 @@ describe('CategorySection', () => {
const imgs = screen.getAllByRole('img');
const links = screen.getAllByRole('link');
- expect(imgs).toHaveLength(3); //.length()
- expect(links).toHaveLength(3);
+ expect(imgs).toHaveLength(3); //.length() - from chai - TestFramework
+ expect(links).toHaveLength(3); // .lengthOf()
});
});
diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/Footer.test.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/Footer.test.tsx
index 6517c7f..5cc909c 100644
--- a/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/Footer.test.tsx
+++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/Footer.test.tsx
@@ -19,7 +19,7 @@ describe('Footer', () => {
navItems.forEach((item) => {
const spanEl = screen.getByText(item.name);
- const linkEl = spanEl.parentElement;
+ const linkEl = spanEl.parentNode;
expect(spanEl).toBeInTheDocument();
expect(linkEl).toHaveAttribute('href', item.href);
});
diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/ImageGallery.test.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/ImageGallery.test.tsx
index d431cda..06963be 100644
--- a/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/ImageGallery.test.tsx
+++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/tests/ImageGallery.test.tsx
@@ -65,11 +65,11 @@ describe('ImageGallery', () => {
const user = userEvent.setup();
+ expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[0]);
await user.keyboard('{ArrowRight}');
- expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[1]);
});
@@ -79,6 +79,7 @@ describe('ImageGallery', () => {
const user = userEvent.setup();
+ expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[0]);
await user.keyboard('{ArrowRight}');
@@ -86,7 +87,6 @@ describe('ImageGallery', () => {
await user.keyboard('{ArrowLeft}');
- expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[0]);
});
@@ -96,10 +96,11 @@ describe('ImageGallery', () => {
const user = userEvent.setup();
+ expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[0]);
+
await user.keyboard('{ArrowLeft}');
- expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[product.images.length - 1]);
});
});
diff --git a/06_js-debug/unterricht/tag46/01_inversion/03_performance-test-inversion.js b/06_js-debug/unterricht/tag46/01_inversion/03_performance-test-inversion.js
new file mode 100644
index 0000000..1e63f5f
--- /dev/null
+++ b/06_js-debug/unterricht/tag46/01_inversion/03_performance-test-inversion.js
@@ -0,0 +1,57 @@
+'use strict';
+
+const isSystemUpgrading = false;
+const isLoggedIn = true;
+const hasAdminRights = true;
+
+const items = [
+ { name: 'Item A', cost: 150 },
+ { name: 'Item B', cost: 250 },
+ { name: 'Item C', cost: 350 },
+];
+
+function runNested() {
+ let counter = 0;
+ if (!isSystemUpgrading) {
+ if (isLoggedIn) {
+ if (hasAdminRights) {
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].cost > 200) counter++;
+ }
+ }
+ }
+ }
+ return counter;
+}
+
+function runGuard() {
+ let counter = 0;
+ if (isSystemUpgrading) return counter;
+ if (!isLoggedIn) return counter;
+ if (!hasAdminRights) return counter;
+
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].cost > 200) counter++;
+ }
+ return counter;
+}
+
+const ITERATIONS = 10_000_000;
+
+// Warmup
+for (let i = 0; i < 100_000; i++) {
+ runNested();
+ runGuard();
+}
+
+console.time('Verschachtelt');
+for (let i = 0; i < ITERATIONS; i++) runNested();
+console.timeEnd('Verschachtelt');
+
+console.time('Early Returns');
+for (let i = 0; i < ITERATIONS; i++) runGuard();
+console.timeEnd('Early Returns');
+
+// In diesem Test laufen beide Varianten im Chrome V8-Engine praktisch gleich schnell (innerhalb normaler CPU-Schwankungen).
+
+// Fazit: Verwende Early Returns (Guard Clauses). Sie bieten zwar keinen nennenswerten Performance-Vorsprung gegenüber verschachtelten Abfragen, reduzieren aber die zyklomatische Komplexität und machen den Code deutlich lesbarer.
diff --git a/06_js-debug/unterricht/tag46/01_inversion/04_README.md b/06_js-debug/unterricht/tag46/01_inversion/04_README.md
new file mode 100644
index 0000000..943bd0c
--- /dev/null
+++ b/06_js-debug/unterricht/tag46/01_inversion/04_README.md
@@ -0,0 +1,38 @@
+## Kein Performanceunterschied bei Inversion
+
+### 1. AST-Normalisierung & SSA (Control Flow Graphs)
+
+V8 (über den Turbofan-Optimierungs-Compiler) führt deinen Code nicht zeilenweise so aus, wie du ihn schreibst:
+
+* **Control Flow Graph (CFG):** V8 wandelt sowohl verschachtelte Blöcke als auch Early Returns in dieselbe abstrakte Repräsentation um (Static Single Assignment / SSA Form).
+* **Pfad-Reduktion:** Ein verschachtelter Baum `if (A) { if (B) { if (C) { ... } } }` wird im Zwischencode auf denselben Entscheidungspfad reduziert wie `if (!A) return; if (!B) return; if (!C) return;`.
+* **Identischer Maschinencode:** Sobald Turbofan den Code optimiert, erzeugen beide Varianten exakt dieselbe Sequenz aus bedingten Sprungbefehlen (`test`, `jnz` / `jz`) auf Assemblerebene.
+
+---
+
+### 2. Hardware-Ebene: CPU Branch Prediction
+
+Unabhängig von der JS-Engine entscheidet die Hardware über die Geschwindigkeit von Verzweigungen:
+
+* Wenn Bedingungen wie `isLoggedIn` stabil sind (z. B. immer `true`), lernt der **Branch Predictor** der CPU das Muster nach wenigen Durchläufen.
+* Die CPU führt den Zweig spekulativ ohne Pipeline-Stall aus (Branch Prediction Penalty = 0 Takte).
+* Ob der nicht genommene Zweig am Ende der Funktion (`nested`) oder direkt hinter dem Check (`early return`) liegt, macht für die Ausführungszeit der CPU keinen Unterschied.
+
+---
+
+### 3. Warum misst man im Firefox (SpiderMonkey) manchmal Unterschiede?
+
+SpiderMonkey (Firefox) und V8 (Chrome) verfolgen unterschiedliche Strategien bei der Bytecode-Erzeugung und den JIT-Stufen:
+
+| Aspekt | Chrome (V8) | Firefox (SpiderMonkey) |
+| --- | --- | --- |
+| **Pipeline** | Ignition (Interpreter) $\rightarrow$ Sparkplug $\rightarrow$ Maglev $\rightarrow$ Turbofan | C++ Interpreter $\rightarrow$ Baseline Interpreter $\rightarrow$ Baseline Compiler $\rightarrow$ WarpMonkey |
+| **Bytecode-Layout** | V8 optimiert Jump-Targets früh im Bytecode-Generator; Basic Blocks werden linear angeordnet. | SpiderMonkey behält in frühen Phasen oft ein Bytecode-Layout bei, das näher an der Quellcode-Struktur liegt. |
+| **Bailout / OSR** | Sehr aggressives Inlining und Dead-Code-Elimination im `Maglev`/`Turbofan`-Layer. | `WarpMonkey` nutzt Transpilation über CacheIR; je nach Verschachtelungstiefe können Scope- und Frame-Handling im Baseline-Tier minimal variieren. |
+
+In **nicht-hochoptimiertem Code** (z. B. Skripte, die nur wenige Male laufen und im Interpreter bzw. Baseline JIT verbleiben):
+
+* Verursacht tiefe Verschachtelung in manchen Engines zusätzlichen Overhead beim Verwalten von Lexical Environments/Scopes auf dem Stack.
+* Early Returns erlauben es dem Interpreter, den aktuellen Stack-Frame schneller abzubauen, ohne tiefer liegende Scope-Hierarchien zu durchlaufen.
+
+Sobald der Code jedoch "heiß" läuft (nach einigen tausend Iterationen), eliminieren sowohl Turbofan als auch WarpMonkey diesen Unterschied vollständig.
\ No newline at end of file
diff --git a/06_js-debug/unterricht/tag46/01_inversion/04_README.pdf b/06_js-debug/unterricht/tag46/01_inversion/04_README.pdf
new file mode 100644
index 0000000..23e0860
Binary files /dev/null and b/06_js-debug/unterricht/tag46/01_inversion/04_README.pdf differ