From d2fdcc20ef0f6332c63e4cd5cc53b1f64eb5cda0 Mon Sep 17 00:00:00 2001 From: Denis Donici Date: Fri, 6 Mar 2026 12:00:29 +0200 Subject: [PATCH] chore: added runed example --- .../assets/svelte/RunedDemo.svelte | 96 +++++++++++++++++++ .../lib/example_web/components/layouts.ex | 1 + .../controllers/page_html/home.html.heex | 6 ++ .../lib/example_web/live/live_runed.ex | 89 +++++++++++++++++ example_project/lib/example_web/router.ex | 1 + example_project/package-lock.json | 39 +++++++- example_project/package.json | 1 + .../priv/static/.vite/manifest.json | 4 +- .../test/example_web/live/live_runed_test.exs | 33 +++++++ .../test/example_web/live/streams_test.exs | 3 + .../phoenix_test/live_editor_test.exs | 10 +- .../phoenix_test/live_runed_test.exs | 72 ++++++++++++++ 12 files changed, 347 insertions(+), 8 deletions(-) create mode 100644 example_project/assets/svelte/RunedDemo.svelte create mode 100644 example_project/lib/example_web/live/live_runed.ex create mode 100644 example_project/test/example_web/live/live_runed_test.exs create mode 100644 example_project/test/example_web/phoenix_test/live_runed_test.exs diff --git a/example_project/assets/svelte/RunedDemo.svelte b/example_project/assets/svelte/RunedDemo.svelte new file mode 100644 index 0000000..5f82ec9 --- /dev/null +++ b/example_project/assets/svelte/RunedDemo.svelte @@ -0,0 +1,96 @@ + + + +
+
+

+ Debounced +

