obxcura 0.1.1 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2f8eab2a1520c5f96b85a50ddafee3b7938cd88014e1b288bb4d7a2839454ba
4
- data.tar.gz: 2d81b467a60b702cfeea5d90792d184f2362f9f2bf96f9a26fdfce68929be12d
3
+ metadata.gz: d0870aca291d2df42d4229d960e6382150034ee7a7294a1b75829392d0e13188
4
+ data.tar.gz: 9daacc30f29cfd803cab0135173ca1466f75f27e077ccba09f578a3b4edcc8dc
5
5
  SHA512:
6
- metadata.gz: 43e149575bda036ff64b42670f8b86147999142538cc89f459bace265ac3cef2f1feccbea1dda40adec6dbed151958d8ae8d2f40941f40155093226e8cf8232e
7
- data.tar.gz: ea2770f6a9799e45ab8d1dbbb7c79a7b62c621305831a319f182d7cae3fdc2f257f0a49be55d1757e387065bc6e52ae4d596d0165599d76f501d8ae6ebcaec99
6
+ metadata.gz: f1cb4c51f185f9898115c38743f5b83964442be4b14381f7cdb45b1bb75da1698ef8b918c053a6f9df4c5b9fed7058614e12a6bed1baf43a44813c16396e26e7
7
+ data.tar.gz: 510b721af05b438f4f72791a3e78c49e97be680912f21fd1ca59064e59f72191f27cc3a6058d86d68b97445c15d7ca65091a1bd51de0bc50da2bfb51c4567efc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-07-20
4
+
5
+ ### Added
6
+
7
+ - `Node` form actions: `value`/`attribute`, `at_css` (scoped `querySelector`),
8
+ `focus`, `type` (sets value and fires `input`/`change`, since Obscura has no
9
+ Input domain), and `submit` (raising `ProtocolError` for a node with no form).
10
+ - `Frame#to_node` is now public so `Node#at_css` reuses the same node
11
+ resolution.
12
+
3
13
  ## [0.1.1] - 2026-07-14
4
14
 
5
15
  ### Fixed
@@ -110,6 +110,7 @@ module Obxcura
110
110
  @closing = true
111
111
  @driver&.close
112
112
  @socket&.close
113
+ @subscribers.clear
113
114
  @reader&.kill
114
115
  rescue IOError, SystemCallError
115
116
  nil
@@ -46,9 +46,8 @@ module Obxcura
46
46
  .filter_map { |stub| to_node(stub) }
47
47
  end
48
48
 
49
- private
50
-
51
49
  # Resolve Obscura's serialized node stub ({"_nid"=>N,...}) into a Node.
50
+ # Public so {Node#at_css} can reuse the same resolution path.
52
51
  #
53
52
  # @param stub [Hash, nil] the serialized node from a querySelector call.
54
53
  # @return [Obxcura::Node, nil] a live node handle, or nil for a blank match.
data/lib/obxcura/node.rb CHANGED
@@ -5,19 +5,34 @@ module Obxcura
5
5
  # reads run as function calls bound to that object and reflect the current DOM.
6
6
  # Returned by Frame::DOM#at_css / #css.
7
7
  class Node
8
- # @return [String] the CDP remote objectId backing this node.
9
- attr_reader :object_id
8
+ # @return [String] the CDP remote objectId backing this node. Named
9
+ # `remote_object_id` (not `object_id`) so it doesn't shadow Ruby's
10
+ # `Object#object_id`, which every object relies on for identity.
11
+ attr_reader :remote_object_id
10
12
 
11
13
  # @param frame [Obxcura::Frame] the frame the node lives in.
12
14
  # @param object_id [String] the CDP objectId of the node.
13
15
  def initialize(frame, object_id)
14
16
  @frame = frame
15
- @object_id = object_id
17
+ @remote_object_id = object_id
16
18
  end
17
19
 
18
20
  # @return [String] the visible text of the node and its descendants.
19
21
  def text
