ruby_wasm_ui 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/rules/ruby_comments.mdc +29 -0
  3. data/.github/workflows/playwright.yml +74 -0
  4. data/.github/workflows/rspec.yml +33 -0
  5. data/.node-version +1 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +218 -0
  9. data/Rakefile +4 -0
  10. data/docs/conditional-rendering.md +119 -0
  11. data/docs/lifecycle-hooks.md +75 -0
  12. data/docs/list-rendering.md +51 -0
  13. data/examples/Gemfile +5 -0
  14. data/examples/Gemfile.lock +41 -0
  15. data/examples/Makefile +15 -0
  16. data/examples/npm-packages/runtime/counter/index.html +28 -0
  17. data/examples/npm-packages/runtime/counter/index.rb +62 -0
  18. data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
  19. data/examples/npm-packages/runtime/hello/index.html +28 -0
  20. data/examples/npm-packages/runtime/hello/index.rb +29 -0
  21. data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
  22. data/examples/npm-packages/runtime/input/index.html +28 -0
  23. data/examples/npm-packages/runtime/input/index.rb +46 -0
  24. data/examples/npm-packages/runtime/input/index.spec.js +58 -0
  25. data/examples/npm-packages/runtime/list/index.html +27 -0
  26. data/examples/npm-packages/runtime/list/index.rb +33 -0
  27. data/examples/npm-packages/runtime/list/index.spec.js +46 -0
  28. data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
  29. data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
  30. data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
  31. data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
  32. data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
  33. data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
  34. data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
  35. data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
  36. data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
  37. data/examples/npm-packages/runtime/search_field/index.html +27 -0
  38. data/examples/npm-packages/runtime/search_field/index.rb +39 -0
  39. data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
  40. data/examples/npm-packages/runtime/todos/index.html +28 -0
  41. data/examples/npm-packages/runtime/todos/index.rb +239 -0
  42. data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
  43. data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
  44. data/examples/package.json +12 -0
  45. data/examples/src/counter/index.html +23 -0
  46. data/examples/src/counter/index.rb +60 -0
  47. data/lib/ruby_wasm_ui +1 -0
  48. data/lib/ruby_wasm_ui.rb +1 -0
  49. data/package-lock.json +100 -0
  50. data/package.json +32 -0
  51. data/packages/npm-packages/runtime/Gemfile +3 -0
  52. data/packages/npm-packages/runtime/Gemfile.lock +26 -0
  53. data/packages/npm-packages/runtime/README.md +5 -0
  54. data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
  55. data/packages/npm-packages/runtime/package-lock.json +6668 -0
  56. data/packages/npm-packages/runtime/package.json +38 -0
  57. data/packages/npm-packages/runtime/rollup.config.mjs +89 -0
  58. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/component_spec.rb +416 -0
  59. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/dom/scheduler_spec.rb +98 -0
  60. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/nodes_equal_spec.rb +190 -0
  61. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb +505 -0
  62. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_for_group_spec.rb +377 -0
  63. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_vdom_spec.rb +573 -0
  64. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/parser_spec.rb +627 -0
  65. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/arrays_spec.rb +228 -0
  66. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/objects_spec.rb +127 -0
  67. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/props_spec.rb +205 -0
  68. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/strings_spec.rb +107 -0
  69. data/packages/npm-packages/runtime/spec/spec_helper.rb +16 -0
  70. data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
  71. data/packages/npm-packages/runtime/src/index.js +37 -0
  72. data/packages/npm-packages/runtime/src/ruby_wasm_ui/app.rb +53 -0
  73. data/packages/npm-packages/runtime/src/ruby_wasm_ui/component.rb +215 -0
  74. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dispatcher.rb +46 -0
  75. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/attributes.rb +105 -0
  76. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/destroy_dom.rb +63 -0
  77. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/events.rb +40 -0
  78. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/mount_dom.rb +108 -0
  79. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/patch_dom.rb +237 -0
  80. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/scheduler.rb +51 -0
  81. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom.rb +13 -0
  82. data/packages/npm-packages/runtime/src/ruby_wasm_ui/nodes_equal.rb +45 -0
  83. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_conditional_group.rb +150 -0
  84. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_for_group.rb +125 -0
  85. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_vdom.rb +220 -0
  86. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/parser.rb +134 -0
  87. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template.rb +11 -0
  88. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/arrays.rb +185 -0
  89. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/objects.rb +37 -0
  90. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/props.rb +25 -0
  91. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/strings.rb +19 -0
  92. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils.rb +11 -0
  93. data/packages/npm-packages/runtime/src/ruby_wasm_ui/vdom.rb +84 -0
  94. data/packages/npm-packages/runtime/src/ruby_wasm_ui/version.rb +5 -0
  95. data/packages/npm-packages/runtime/src/ruby_wasm_ui.rb +14 -0
  96. data/packages/npm-packages/runtime/vitest.config.js +8 -0
  97. data/playwright.config.js +78 -0
  98. data/sig/ruby_wasm_ui.rbs +4 -0
  99. metadata +168 -0