+ +
+ Typing: {query} + Debounced: {debounced.current} +
+
    + {#each matches as item} +
  • {item}
  • + {/each} +
+
+ +
+ + +
+

+ ElementSize +

+ +

+ Live: {size.width}×{size.height}px +

+
+ +
+ + +
+

+ PressedKeys +

+
+ {#each [...keys.all] as key} + {key} + {/each} + {#if keys.all.size === 0} + Hold any keys... + {/if} +
+

+ Press Ctrl+Enter + to increment the server counter (current: {comboCount}) +

+
+
diff --git a/example_project/lib/example_web/components/layouts.ex b/example_project/lib/example_web/components/layouts.ex index 47be7c9..da56c70 100644 --- a/example_project/lib/example_web/components/layouts.ex +++ b/example_project/lib/example_web/components/layouts.ex @@ -49,6 +49,7 @@ defmodule ExampleWeb.Layouts do links: [ %{label: "Client Loading", to: ~p"/live-client-side-loading"}, %{label: "Rich Editor (@attach)", to: ~p"/live-editor"}, + %{label: "Runed Utilities", to: ~p"/live-runed"}, %{label: "SSR Demo", to: ~p"/live-ssr"} ] }, diff --git a/example_project/lib/example_web/controllers/page_html/home.html.heex b/example_project/lib/example_web/controllers/page_html/home.html.heex index aa5e721..1e80a50 100644 --- a/example_project/lib/example_web/controllers/page_html/home.html.heex +++ b/example_project/lib/example_web/controllers/page_html/home.html.heex @@ -186,6 +186,12 @@ - Server-side rendering with NodeJS +
  • + + Right Editor (@attach) + + - Use Editor.js via Svelte's @attach directive +
  • diff --git a/example_project/lib/example_web/live/live_runed.ex b/example_project/lib/example_web/live/live_runed.ex new file mode 100644 index 0000000..5925824 --- /dev/null +++ b/example_project/lib/example_web/live/live_runed.ex @@ -0,0 +1,89 @@ +defmodule ExampleWeb.LiveRuned do + use ExampleWeb, :live_view + + @items ~w(Elixir Erlang Phoenix LiveView Svelte Vue React Angular TypeScript + JavaScript Rust Go Python Ruby Java Kotlin Swift Haskell Clojure Scala) + + def mount(_params, _session, socket) do + {:ok, + assign(socket, + items: @items, + matches: @items, + last_size: %{width: nil, height: nil}, + combo_count: 0 + )} + end + + def handle_event("search", %{"query" => query}, socket) do + q = String.downcase(query) + matches = Enum.filter(@items, &String.contains?(String.downcase(&1), q)) + {:noreply, assign(socket, matches: matches)} + end + + def handle_event("resize", %{"width" => w, "height" => h}, socket) do + {:noreply, assign(socket, last_size: %{width: round(w), height: round(h)})} + end + + def handle_event("combo", _params, socket) do + {:noreply, assign(socket, combo_count: socket.assigns.combo_count + 1)} + end + + def render(assigns) do + ~H""" +
    +
    +

    Runed Utilities

    +

    + Three utilities from runed + — Debounced, ElementSize, PressedKeys — each syncing client state back to Phoenix LiveView. +

    + +
    +
    +
    + + RunedDemo + + <.svelte + name="RunedDemo" + props={%{ + items: @items, + matches: @matches, + lastSize: @last_size, + comboCount: @combo_count + }} + socket={@socket} + ssr={false} + /> +
    +
    + +
    +
    + + Server state + +

    + Search matches: {length(@matches)} +

    +

    + Last synced size: + + <%= if @last_size.width do %> + {@last_size.width}×{@last_size.height}px + <% else %> + not yet synced + <% end %> + +

    +

    + Ctrl+Enter combos: {@combo_count} +

    +
    +
    +
    +
    +
    + """ + end +end diff --git a/example_project/lib/example_web/router.ex b/example_project/lib/example_web/router.ex index 9cbf5b7..9572d22 100644 --- a/example_project/lib/example_web/router.ex +++ b/example_project/lib/example_web/router.ex @@ -48,6 +48,7 @@ defmodule ExampleWeb.Router do live("/live-navigation", LiveNavigation) live("/live-navigation/:page", LiveNavigation) live("/live-editor", LiveEditor) + live("/live-runed", LiveRuned) live("/live-ssr", LiveSsr) live("/live-composition", LiveComposition) end diff --git a/example_project/package-lock.json b/example_project/package-lock.json index 09375b4..3329eaa 100644 --- a/example_project/package-lock.json +++ b/example_project/package-lock.json @@ -13,6 +13,7 @@ "phoenix": "file:./deps/phoenix", "phoenix_html": "file:./deps/phoenix_html", "phoenix_live_view": "file:./deps/phoenix_live_view", + "runed": "^0.37.1", "svelte-dnd-action": "^0.9.56", "topbar": "^3.0.0" }, @@ -7843,7 +7844,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13266,6 +13266,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/lz-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lz-utils/-/lz-utils-2.1.0.tgz", @@ -15718,6 +15727,34 @@ "dev": true, "license": "MIT" }, + "node_modules/runed": { + "version": "0.37.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.37.1.tgz", + "integrity": "sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0", + "zod": "^4.1.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", diff --git a/example_project/package.json b/example_project/package.json index 20015bb..971bfd6 100644 --- a/example_project/package.json +++ b/example_project/package.json @@ -9,6 +9,7 @@ "phoenix": "file:./deps/phoenix", "phoenix_html": "file:./deps/phoenix_html", "phoenix_live_view": "file:./deps/phoenix_live_view", + "runed": "^0.37.1", "svelte-dnd-action": "^0.9.56", "topbar": "^3.0.0" }, diff --git a/example_project/priv/static/.vite/manifest.json b/example_project/priv/static/.vite/manifest.json index 9223c6b..f503c5d 100644 --- a/example_project/priv/static/.vite/manifest.json +++ b/example_project/priv/static/.vite/manifest.json @@ -18,12 +18,12 @@ "isDynamicEntry": true }, "css/app.css": { - "file": "assets/app-fZIL_d3Z.css", + "file": "assets/app-DwTE1PSy.css", "src": "css/app.css", "isEntry": true }, "js/app.js": { - "file": "assets/app-EcY7_2bB.js", + "file": "assets/app-Bmf8wBN3.js", "name": "app", "src": "js/app.js", "isEntry": true, diff --git a/example_project/test/example_web/live/live_runed_test.exs b/example_project/test/example_web/live/live_runed_test.exs new file mode 100644 index 0000000..3e786d5 --- /dev/null +++ b/example_project/test/example_web/live/live_runed_test.exs @@ -0,0 +1,33 @@ +defmodule ExampleWeb.LiveRunedTest do + @moduledoc """ + E2E tests for the /live-runed LiveView with the RunedDemo Svelte component. + Validates the full pipeline: LiveView → LiveSvelte hook → Svelte mounts runed utilities. + """ + use ExampleWeb.FeatureCase, async: false + + @moduletag :e2e + + test "page loads and shows heading", %{session: session} do + session + |> visit("/live-runed") + |> assert_has(Query.css("h1", text: "Runed Utilities")) + end + + test "search input renders after Svelte mounts", %{session: session} do + session + |> visit("/live-runed") + |> assert_has(Query.css("[data-testid='search-input']")) + end + + test "resizable element renders after Svelte mounts", %{session: session} do + session + |> visit("/live-runed") + |> assert_has(Query.css("[data-testid='resizable-element']")) + end + + test "pressed-keys container renders after Svelte mounts", %{session: session} do + session + |> visit("/live-runed") + |> assert_has(Query.css("[data-testid='pressed-keys']")) + end +end diff --git a/example_project/test/example_web/live/streams_test.exs b/example_project/test/example_web/live/streams_test.exs index 5cf5776..b1fd11c 100644 --- a/example_project/test/example_web/live/streams_test.exs +++ b/example_project/test/example_web/live/streams_test.exs @@ -90,6 +90,9 @@ defmodule ExampleWeb.StreamsTest do |> click(Query.css("[data-testid='clear-button']")) |> click(Query.css("[data-testid='reset-button-at-0']")) + # Wait for all 3 items to appear before checking order + session |> find(Query.css("[data-testid^='item-name-']", count: 3)) + # All 3 items back but in reversed order (each prepended at 0) names = all(session, Query.css("[data-testid^='item-name-']")) name_texts = Enum.map(names, &Wallaby.Element.text/1) diff --git a/example_project/test/example_web/phoenix_test/live_editor_test.exs b/example_project/test/example_web/phoenix_test/live_editor_test.exs index b956078..1cb8d23 100644 --- a/example_project/test/example_web/phoenix_test/live_editor_test.exs +++ b/example_project/test/example_web/phoenix_test/live_editor_test.exs @@ -1,7 +1,7 @@ defmodule ExampleWeb.PhoenixTest.LiveEditorTest do @moduledoc """ PhoenixTest (in-process) for LiveEditor (/live-editor). - Validates server-side rendering, data-props contract, and save_content event handling. + Validates server-side rendering, data-props contract, and sync_content event handling. Editor.js is browser-only; these tests skip SSR and validate the LiveView layer only. """ use ExampleWeb.ConnCase, async: false @@ -42,13 +42,13 @@ defmodule ExampleWeb.PhoenixTest.LiveEditorTest do |> assert_has("[data-testid='no-save-yet']") end - test "save_content event updates block count and removes no-save message", %{conn: conn} do + test "sync_content event updates block count and removes no-save message", %{conn: conn} do conn |> visit("/live-editor") |> assert_has("[data-testid='no-save-yet']") |> assert_has("[data-testid='block-count']", text: "2") |> unwrap(fn view -> - render_click(view, "save_content", %{ + render_click(view, "sync_content", %{ "blocks" => [ %{"type" => "header", "data" => %{"text" => "Hello", "level" => 2}}, %{"type" => "paragraph", "data" => %{"text" => "World"}}, @@ -60,11 +60,11 @@ defmodule ExampleWeb.PhoenixTest.LiveEditorTest do |> refute_has("[data-testid='no-save-yet']") end - test "save_content event with single block updates count to 1", %{conn: conn} do + test "sync_content event with single block updates count to 1", %{conn: conn} do conn |> visit("/live-editor") |> unwrap(fn view -> - render_click(view, "save_content", %{ + render_click(view, "sync_content", %{ "blocks" => [ %{"type" => "header", "data" => %{"text" => "Only block", "level" => 2}} ] diff --git a/example_project/test/example_web/phoenix_test/live_runed_test.exs b/example_project/test/example_web/phoenix_test/live_runed_test.exs new file mode 100644 index 0000000..0321b33 --- /dev/null +++ b/example_project/test/example_web/phoenix_test/live_runed_test.exs @@ -0,0 +1,72 @@ +defmodule ExampleWeb.PhoenixTest.LiveRunedTest do + @moduledoc """ + PhoenixTest (in-process) for LiveRuned (/live-runed). + Validates server-side rendering, data-props contract, and event handling. + RunedDemo is browser-only (ssr={false}); these tests validate the LiveView layer only. + """ + use ExampleWeb.ConnCase, async: false + import PhoenixTest + import Phoenix.LiveViewTest + + @moduletag :phoenix_test + + test "renders page heading", %{conn: conn} do + conn + |> visit("/live-runed") + |> assert_has("h1", text: "Runed Utilities") + end + + test "renders RunedDemo Svelte component with correct props", %{conn: conn} do + conn + |> visit("/live-runed") + |> assert_has("[data-name='RunedDemo']", count: 1) + |> assert_has("[data-props*='matches']") + |> assert_has("[data-props*='comboCount']") + end + + test "initial match count is 20 (all items)", %{conn: conn} do + conn + |> visit("/live-runed") + |> assert_has("[data-testid='match-count']", text: "20") + end + + test "search event filters matches", %{conn: conn} do + conn + |> visit("/live-runed") + |> assert_has("[data-testid='match-count']", text: "20") + |> unwrap(fn view -> + render_click(view, "search", %{"query" => "eli"}) + end) + |> assert_has("[data-testid='match-count']", text: "1") + end + + test "resize event updates server-size display", %{conn: conn} do + conn + |> visit("/live-runed") + |> unwrap(fn view -> + render_click(view, "resize", %{"width" => 400, "height" => 200}) + end) + |> assert_has("[data-testid='server-size']", text: "400×200px") + end + + test "combo event increments combo-count", %{conn: conn} do + conn + |> visit("/live-runed") + |> assert_has("[data-testid='combo-count']", text: "0") + |> unwrap(fn view -> + render_click(view, "combo", %{}) + end) + |> assert_has("[data-testid='combo-count']", text: "1") + |> unwrap(fn view -> + render_click(view, "combo", %{}) + end) + |> assert_has("[data-testid='combo-count']", text: "2") + end + + test "server size shows not yet synced initially", %{conn: conn} do + conn + |> visit("/live-runed") + |> assert_has("[data-testid='server-size']", text: "not yet synced") + |> refute_has("[data-testid='server-size']", text: "px") + end +end