20
- @frame.call_on(@object_id, "function() { return this.textContent; }")
22
+ @frame.call_on(remote_object_id, "function() { return this.textContent; }")
23
+ end
24
+
25
+ # @return [String, nil] the node's `value` (form controls), or nil.
26
+ def value
27
+ @frame.call_on(remote_object_id, "function() { return this.value; }")
28
+ end
29
+
30
+ # First descendant matching a CSS selector, searched within this node.
31
+ #
32
+ # @param selector [String] a CSS selector.
33
+ # @return [Obxcura::Node, nil] the matching node, or nil if none.
34
+ def at_css(selector)
35
+ @frame.to_node(@frame.call_on(remote_object_id, "function(s) { return this.querySelector(s); }", [ selector ]))
21
36
  end
22
37
 
23
38
  # Read an attribute value.
@@ -25,12 +40,58 @@ module Obxcura
25
40
  # @param name [String] the attribute name.
26
41
  # @return [String, nil] the attribute value, or nil if absent.
27
42
  def [](name)
28
- @frame.call_on(@object_id, "function(name) { return this.getAttribute(name); }", [ name ])
43
+ @frame.call_on(remote_object_id, "function(name) { return this.getAttribute(name); }", [ name ])
44
+ end
45
+ alias attribute []
46
+
47
+ # Give this node keyboard focus.
48
+ #
49
+ # @return [self]
50
+ def focus
51
+ tap { @frame.page.command("DOM.focus", objectId: remote_object_id) }
52
+ end
53
+
54
+ # Type text into this node. Obscura has no Input domain (Input.insertText /
55
+ # dispatchKeyEvent are unimplemented), so this sets `value` in page context
56
+ # and fires the `input`/`change` events real typing would, letting listeners
57
+ # react. Appends, matching keyboard behaviour when the node already has text.
58
+ #
59
+ # @param keys [Array<String>] text fragments to type (joined).
60
+ # @return [self]
61
+ def type(*keys)
62
+ tap { @frame.call_on(remote_object_id, <<~JS, [ keys.join ])
63
+ function(text) {
64
+ this.value = (this.value || "") + text;
65
+ this.dispatchEvent(new Event("input", { bubbles: true }));
66
+ this.dispatchEvent(new Event("change", { bubbles: true }));
67
+ }
68
+ JS
69
+ }
70
+ end
71
+
72
+ # Submit this node's form. Works whether the node is the `<form>` itself or a
73
+ # control inside one (resolved via `.form` / closest `<form>`). Prefers
74
+ # `requestSubmit` (runs validation and fires the submit event) and falls back
75
+ # to `submit` where it's unavailable.
76
+ #
77
+ # @return [self]
78
+ # @raise [Obxcura::ProtocolError] if the node isn't a form or inside one.
79
+ def submit
80
+ result = @frame.call_on(remote_object_id, <<~JS)
81
+ function() {
82
+ const form = this.tagName === "FORM" ? this : (this.form || this.closest("form"));
83
+ if (!form) return { error: "node is not a form and has no ancestor form" };
84
+ form.requestSubmit ? form.requestSubmit() : form.submit();
85
+ }
86
+ JS
87
+ raise ProtocolError, result["error"] if result.is_a?(Hash) && result["error"]
88
+
89
+ self
29
90
  end
30
91
 
31
92
  # @return [String] the node's serialized outer HTML.
32
93
  def outer_html
33
- @frame.call_on(@object_id, "function() { return this.outerHTML; }")
94
+ @frame.call_on(remote_object_id, "function() { return this.outerHTML; }")
34
95
  end
35
96
  end
36
97
  end
data/lib/obxcura/page.rb CHANGED
@@ -67,7 +67,7 @@ module Obxcura
67
67
  # @return [Obxcura::Page, nil] the page, removed from the browser.
68
68
  def close
69
69
  @client.unsubscribe(@session_id)
70
- @client.command("Network.clearBrowserCookies", { targetId: @target_id })
70
+ # @client.command("Network.clearBrowserCookies", { targetId: @target_id })
71
71
  @client.command("Target.closeTarget", { targetId: @target_id })
72
72
  rescue ProtocolError
73
73
  # Target already gone — nothing to do.
@@ -4,5 +4,5 @@ module Obxcura
4
4
  # The gem's semantic version string.
5
5
  #
6
6
  # @return [String]
7
- VERSION = "0.1.1"
7
+ VERSION = "0.2.0"
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: obxcura
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - memoxmrdl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-15 00:00:00.000000000 Z
11
+ date: 2026-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket-driver