From 3b0f059ceac2e10f7ce49980d95c72c160c5d4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Wed, 15 Apr 2026 17:51:57 +0200 Subject: [PATCH] spa(phase 5, tasks 26-32): reactions, replies, nip-07 kommentare, e2e MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Komponenten unter $lib/components/: - Reactions.svelte: lädt kind:7-Aggregation via loadReactions, rendert Chips mit Emoji + Count. +/- werden zu 👍/👎 gemappt. - ReplyItem.svelte: einzelner Kommentar mit Author-Npub-Prefix + Datum. - ReplyList.svelte: lädt kind:1-Replies, merged mit optimistic-Props (dedup per id), sortiert chronologisch. - ReplyComposer.svelte: Textarea + Senden-Button. Nutzt NIP-07-Wrapper (getPublicKey, signEvent), baut kind:1-Event mit a/e/p-Tags, pusht via pool.publish() zu allen Read-Relays. Fehlertolerant: zeigt Hinweis, wenn NIP-07-Extension fehlt. Integration in PostView: Reactions, Composer, ReplyList unterhalb des Artikel-Bodys. Optimistisches Reply-Pattern: Composer.onPublished pushed signed event in PostView-local $state, ReplyList merged mit fetched events. Playwright-E2E: - playwright.config.ts mit Dev-Server-Auto-Start - home.test.ts: Profil + Beitragsliste sichtbar - post.test.ts: Titel + Body + Legacy-URL-Redirect Alle 3 E2E-Tests grün. npm run check: 600 files, 0 errors. Deploy live auf svelte.joerg-lohrer.de (Phase 5 inklusive). Co-Authored-By: Claude Opus 4.6 (1M context) --- app/playwright.config.ts | 13 ++ app/src/lib/components/PostView.svelte | 18 +++ app/src/lib/components/Reactions.svelte | 58 ++++++++ app/src/lib/components/ReplyComposer.svelte | 148 ++++++++++++++++++++ app/src/lib/components/ReplyItem.svelte | 44 ++++++ app/src/lib/components/ReplyList.svelte | 68 +++++++++ app/test-results/.last-run.json | 4 + app/tests/e2e/home.test.ts | 8 ++ app/tests/e2e/post.test.ts | 16 +++ 9 files changed, 377 insertions(+) create mode 100644 app/playwright.config.ts create mode 100644 app/src/lib/components/Reactions.svelte create mode 100644 app/src/lib/components/ReplyComposer.svelte create mode 100644 app/src/lib/components/ReplyItem.svelte create mode 100644 app/src/lib/components/ReplyList.svelte create mode 100644 app/test-results/.last-run.json create mode 100644 app/tests/e2e/home.test.ts create mode 100644 app/tests/e2e/post.test.ts diff --git a/app/playwright.config.ts b/app/playwright.config.ts new file mode 100644 index 0000000..676a7b3 --- /dev/null +++ b/app/playwright.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: 'tests/e2e', + use: { baseURL: 'http://localhost:5173' }, + webServer: { + command: 'npm run dev', + port: 5173, + reuseExistingServer: true, + timeout: 120_000 + }, + timeout: 60_000 +}); diff --git a/app/src/lib/components/PostView.svelte b/app/src/lib/components/PostView.svelte index 0b06b2a..f838d01 100644 --- a/app/src/lib/components/PostView.svelte +++ b/app/src/lib/components/PostView.svelte @@ -1,6 +1,10 @@ + +{#if reactions.length > 0} +
+ {#each reactions as r} + + {displayChar(r.content)} + {r.count} + + {/each} +
+{/if} + + diff --git a/app/src/lib/components/ReplyComposer.svelte b/app/src/lib/components/ReplyComposer.svelte new file mode 100644 index 0000000..49918b2 --- /dev/null +++ b/app/src/lib/components/ReplyComposer.svelte @@ -0,0 +1,148 @@ + + +
+ {#if !nip07} +

+ Um zu kommentieren, benötigst du eine Nostr-Extension + (Alby, + nos2x), oder + kommentiere direkt in einem Nostr-Client. +

+ {:else} + +
+ +
+ {#if error}

{error}

{/if} + {#if info}

{info}

{/if} + {/if} +
+ + diff --git a/app/src/lib/components/ReplyItem.svelte b/app/src/lib/components/ReplyItem.svelte new file mode 100644 index 0000000..63d746f --- /dev/null +++ b/app/src/lib/components/ReplyItem.svelte @@ -0,0 +1,44 @@ + + +
  • +
    + {authorNpub} + · + {date} +
    +
    {event.content}
    +
  • + + diff --git a/app/src/lib/components/ReplyList.svelte b/app/src/lib/components/ReplyList.svelte new file mode 100644 index 0000000..3bff962 --- /dev/null +++ b/app/src/lib/components/ReplyList.svelte @@ -0,0 +1,68 @@ + + +
    +

    Kommentare ({merged.length})

    + {#if loading} +

    Lade Kommentare …

    + {:else if merged.length === 0} +

    Noch keine Kommentare.

    + {:else} +
      + {#each merged as reply (reply.id)} + + {/each} +
    + {/if} +
    + + diff --git a/app/test-results/.last-run.json b/app/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/app/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/app/tests/e2e/home.test.ts b/app/tests/e2e/home.test.ts new file mode 100644 index 0000000..443ed31 --- /dev/null +++ b/app/tests/e2e/home.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test'; + +test('Home zeigt Profil und mindestens einen Post', async ({ page }) => { + await page.goto('/'); + await expect(page.getByRole('heading', { level: 1, name: /Beiträge/ })).toBeVisible(); + await expect(page.locator('.profile .name')).toBeVisible({ timeout: 15_000 }); + await expect(page.locator('a.card').first()).toBeVisible({ timeout: 15_000 }); +}); diff --git a/app/tests/e2e/post.test.ts b/app/tests/e2e/post.test.ts new file mode 100644 index 0000000..1f32c9b --- /dev/null +++ b/app/tests/e2e/post.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test'; + +test('Einzelpost rendert Titel und Markdown-Body', async ({ page }) => { + await page.goto('/dezentrale-oep-oer/'); + // Titel steht einmal als .post-title (H1 außerhalb des Artikels), + // und nochmal im Markdown-Body des Events — wir prüfen den ersten. + await expect(page.locator('h1.post-title')).toBeVisible({ timeout: 15_000 }); + await expect(page.locator('h1.post-title')).toContainText('Gemeinsam die Bildungszukunft'); + await expect(page.locator('article')).toContainText('Open Educational'); +}); + +test('Legacy-URL wird auf kurze Form umgeleitet', async ({ page }) => { + await page.goto('/2025/03/04/dezentrale-oep-oer.html/'); + await expect(page).toHaveURL(/\/dezentrale-oep-oer\/$/); + await expect(page.locator('h1.post-title')).toBeVisible({ timeout: 15_000 }); +});