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.
- checksums.yaml +7 -0
- data/.cursor/rules/ruby_comments.mdc +29 -0
- data/.github/workflows/playwright.yml +74 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.node-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +4 -0
- data/docs/conditional-rendering.md +119 -0
- data/docs/lifecycle-hooks.md +75 -0
- data/docs/list-rendering.md +51 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +41 -0
- data/examples/Makefile +15 -0
- data/examples/npm-packages/runtime/counter/index.html +28 -0
- data/examples/npm-packages/runtime/counter/index.rb +62 -0
- data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
- data/examples/npm-packages/runtime/hello/index.html +28 -0
- data/examples/npm-packages/runtime/hello/index.rb +29 -0
- data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
- data/examples/npm-packages/runtime/input/index.html +28 -0
- data/examples/npm-packages/runtime/input/index.rb +46 -0
- data/examples/npm-packages/runtime/input/index.spec.js +58 -0
- data/examples/npm-packages/runtime/list/index.html +27 -0
- data/examples/npm-packages/runtime/list/index.rb +33 -0
- data/examples/npm-packages/runtime/list/index.spec.js +46 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
- data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
- data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
- data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
- data/examples/npm-packages/runtime/search_field/index.html +27 -0
- data/examples/npm-packages/runtime/search_field/index.rb +39 -0
- data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
- data/examples/npm-packages/runtime/todos/index.html +28 -0
- data/examples/npm-packages/runtime/todos/index.rb +239 -0
- data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
- data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
- data/examples/package.json +12 -0
- data/examples/src/counter/index.html +23 -0
- data/examples/src/counter/index.rb +60 -0
- data/lib/ruby_wasm_ui +1 -0
- data/lib/ruby_wasm_ui.rb +1 -0
- data/package-lock.json +100 -0
- data/package.json +32 -0
- data/packages/npm-packages/runtime/Gemfile +3 -0
- data/packages/npm-packages/runtime/Gemfile.lock +26 -0
- data/packages/npm-packages/runtime/README.md +5 -0
- data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
- data/packages/npm-packages/runtime/package-lock.json +6668 -0
- data/packages/npm-packages/runtime/package.json +38 -0
- data/packages/npm-packages/runtime/rollup.config.mjs +89 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/component_spec.rb +416 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/dom/scheduler_spec.rb +98 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/nodes_equal_spec.rb +190 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb +505 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_for_group_spec.rb +377 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_vdom_spec.rb +573 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/parser_spec.rb +627 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/arrays_spec.rb +228 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/objects_spec.rb +127 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/props_spec.rb +205 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/strings_spec.rb +107 -0
- data/packages/npm-packages/runtime/spec/spec_helper.rb +16 -0
- data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
- data/packages/npm-packages/runtime/src/index.js +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/app.rb +53 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/component.rb +215 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dispatcher.rb +46 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/attributes.rb +105 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/destroy_dom.rb +63 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/events.rb +40 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/mount_dom.rb +108 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/patch_dom.rb +237 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/scheduler.rb +51 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom.rb +13 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/nodes_equal.rb +45 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_conditional_group.rb +150 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_for_group.rb +125 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_vdom.rb +220 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/parser.rb +134 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/arrays.rb +185 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/objects.rb +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/props.rb +25 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/strings.rb +19 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/vdom.rb +84 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/version.rb +5 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui.rb +14 -0
- data/packages/npm-packages/runtime/vitest.config.js +8 -0
- data/playwright.config.js +78 -0
- data/sig/ruby_wasm_ui.rbs +4 -0
- 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
|
+
});
|