@@ -0,0 +1,46 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test.describe("List Example", () => {
4
+ test("should display list with initial todos", async ({ page }) => {
5
+ await page.goto("/examples/npm-packages/runtime/list/index.html?env=DEV");
6
+ await page.waitForTimeout(3000);
7
+
8
+ // Check the page title
9
+ await expect(page).toHaveTitle("List");
10
+
11
+ // Check that the list is rendered
12
+ await expect(page.locator("ul")).toBeVisible();
13
+
14
+ // Check that all initial todos are displayed (both component and element versions)
15
+ const listItems = page.locator("li");
16
+ await expect(listItems).toHaveCount(6);
17
+
18
+ // Check the content of each list item (component version first, then element version)
19
+ await expect(listItems.nth(0)).toHaveText("foo");
20
+ await expect(listItems.nth(1)).toHaveText("bar");
21
+ await expect(listItems.nth(2)).toHaveText("baz");
22
+ await expect(listItems.nth(3)).toHaveText("foo");
23
+ await expect(listItems.nth(4)).toHaveText("bar");
24
+ await expect(listItems.nth(5)).toHaveText("baz");
25
+ });
26
+
27
+ test("should render r-for with components correctly", async ({ page }) => {
28
+ await page.goto("/examples/npm-packages/runtime/list/index.html?env=DEV");
29
+ await page.waitForTimeout(3000);
30
+
31
+ // Check that ListItem components are rendered (first 3 items)
32
+ const componentItems = page.locator("li").nth(0);
33
+ await expect(componentItems).toBeVisible();
34
+ await expect(componentItems).toHaveText("foo");
35
+ });
36
+
37
+ test("should render r-for with elements correctly", async ({ page }) => {
38
+ await page.goto("/examples/npm-packages/runtime/list/index.html?env=DEV");
39
+ await page.waitForTimeout(3000);
40
+
41
+ // Check that direct li elements are rendered (last 3 items)
42
+ const elementItems = page.locator("li").nth(3);
43
+ await expect(elementItems).toBeVisible();
44
+ await expect(elementItems).toHaveText("foo");
45
+ });
46
+ });
@@ -0,0 +1,40 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>on_mounted Demo</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ }
14
+ div {
15
+ margin: 20px 0;
16
+ padding: 15px;
17
+ border: 1px solid #ddd;
18
+ border-radius: 5px;
19
+ }
20
+ h1 {
21
+ color: #333;
22
+ text-align: center;
23
+ }
24
+ h2 {
25
+ color: #666;
26
+ }
27
+ p {
28
+ color: #777;
29
+ }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <div id="app"></div>
34
+ <script
35
+ type="module"
36
+ src="../../../../packages/npm-packages/runtime/dist/ruby-wasm-ui.js"
37
+ ></script>
38
+ <script type="text/ruby" src="./index.rb"></script>
39
+ </body>
40
+ </html>
@@ -0,0 +1,59 @@
1
+ require "js"
2
+
3
+ # Component with argument-less on_mounted that can call component methods directly
4
+ SimpleComponent = RubyWasmUi.define_component(
5
+ state: -> { { message: "Not mounted yet" } },
6
+
7
+ # on_mounted without arguments - can call update_state directly!
8
+ on_mounted: -> {
9
+ puts "SimpleComponent mounted without arguments!"
10
+ update_state(message: "Mounted and state updated without component argument!")
11
+ },
12
+
13
+ template: ->() {
14
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
15
+ <div>
16
+ <h2>Simple Component (no args in on_mounted)</h2>
17
+ <p>{state[:message]}</p>
18
+ </div>
19
+ HTML
20
+ }
21
+ )
22
+
23
+ # Component with on_mounted that takes component argument (existing behavior)
24
+ AdvancedComponent = RubyWasmUi.define_component(
25
+ state: -> { { message: "Not mounted yet" } },
26
+
27
+ # on_mounted with component argument - existing behavior still works
28
+ on_mounted: ->() {
29
+ puts "AdvancedComponent mounted with component argument!"
30
+ update_state(message: "Mounted and state updated!")
31
+ },
32
+
33
+ template: ->() {
34
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
35
+ <div>
36
+ <h2>Advanced Component (with args in on_mounted)</h2>
37
+ <p>{state[:message]}</p>
38
+ </div>
39
+ HTML
40
+ }
41
+ )
42
+
43
+ # Main App component
44
+ AppComponent = RubyWasmUi.define_component(
45
+ template: -> {
46
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
47
+ <div>
48
+ <h1>on_mounted Demo</h1>
49
+ <SimpleComponent />
50
+ <AdvancedComponent />
51
+ </div>
52
+ HTML
53
+ }
54
+ )
55
+
56
+ # Mount the app
57
+ app = RubyWasmUi::App.create(AppComponent)
58
+ app_element = JS.global[:document].getElementById("app")
59
+ app.mount(app_element)
@@ -0,0 +1,50 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test.describe("On Mounted Demo Example", () => {
4
+ test("should display components and verify on_mounted hooks execute correctly", async ({
5
+ page,
6
+ }) => {
7
+ await page.goto("/examples/npm-packages/runtime/on_mounted_demo/index.html?env=DEV");
8
+ await page.waitForTimeout(3000);
9
+
10
+ // Check the page title
11
+ await expect(page).toHaveTitle("on_mounted Demo");
12
+
13
+ // Check main heading
14
+ await expect(page.locator("h1")).toHaveText("on_mounted Demo");
15
+
16
+ // Check both component headings are present
17
+ await expect(page.locator("h2").first()).toHaveText(
18
+ "Simple Component (no args in on_mounted)"
19
+ );
20
+ await expect(page.locator("h2").last()).toHaveText(
21
+ "Advanced Component (with args in on_mounted)"
22
+ );
23
+
24
+ // Verify on_mounted hooks have executed and updated the messages
25
+ const simpleComponentMessage = page
26
+ .locator("h2")
27
+ .first()
28
+ .locator("..")
29
+ .locator("p");
30
+ await expect(simpleComponentMessage).toHaveText(
31
+ "Mounted and state updated without component argument!"
32
+ );
33
+
34
+ const advancedComponentMessage = page
35
+ .locator("h2")
36
+ .last()
37
+ .locator("..")
38
+ .locator("p");
39
+ await expect(advancedComponentMessage).toHaveText(
40
+ "Mounted and state updated!"
41
+ );
42
+
43
+ // Neither component should show the initial "Not mounted yet" message
44
+ const paragraphs = page.locator("p");
45
+ for (let i = 0; i < (await paragraphs.count()); i++) {
46
+ const text = await paragraphs.nth(i).textContent();
47
+ expect(text).not.toBe("Not mounted yet");
48
+ }
49
+ });
50
+ });
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>r-if Attribute Demo</title>
7
+ <script>
8
+ const urlParams = new URLSearchParams(window.location.search);
9
+ const isDev = urlParams.get("env") === "DEV";
10
+ const script = document.createElement("script");
11
+ script.defer = true;
12
+ script.src = isDev
13
+ ? "../../../../packages/npm-packages/runtime/dist/ruby-wasm-ui.js"
14
+ : "https://unpkg.com/ruby-wasm-ui@latest";
15
+ document.head.appendChild(script);
16
+ </script>
17
+ <script defer type="text/ruby" src="index.rb"></script>
18
+ <style>
19
+ body {
20
+ font-family: Arial, sans-serif;
21
+ max-width: 600px;
22
+ margin: 20px auto;
23
+ padding: 20px;
24
+ background-color: #f5f5f5;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <h1>r-if Attribute Demo</h1>
30
+ <div id="app">
31
+ <!-- Ruby WASM UI app will mount here -->
32
+ </div>
33
+ </body>
34
+ </html>
@@ -0,0 +1,113 @@
1
+ require "js"
2
+
3
+ # R-If Attribute Demo component to demonstrate conditional rendering using r-if attribute
4
+ RIfAttributeDemo = RubyWasmUi.define_component(
5
+ # Initialize component state
6
+ state: ->() {
7
+ {
8
+ show_message: false,
9
+ counter: 0
10
+ }
11
+ },
12
+
13
+ # Render the component with r-if attribute examples
14
+ template: ->() {
15
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
16
+ <div>
17
+ <p>Using r-if as an attribute (like Vue.js r-if)</p>
18
+
19
+ <!-- Example 1: Simple boolean toggle -->
20
+ <div style="border: 1px solid #ccc; margin: 10px 0; padding: 10px;">
21
+ <h2>Toggle Message</h2>
22
+ <button
23
+ style="background: #007bff; color: white; padding: 8px 16px; border: none; cursor: pointer;"
24
+ on="{click: ->() { toggle_message }}"
25
+ >
26
+ {state[:show_message] ? "Hide" : "Show"} Message
27
+ </button>
28
+
29
+ <div
30
+ style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 10px; margin-top: 10px;"
31
+ r-if="{state[:show_message]}"
32
+ >
33
+ <p>This message is conditionally rendered using r-if attribute!</p>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Example 2: Counter-based condition -->
38
+ <div style="border: 1px solid #ccc; margin: 10px 0; padding: 10px;">
39
+ <h2>Counter Conditions</h2>
40
+ <button
41
+ style="background: #28a745; color: white; padding: 8px 16px; border: none; cursor: pointer; margin-right: 5px;"
42
+ on="{click: ->() { increment_counter }}"
43
+ >
44
+ +1
45
+ </button>
46
+ <button
47
+ style="background: #dc3545; color: white; padding: 8px 16px; border: none; cursor: pointer; margin-right: 5px;"
48
+ on="{click: ->() { decrement_counter }}"
49
+ >
50
+ -1
51
+ </button>
52
+ <button
53
+ style="background: #6c757d; color: white; padding: 8px 16px; border: none; cursor: pointer;"
54
+ on="{click: ->() { reset_counter }}"
55
+ >
56
+ Reset
57
+ </button>
58
+
59
+ <p>Counter: <strong>{state[:counter]}</strong></p>
60
+
61
+ <div
62
+ style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 10px; margin: 5px 0;"
63
+ r-if="{state[:counter] > 0}"
64
+ >
65
+ <p>Counter is positive! ({state[:counter]})</p>
66
+ </div>
67
+
68
+ <div
69
+ style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 10px; margin: 5px 0;"
70
+ r-if="{state[:counter] < 0}"
71
+ >
72
+ <p>Counter is negative! ({state[:counter]})</p>
73
+ </div>
74
+
75
+ <div
76
+ style="background: #e2e3e5; border: 1px solid #d6d8db; color: #383d41; padding: 10px; margin: 5px 0;"
77
+ r-if="{state[:counter] == 0}"
78
+ >
79
+ <p>Counter is zero.</p>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ HTML
84
+ },
85
+
86
+ # Component methods
87
+ methods: {
88
+ # Toggle the message visibility
89
+ toggle_message: ->() {
90
+ update_state(show_message: !state[:show_message])
91
+ },
92
+
93
+ # Increment the counter
94
+ increment_counter: ->() {
95
+ update_state(counter: state[:counter] + 1)
96
+ },
97
+
98
+ # Decrement the counter
99
+ decrement_counter: ->() {
100
+ update_state(counter: state[:counter] - 1)
101
+ },
102
+
103
+ # Reset the counter to zero
104
+ reset_counter: ->() {
105
+ update_state(counter: 0)
106
+ }
107
+ }
108
+ )
109
+
110
+ # Create and mount the app
111
+ app = RubyWasmUi::App.create(RIfAttributeDemo)
112
+ app_element = JS.global[:document].getElementById("app")
113
+ app.mount(app_element)
@@ -0,0 +1,140 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test.describe("R-If Attribute Demo Example", () => {
4
+ test("should display page layout and toggle message functionality", async ({
5
+ page,
6
+ }) => {
7
+ await page.goto("/examples/npm-packages/runtime/r_if_attribute_demo/index.html?env=DEV");
8
+ await page.waitForTimeout(3000);
9
+
10
+ // Check the page title
11
+ await expect(page).toHaveTitle("r-if Attribute Demo");
12
+
13
+ // Check main heading
14
+ await expect(page.locator("h1")).toHaveText("r-if Attribute Demo");
15
+
16
+ // Check description
17
+ await expect(page.locator("p").first()).toHaveText(
18
+ "Using r-if as an attribute (like Vue.js r-if)"
19
+ );
20
+
21
+ // Check section headings
22
+ await expect(page.locator("h2").first()).toHaveText("Toggle Message");
23
+ await expect(page.locator("h2").last()).toHaveText("Counter Conditions");
24
+
25
+ // Test message toggle functionality
26
+ const toggleButton = page.locator("button").first();
27
+ const messageDiv = page
28
+ .getByText("This message is conditionally rendered using r-if attribute!")
29
+ .locator("..");
30
+
31
+ // Initially message should be hidden (show_message: false)
32
+ await expect(toggleButton).toHaveText("Show Message");
33
+ await expect(messageDiv).not.toBeVisible();
34
+
35
+ // Click to show message
36
+ await toggleButton.click();
37
+ await page.waitForTimeout(100);
38
+
39
+ await expect(toggleButton).toHaveText("Hide Message");
40
+ await expect(messageDiv).toBeVisible();
41
+ await expect(messageDiv.locator("p")).toHaveText(
42
+ "This message is conditionally rendered using r-if attribute!"
43
+ );
44
+
45
+ // Click to hide message again
46
+ await toggleButton.click();
47
+ await page.waitForTimeout(100);
48
+
49
+ await expect(toggleButton).toHaveText("Show Message");
50
+ await expect(messageDiv).not.toBeVisible();
51
+ });
52
+
53
+ test("should handle counter operations and conditional message display", async ({
54
+ page,
55
+ }) => {
56
+ await page.goto("/examples/npm-packages/runtime/r_if_attribute_demo/index.html?env=DEV");
57
+ await page.waitForTimeout(3000);
58
+
59
+ // Check counter buttons
60
+ const incrementButton = page.locator("button").nth(1); // +1 button
61
+ const decrementButton = page.locator("button").nth(2); // -1 button
62
+ const resetButton = page.locator("button").nth(3); // Reset button
63
+ const counterDisplay = page.locator("p").filter({ hasText: "Counter:" });
64
+
65
+ await expect(incrementButton).toHaveText("+1");
66
+ await expect(decrementButton).toHaveText("-1");
67
+ await expect(resetButton).toHaveText("Reset");
68
+
69
+ // Check initial counter display and zero message
70
+ await expect(counterDisplay).toContainText("Counter:0");
71
+
72
+ const positiveMessage = page
73
+ .getByText("Counter is positive!")
74
+ .locator("..");
75
+ const negativeMessage = page
76
+ .getByText("Counter is negative!")
77
+ .locator("..");
78
+ const zeroMessage = page.getByText("Counter is zero.").locator("..");
79
+
80
+ await expect(zeroMessage).toBeVisible();
81
+ await expect(positiveMessage).not.toBeVisible();
82
+ await expect(negativeMessage).not.toBeVisible();
83
+
84
+ // Test increment operations and positive message
85
+ await incrementButton.click();
86
+ await page.waitForTimeout(100);
87
+ await expect(counterDisplay).toContainText("Counter:1");
88
+ await expect(positiveMessage).toBeVisible();
89
+ await expect(positiveMessage.locator("p")).toHaveText(
90
+ "Counter is positive! (1)"
91
+ );
92
+ await expect(zeroMessage).not.toBeVisible();
93
+ await expect(negativeMessage).not.toBeVisible();
94
+
95
+ // Test more increments
96
+ await incrementButton.click();
97
+ await incrementButton.click();
98
+ await page.waitForTimeout(100);
99
+ await expect(counterDisplay).toContainText("Counter:3");
100
+ await expect(positiveMessage.locator("p")).toHaveText(
101
+ "Counter is positive! (3)"
102
+ );
103
+
104
+ // Test reset
105
+ await resetButton.click();
106
+ await page.waitForTimeout(100);
107
+ await expect(counterDisplay).toContainText("Counter:0");
108
+ await expect(zeroMessage).toBeVisible();
109
+ await expect(positiveMessage).not.toBeVisible();
110
+ await expect(negativeMessage).not.toBeVisible();
111
+
112
+ // Test decrement operations and negative message
113
+ await decrementButton.click();
114
+ await page.waitForTimeout(100);
115
+ await expect(counterDisplay).toContainText("Counter:-1");
116
+ await expect(negativeMessage).toBeVisible();
117
+ await expect(negativeMessage.locator("p")).toHaveText(
118
+ "Counter is negative! (-1)"
119
+ );
120
+ await expect(zeroMessage).not.toBeVisible();
121
+ await expect(positiveMessage).not.toBeVisible();
122
+
123
+ // Test more decrements
124
+ await decrementButton.click();
125
+ await decrementButton.click();
126
+ await page.waitForTimeout(100);
127
+ await expect(counterDisplay).toContainText("Counter:-3");
128
+ await expect(negativeMessage.locator("p")).toHaveText(
129
+ "Counter is negative! (-3)"
130
+ );
131
+
132
+ // Verify styling for conditional messages
133
+ await expect(negativeMessage).toHaveAttribute("style");
134
+
135
+ // Reset and verify zero message styling
136
+ await resetButton.click();
137
+ await page.waitForTimeout(100);
138
+ await expect(zeroMessage).toHaveAttribute("style");
139
+ });
140
+ });
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
8
+ <script>
9
+ const urlParams = new URLSearchParams(window.location.search);
10
+ const isDev = urlParams.get("env") === "DEV";
11
+ const script = document.createElement("script");
12
+ script.defer = true;
13
+ script.src = isDev
14
+ ? "../../../../packages/npm-packages/runtime/dist/ruby-wasm-ui.js"
15
+ : "https://unpkg.com/ruby-wasm-ui@latest";
16
+ document.head.appendChild(script);
17
+ </script>
18
+ <script defer type="text/ruby" src="index.rb"></script>
19
+
20
+ <title>Random Cocktail</title>
21
+ </head>
22
+
23
+ <body>
24
+ <h1>Random Cocktail</h1>
25
+ <div id="app"></div>
26
+ </body>
27
+ </html>
@@ -0,0 +1,69 @@
1
+ require "js"
2
+
3
+ random_cocktail = RubyWasmUi.define_component(
4
+ state: ->() {
5
+ {
6
+ is_loading: false,
7
+ cocktail: nil
8
+ }
9
+ },
10
+
11
+ template: ->() {
12
+ is_loading = state[:is_loading] # Used in template
13
+ cocktail = state[:cocktail] # Used in template
14
+
15
+ template = <<~HTML
16
+ <p r-if="{is_loading}">
17
+ Loading...
18
+ </p>
19
+ <button-component r-elsif="{cocktail.nil?}" on="{click_button: ->() { fetch_cocktail }}" label="Get a cocktail"/>
20
+ <template r-else>
21
+ <h1>{cocktail['strDrink']}</h1>
22
+ <p>{cocktail['strInstructions']}</p>
23
+ <img src="{cocktail['strDrinkThumb']}" alt="{cocktail['strDrink']}" style="width: 300px; height: 300px" />
24
+ <button-component on="{click_button: ->() { fetch_cocktail }}" label="Get another cocktail"/>
25
+ </template>
26
+ HTML
27
+
28
+ RubyWasmUi::Template::Parser.parse_and_eval(template, binding)
29
+ },
30
+
31
+ methods: {
32
+ fetch_cocktail: ->{
33
+ # Set loading state
34
+ update_state(is_loading: true, cocktail: nil)
35
+
36
+ Fiber.new do
37
+ response = JS.global.fetch("https://www.thecocktaildb.com/api/json/v1/1/random.php").await
38
+ response.call(:json).then(->(data) {
39
+ update_state(is_loading: false, cocktail: data[:drinks][0])
40
+ }).catch(->(error) {
41
+ update_state(is_loading: false, cocktail: nil)
42
+ })
43
+ end.transfer
44
+ }
45
+ },
46
+
47
+ on_mounted: ->() {
48
+ fetch_cocktail
49
+ }
50
+ )
51
+
52
+ ButtonComponent = RubyWasmUi.define_component(
53
+ state: ->(props) {
54
+ { label: props[:label] }
55
+ },
56
+
57
+ template: ->() {
58
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
59
+ <button on="{click: ->() { emit('click_button') }}" style="display: block; margin: 1em auto">
60
+ {state[:label]}
61
+ </button>
62
+ HTML
63
+ }
64
+ )
65
+
66
+ # Create and mount the app
67
+ app = RubyWasmUi::App.create(random_cocktail)
68
+ app_element = JS.global[:document].getElementById("app")
69
+ app.mount(app_element)
@@ -0,0 +1,101 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test.describe("Random Cocktail Example", () => {
4
+ test("should display loading state and fetch initial cocktail data", async ({
5
+ page,
6
+ }) => {
7
+ await page.goto("/examples/npm-packages/runtime/random_cocktail/index.html?env=DEV");
8
+ await page.waitForTimeout(1000); // Shorter wait to catch loading state
9
+
10
+ // Check the page title
11
+ await expect(page).toHaveTitle("Random Cocktail");
12
+
13
+ // Should initially show loading state or quickly transition to cocktail data
14
+ // const hasLoadingOrCocktail = await Promise.race([
15
+ // page
16
+ // .getByText("Loading...")
17
+ // .isVisible()
18
+ // .catch(() => false),
19
+ // page
20
+ // .locator("#app h1")
21
+ // .isVisible()
22
+ // .catch(() => false),
23
+ // ]);
24
+ // expect(hasLoadingOrCocktail).toBe(true);
25
+
26
+ // Wait for the cocktail to load (API call)
27
+ await page.waitForTimeout(5000);
28
+
29
+ // After loading, should show cocktail information
30
+ await expect(page.locator("#app h1")).toBeVisible();
31
+ await expect(page.locator("#app p").last()).toBeVisible();
32
+ await expect(page.locator("img")).toBeVisible();
33
+ await expect(page.locator("button")).toHaveText("Get another cocktail");
34
+
35
+ // Verify cocktail data is displayed correctly
36
+ const cocktailName = await page.locator("#app h1").textContent();
37
+ expect(cocktailName).toBeTruthy();
38
+ expect(cocktailName.length).toBeGreaterThan(0);
39
+
40
+ const instructions = await page.locator("#app p").last().textContent();
41
+ expect(instructions).toBeTruthy();
42
+ expect(instructions.length).toBeGreaterThan(0);
43
+
44
+ // Verify image attributes and styling
45
+ const img = page.locator("img");
46
+ await expect(img).toHaveAttribute("alt");
47
+ await expect(img).toHaveAttribute("src");
48
+ await expect(img).toHaveCSS("width", "300px");
49
+ await expect(img).toHaveCSS("height", "300px");
50
+
51
+ const imgSrc = await img.getAttribute("src");
52
+ const imgAlt = await img.getAttribute("alt");
53
+ expect(imgSrc).toBeTruthy();
54
+ expect(imgAlt).toBeTruthy();
55
+ expect(imgSrc).toMatch(/^https?:\/\//); // Should be a valid URL
56
+
57
+ // Verify button styling and behavior
58
+ const button = page.locator("button");
59
+ await expect(button).toBeVisible();
60
+ await expect(button).toBeEnabled();
61
+ await expect(button).toHaveCSS("display", "block");
62
+ });
63
+
64
+ test("should fetch new cocktail when button is clicked", async ({ page }) => {
65
+ await page.goto("/examples/npm-packages/runtime/random_cocktail/index.html?env=DEV");
66
+ await page.waitForTimeout(3000);
67
+
68
+ // Wait for initial cocktail to load
69
+ await page.waitForTimeout(5000);
70
+
71
+ // Get the initial cocktail name
72
+ const initialCocktailName = await page.locator("#app h1").textContent();
73
+
74
+ // Click "Get another cocktail" button
75
+ const button = page.locator("button");
76
+ await button.click();
77
+
78
+ // Should show loading state
79
+ await expect(page.getByText("Loading...")).toBeVisible();
80
+
81
+ // Wait for new cocktail to load
82
+ await page.waitForTimeout(5000);
83
+
84
+ // Should display new cocktail data with correct structure
85
+ await expect(page.locator("#app h1")).toBeVisible();
86
+ await expect(page.locator("#app p").last()).toBeVisible();
87
+ await expect(page.locator("img")).toBeVisible();
88
+ await expect(button).toHaveText("Get another cocktail");
89
+
90
+ // Verify we have valid cocktail data
91
+ const newCocktailName = await page.locator("#app h1").textContent();
92
+ expect(newCocktailName).toBeTruthy();
93
+ expect(newCocktailName.length).toBeGreaterThan(0);
94
+
95
+ // Verify the component maintains proper state
96
+ const hasLoading = (await page.getByText("Loading...").count()) > 0;
97
+ const hasCocktail = (await page.locator("#app h1").count()) > 0;
98
+ const hasButton = (await button.count()) > 0;
99
+ expect(hasLoading || hasCocktail || hasButton).toBe(true);
100
+ });
101
+ });