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,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ruby-wasm-ui",
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/ruby-wasm-ui.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/ruby-wasm-ui.js",
|
|
8
|
+
"dist/ruby_wasm_ui.rb",
|
|
9
|
+
"dist/ruby_wasm_ui/**/*.rb"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepack": "npm run build",
|
|
13
|
+
"build": "NODE_ENV=production rollup -c",
|
|
14
|
+
"build:dev": "NODE_ENV=development rollup -c",
|
|
15
|
+
"lint": "eslint src",
|
|
16
|
+
"lint:fix": "eslint src --fix",
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"test:run": "vitest run"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ruby",
|
|
22
|
+
"wasm",
|
|
23
|
+
"ui"
|
|
24
|
+
],
|
|
25
|
+
"author": "t0yohei <k.t0yohei@gmail.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
30
|
+
"eslint": "^9.25.1",
|
|
31
|
+
"jsdom": "^26.1.0",
|
|
32
|
+
"rollup": "^4.40.0",
|
|
33
|
+
"rollup-plugin-cleanup": "^3.2.1",
|
|
34
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
35
|
+
"rollup-plugin-filesize": "^10.0.0",
|
|
36
|
+
"vitest": "^3.1.2"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import copy from "rollup-plugin-copy";
|
|
2
|
+
import cleanup from "rollup-plugin-cleanup";
|
|
3
|
+
import filesize from "rollup-plugin-filesize";
|
|
4
|
+
import replace from "@rollup/plugin-replace";
|
|
5
|
+
import { glob } from "glob";
|
|
6
|
+
import process from "process";
|
|
7
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
// Find all Ruby files in ruby_wasm_ui directory
|
|
14
|
+
const rubyFiles = glob
|
|
15
|
+
.sync("src/ruby_wasm_ui/**/*.rb")
|
|
16
|
+
.map((file) => file.replace("src/", ""))
|
|
17
|
+
.sort((a, b) => {
|
|
18
|
+
// Files in root directory should be loaded first
|
|
19
|
+
const aIsRoot = !a.includes("/");
|
|
20
|
+
const bIsRoot = !b.includes("/");
|
|
21
|
+
if (aIsRoot && !bIsRoot) return -1;
|
|
22
|
+
if (!aIsRoot && bIsRoot) return 1;
|
|
23
|
+
|
|
24
|
+
// Then sort by directory and filename
|
|
25
|
+
return a.localeCompare(b);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Determine environment based on NODE_ENV
|
|
29
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
30
|
+
|
|
31
|
+
// Plugin to remove require_relative lines from Ruby files
|
|
32
|
+
const removeRequireRelative = () => {
|
|
33
|
+
return {
|
|
34
|
+
name: "remove-require-relative",
|
|
35
|
+
writeBundle() {
|
|
36
|
+
// Process all Ruby files in dist directory recursively
|
|
37
|
+
const distRubyFiles = glob.sync("dist/**/*.rb");
|
|
38
|
+
|
|
39
|
+
distRubyFiles.forEach((file) => {
|
|
40
|
+
const filePath = join(__dirname, file);
|
|
41
|
+
let content = readFileSync(filePath, "utf-8");
|
|
42
|
+
// Remove lines that start with require_relative (with optional whitespace)
|
|
43
|
+
content = content.replace(/^\s*require_relative\s+.*$/gm, "");
|
|
44
|
+
// Remove multiple consecutive empty lines
|
|
45
|
+
content = content.replace(/\n{3,}/g, "\n\n");
|
|
46
|
+
writeFileSync(filePath, content, "utf-8");
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default {
|
|
53
|
+
input: "src/index.js",
|
|
54
|
+
output: {
|
|
55
|
+
file: "dist/ruby-wasm-ui.js",
|
|
56
|
+
format: "esm",
|
|
57
|
+
sourcemap: true,
|
|
58
|
+
},
|
|
59
|
+
plugins: [
|
|
60
|
+
replace({
|
|
61
|
+
preventAssignment: true,
|
|
62
|
+
values: {
|
|
63
|
+
"window.RUBY_WASM_UI_ENV": JSON.stringify(
|
|
64
|
+
isDevelopment ? "development" : "production"
|
|
65
|
+
),
|
|
66
|
+
"window.RUBY_WASM_UI_FILES": JSON.stringify(rubyFiles),
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
copy({
|
|
70
|
+
targets: [
|
|
71
|
+
{
|
|
72
|
+
src: "src/ruby_wasm_ui/**/*",
|
|
73
|
+
dest: "dist/ruby_wasm_ui",
|
|
74
|
+
flatten: false,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
src: "src/ruby_wasm_ui.rb",
|
|
78
|
+
dest: "dist",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
cleanup({
|
|
83
|
+
comments: "none",
|
|
84
|
+
extensions: ["js"],
|
|
85
|
+
}),
|
|
86
|
+
removeRequireRelative(),
|
|
87
|
+
filesize(),
|
|
88
|
+
],
|
|
89
|
+
};
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyWasmUi do
|
|
6
|
+
describe '.define_component' do
|
|
7
|
+
let(:template) { ->(component) { 'rendered content' } }
|
|
8
|
+
let(:state) { ->(props) { { count: 0 } } }
|
|
9
|
+
|
|
10
|
+
context 'with template proc' do
|
|
11
|
+
it 'works with template proc that accepts component argument' do
|
|
12
|
+
component_class = RubyWasmUi.define_component(
|
|
13
|
+
template: ->(component) { 'rendered with component' }
|
|
14
|
+
)
|
|
15
|
+
instance = component_class.new
|
|
16
|
+
expect(instance.template).to eq('rendered with component')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'works with template proc that accepts no arguments' do
|
|
20
|
+
component_class = RubyWasmUi.define_component(
|
|
21
|
+
template: -> { 'rendered without args' }
|
|
22
|
+
)
|
|
23
|
+
instance = component_class.new
|
|
24
|
+
expect(instance.template).to eq('rendered without args')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'can access component state and props in template proc without arguments' do
|
|
28
|
+
component_class = RubyWasmUi.define_component(
|
|
29
|
+
template: -> { "count: #{@state[:count]}, name: #{@props[:name]}" },
|
|
30
|
+
state: -> { { count: 5 } }
|
|
31
|
+
)
|
|
32
|
+
instance = component_class.new(name: 'test')
|
|
33
|
+
expect(instance.template).to eq('count: 5, name: test')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'can access component methods in template proc without arguments' do
|
|
37
|
+
component_class = RubyWasmUi.define_component(
|
|
38
|
+
template: -> { helper_method },
|
|
39
|
+
methods: {
|
|
40
|
+
helper_method: -> { 'helper result' }
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
instance = component_class.new
|
|
44
|
+
expect(instance.template).to eq('helper result')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'passes correct component instance when template proc has arguments' do
|
|
48
|
+
received_component = nil
|
|
49
|
+
component_class = RubyWasmUi.define_component(
|
|
50
|
+
template: ->(component) {
|
|
51
|
+
received_component = component
|
|
52
|
+
'rendered'
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
instance = component_class.new
|
|
56
|
+
instance.template
|
|
57
|
+
expect(received_component).to eq(instance)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'handles template proc with variable arity correctly' do
|
|
61
|
+
# Test with proc that can accept 0 or more arguments
|
|
62
|
+
component_class = RubyWasmUi.define_component(
|
|
63
|
+
template: ->(*args) { "args count: #{args.length}" }
|
|
64
|
+
)
|
|
65
|
+
instance = component_class.new
|
|
66
|
+
# Variable arity procs (arity < 0) should be called with component argument
|
|
67
|
+
expect(instance.template).to eq('args count: 1')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'works with Vdom.h in template proc without arguments' do
|
|
71
|
+
component_class = RubyWasmUi.define_component(
|
|
72
|
+
template: -> {
|
|
73
|
+
RubyWasmUi::Vdom.h('div', {}, ["Hello #{@props[:name]}"])
|
|
74
|
+
},
|
|
75
|
+
state: -> { { count: 0 } }
|
|
76
|
+
)
|
|
77
|
+
instance = component_class.new(name: 'World')
|
|
78
|
+
result = instance.template
|
|
79
|
+
expect(result).to be_a(RubyWasmUi::Vdom)
|
|
80
|
+
expect(result.tag).to eq('div')
|
|
81
|
+
expect(result.children.length).to eq(1)
|
|
82
|
+
expect(result.children.first).to be_a(RubyWasmUi::Vdom)
|
|
83
|
+
expect(result.children.first.type).to eq('text')
|
|
84
|
+
expect(result.children.first.value).to eq('Hello World')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'works with Vdom.h in template proc with component argument' do
|
|
88
|
+
component_class = RubyWasmUi.define_component(
|
|
89
|
+
template: ->(component) {
|
|
90
|
+
RubyWasmUi::Vdom.h('div', {}, ["Count: #{component.state[:count]}"])
|
|
91
|
+
},
|
|
92
|
+
state: -> { { count: 42 } }
|
|
93
|
+
)
|
|
94
|
+
instance = component_class.new
|
|
95
|
+
result = instance.template
|
|
96
|
+
expect(result).to be_a(RubyWasmUi::Vdom)
|
|
97
|
+
expect(result.tag).to eq('div')
|
|
98
|
+
expect(result.children.length).to eq(1)
|
|
99
|
+
expect(result.children.first).to be_a(RubyWasmUi::Vdom)
|
|
100
|
+
expect(result.children.first.type).to eq('text')
|
|
101
|
+
expect(result.children.first.value).to eq('Count: 42')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context 'with state proc' do
|
|
106
|
+
it 'works with state proc that accepts props argument' do
|
|
107
|
+
component_class = RubyWasmUi.define_component(
|
|
108
|
+
template: -> { 'content' },
|
|
109
|
+
state: ->(props) { { value: props[:initial] } }
|
|
110
|
+
)
|
|
111
|
+
instance = component_class.new(initial: 5)
|
|
112
|
+
expect(instance.state).to eq({ value: 5 })
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'works with state proc that accepts no arguments' do
|
|
116
|
+
component_class = RubyWasmUi.define_component(
|
|
117
|
+
template: -> { 'content' },
|
|
118
|
+
state: -> { { value: 10 } }
|
|
119
|
+
)
|
|
120
|
+
instance = component_class.new
|
|
121
|
+
expect(instance.state).to eq({ value: 10 })
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'with methods parameter' do
|
|
126
|
+
it 'successfully adds custom methods to the component' do
|
|
127
|
+
custom_methods = {
|
|
128
|
+
increment: -> { @state[:count] += 1 },
|
|
129
|
+
get_double_count: -> { @state[:count] * 2 }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
component_class = RubyWasmUi.define_component(
|
|
133
|
+
template:,
|
|
134
|
+
state:,
|
|
135
|
+
methods: custom_methods
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
instance = component_class.new
|
|
139
|
+
|
|
140
|
+
expect(instance).to respond_to(:increment)
|
|
141
|
+
expect(instance).to respond_to(:get_double_count)
|
|
142
|
+
|
|
143
|
+
# Test the custom methods work correctly
|
|
144
|
+
expect(instance.get_double_count).to eq(0)
|
|
145
|
+
instance.increment
|
|
146
|
+
expect(instance.state[:count]).to eq(1)
|
|
147
|
+
expect(instance.get_double_count).to eq(2)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'raises error when method name conflicts with existing method' do
|
|
151
|
+
custom_methods = {
|
|
152
|
+
template: -> { 'custom template' } # conflicts with existing template method
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
expect {
|
|
156
|
+
RubyWasmUi.define_component(
|
|
157
|
+
template:,
|
|
158
|
+
methods: custom_methods
|
|
159
|
+
)
|
|
160
|
+
}.to raise_error(/Method "template\(\)" already exists in the component\./)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'raises error when method name conflicts with private method' do
|
|
164
|
+
custom_methods = {
|
|
165
|
+
patch: -> { 'custom patch' } # conflicts with existing private method
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
expect {
|
|
169
|
+
RubyWasmUi.define_component(
|
|
170
|
+
template:,
|
|
171
|
+
methods: custom_methods
|
|
172
|
+
)
|
|
173
|
+
}.to raise_error(/Method "patch\(\)" already exists in the component\./)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'works correctly with empty methods hash' do
|
|
177
|
+
component_class = RubyWasmUi.define_component(
|
|
178
|
+
template:,
|
|
179
|
+
methods: {}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
instance = component_class.new
|
|
183
|
+
expect(instance).to be_a(RubyWasmUi::Component)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'allows method names as strings' do
|
|
187
|
+
custom_methods = {
|
|
188
|
+
'string_method' => -> { 'method called' }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
component_class = RubyWasmUi.define_component(
|
|
192
|
+
template:,
|
|
193
|
+
methods: custom_methods
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
instance = component_class.new
|
|
197
|
+
expect(instance).to respond_to('string_method')
|
|
198
|
+
expect(instance.string_method).to eq('method called')
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
context 'without methods parameter' do
|
|
203
|
+
it 'creates component successfully when methods is not provided' do
|
|
204
|
+
component_class = RubyWasmUi.define_component(template:)
|
|
205
|
+
|
|
206
|
+
instance = component_class.new
|
|
207
|
+
expect(instance).to be_a(RubyWasmUi::Component)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
context 'with on_mounted parameter' do
|
|
212
|
+
it 'works with on_mounted proc that accepts component argument' do
|
|
213
|
+
mounted_called = false
|
|
214
|
+
received_component = nil
|
|
215
|
+
|
|
216
|
+
component_class = RubyWasmUi.define_component(
|
|
217
|
+
template: -> { 'content' },
|
|
218
|
+
on_mounted: ->(component) {
|
|
219
|
+
mounted_called = true
|
|
220
|
+
received_component = component
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
instance = component_class.new
|
|
225
|
+
instance.on_mounted
|
|
226
|
+
|
|
227
|
+
expect(mounted_called).to be true
|
|
228
|
+
expect(received_component).to eq(instance)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'works with on_mounted proc that accepts no arguments' do
|
|
232
|
+
mounted_called = false
|
|
233
|
+
|
|
234
|
+
component_class = RubyWasmUi.define_component(
|
|
235
|
+
template: -> { 'content' },
|
|
236
|
+
on_mounted: -> {
|
|
237
|
+
mounted_called = true
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
instance = component_class.new
|
|
242
|
+
instance.on_mounted
|
|
243
|
+
|
|
244
|
+
expect(mounted_called).to be true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'allows calling component methods directly in on_mounted without arguments' do
|
|
248
|
+
method_called_with_self = nil
|
|
249
|
+
|
|
250
|
+
component_class = RubyWasmUi.define_component(
|
|
251
|
+
template: -> { 'content' },
|
|
252
|
+
on_mounted: -> {
|
|
253
|
+
method_called_with_self = self
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
instance = component_class.new
|
|
258
|
+
instance.on_mounted
|
|
259
|
+
|
|
260
|
+
expect(method_called_with_self).to eq(instance)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'uses default empty proc when on_mounted is not provided' do
|
|
264
|
+
component_class = RubyWasmUi.define_component(template: -> { 'content' })
|
|
265
|
+
instance = component_class.new
|
|
266
|
+
|
|
267
|
+
expect { instance.on_mounted }.not_to raise_error
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
context 'with on_unmounted parameter' do
|
|
272
|
+
it 'works with on_unmounted proc that accepts component argument' do
|
|
273
|
+
unmounted_called = false
|
|
274
|
+
received_component = nil
|
|
275
|
+
|
|
276
|
+
component_class = RubyWasmUi.define_component(
|
|
277
|
+
template: -> { 'content' },
|
|
278
|
+
on_unmounted: ->(component) {
|
|
279
|
+
unmounted_called = true
|
|
280
|
+
received_component = component
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
instance = component_class.new
|
|
285
|
+
instance.on_unmounted
|
|
286
|
+
|
|
287
|
+
expect(unmounted_called).to be true
|
|
288
|
+
expect(received_component).to eq(instance)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
it 'works with on_unmounted proc that accepts no arguments' do
|
|
292
|
+
unmounted_called = false
|
|
293
|
+
|
|
294
|
+
component_class = RubyWasmUi.define_component(
|
|
295
|
+
template: -> { 'content' },
|
|
296
|
+
on_unmounted: -> {
|
|
297
|
+
unmounted_called = true
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
instance = component_class.new
|
|
302
|
+
instance.on_unmounted
|
|
303
|
+
|
|
304
|
+
expect(unmounted_called).to be true
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'allows calling component methods directly in on_unmounted without arguments' do
|
|
308
|
+
method_called_with_self = nil
|
|
309
|
+
|
|
310
|
+
component_class = RubyWasmUi.define_component(
|
|
311
|
+
template: -> { 'content' },
|
|
312
|
+
on_unmounted: -> {
|
|
313
|
+
method_called_with_self = self
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
instance = component_class.new
|
|
318
|
+
instance.on_unmounted
|
|
319
|
+
|
|
320
|
+
expect(method_called_with_self).to eq(instance)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'uses default empty proc when on_unmounted is not provided' do
|
|
324
|
+
component_class = RubyWasmUi.define_component(template: -> { 'content' })
|
|
325
|
+
instance = component_class.new
|
|
326
|
+
|
|
327
|
+
expect { instance.on_unmounted }.not_to raise_error
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
describe RubyWasmUi::Component do
|
|
333
|
+
describe '#emit' do
|
|
334
|
+
let(:template) { -> { 'content' } }
|
|
335
|
+
let(:component_class) { RubyWasmUi.define_component(template:) }
|
|
336
|
+
let(:event_name) { 'test_event' }
|
|
337
|
+
|
|
338
|
+
context 'when dispatcher is set' do
|
|
339
|
+
it 'dispatches event with payload' do
|
|
340
|
+
component = component_class.new
|
|
341
|
+
dispatcher = component.instance_variable_get(:@dispatcher)
|
|
342
|
+
payload = { value: 42 }
|
|
343
|
+
|
|
344
|
+
expect(dispatcher).to receive(:dispatch).with(event_name, payload)
|
|
345
|
+
component.emit(event_name, payload)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it 'dispatches event without payload (nil by default)' do
|
|
349
|
+
component = component_class.new
|
|
350
|
+
dispatcher = component.instance_variable_get(:@dispatcher)
|
|
351
|
+
|
|
352
|
+
expect(dispatcher).to receive(:dispatch).with(event_name, nil)
|
|
353
|
+
component.emit(event_name)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
context 'when dispatcher is not set' do
|
|
358
|
+
it 'does not raise error' do
|
|
359
|
+
component = component_class.new
|
|
360
|
+
component.instance_variable_set(:@dispatcher, nil)
|
|
361
|
+
|
|
362
|
+
expect { component.emit(event_name) }.not_to raise_error
|
|
363
|
+
expect { component.emit(event_name, { value: 42 }) }.not_to raise_error
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
describe '#wire_event_handler' do
|
|
369
|
+
let(:template) { -> { 'content' } }
|
|
370
|
+
let(:component_class) { RubyWasmUi.define_component(template:) }
|
|
371
|
+
let(:event_name) { 'test_event' }
|
|
372
|
+
let(:parent_component) { component_class.new }
|
|
373
|
+
|
|
374
|
+
context 'with parent component' do
|
|
375
|
+
it 'handles event with payload when handler has arity of 1' do
|
|
376
|
+
handler = ->(payload) { payload[:value] * 2 }
|
|
377
|
+
component = component_class.new({}, { event_name => handler }, parent_component)
|
|
378
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
379
|
+
expect(subscription).to be_a(Proc)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it 'handles event without payload when handler has arity of 0' do
|
|
383
|
+
handler = -> { 'no payload' }
|
|
384
|
+
component = component_class.new({}, { event_name => handler }, parent_component)
|
|
385
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
386
|
+
expect(subscription).to be_a(Proc)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
context 'without parent component' do
|
|
391
|
+
it 'handles event with payload when handler has arity of 1' do
|
|
392
|
+
handler = ->(payload) { payload[:value] * 2 }
|
|
393
|
+
component = component_class.new({}, { event_name => handler })
|
|
394
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
395
|
+
expect(subscription).to be_a(Proc)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
it 'handles event without payload when handler has arity of 0' do
|
|
399
|
+
handler = -> { 'no payload' }
|
|
400
|
+
component = component_class.new({}, { event_name => handler })
|
|
401
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
402
|
+
expect(subscription).to be_a(Proc)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it 'returns a no-op unsubscription when subscription is nil' do
|
|
407
|
+
allow_any_instance_of(RubyWasmUi::Dispatcher).to receive(:subscribe).and_return(nil)
|
|
408
|
+
handler = -> { 'test' }
|
|
409
|
+
component = component_class.new({}, { event_name => handler })
|
|
410
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
411
|
+
expect(subscription).to be_a(Proc)
|
|
412
|
+
expect { subscription.call }.not_to raise_error
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyWasmUi::Dom::Scheduler do
|
|
6
|
+
let(:mock_job) { -> { 'job executed' } }
|
|
7
|
+
let(:js) { class_double('JS').as_stubbed_const }
|
|
8
|
+
let(:global) { double('JS.global') }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
allow(js).to receive(:global).and_return(global)
|
|
12
|
+
allow(global).to receive(:queueMicrotask)
|
|
13
|
+
described_class.initialize_scheduler
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '.initialize_scheduler' do
|
|
17
|
+
it 'initializes the scheduler with empty jobs and scheduled false' do
|
|
18
|
+
expect(described_class.jobs).to eq([])
|
|
19
|
+
expect(described_class.scheduled).to be false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '.enqueue_job' do
|
|
24
|
+
context 'when jobs is nil' do
|
|
25
|
+
before do
|
|
26
|
+
described_class.jobs = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'initializes scheduler and enqueues the job' do
|
|
30
|
+
described_class.enqueue_job(mock_job)
|
|
31
|
+
expect(described_class.jobs).to eq([mock_job])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context 'when jobs is already initialized' do
|
|
36
|
+
it 'enqueues the job' do
|
|
37
|
+
described_class.enqueue_job(mock_job)
|
|
38
|
+
expect(described_class.jobs).to eq([mock_job])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'schedules an update' do
|
|
43
|
+
expect(global).to receive(:queueMicrotask)
|
|
44
|
+
described_class.enqueue_job(mock_job)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '.schedule_update' do
|
|
49
|
+
context 'when not already scheduled' do
|
|
50
|
+
before do
|
|
51
|
+
described_class.scheduled = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'sets scheduled to true and queues microtask' do
|
|
55
|
+
expect(global).to receive(:queueMicrotask)
|
|
56
|
+
described_class.send(:schedule_update)
|
|
57
|
+
expect(described_class.scheduled).to be true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'when already scheduled' do
|
|
62
|
+
before do
|
|
63
|
+
described_class.scheduled = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'does not queue microtask' do
|
|
67
|
+
expect(global).not_to receive(:queueMicrotask)
|
|
68
|
+
described_class.send(:schedule_update)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '.process_jobs' do
|
|
74
|
+
let(:job1) { spy('job1') }
|
|
75
|
+
let(:job2) { spy('job2') }
|
|
76
|
+
|
|
77
|
+
before do
|
|
78
|
+
described_class.jobs = [job1, job2]
|
|
79
|
+
described_class.scheduled = true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'processes all jobs in the queue' do
|
|
83
|
+
described_class.send(:process_jobs)
|
|
84
|
+
expect(job1).to have_received(:call)
|
|
85
|
+
expect(job2).to have_received(:call)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'empties the jobs queue' do
|
|
89
|
+
described_class.send(:process_jobs)
|
|
90
|
+
expect(described_class.jobs).to be_empty
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'sets scheduled to false after processing' do
|
|
94
|
+
described_class.send(:process_jobs)
|
|
95
|
+
expect(described_class.scheduled).to be false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|