Skip to content

Commit 8f8e8b9

Browse files
authored
feat: implement attribute, class list, dataset, and style views for H… (#19)
* feat: implement attribute, class list, dataset, and style views for HTMLElement * feat: reorder type definitions for clarity in elementMapView * feat: refactor HTMLElement to use dedicated attribute, class, dataset, and style handlers * feat: refactor HTMLElement to use provider methods for attributes, styles, and wait targets * feat: enhance HTMLElement to support attributes, styles, classes, and dataset manipulation * fix: normalize html view assignment keys
1 parent 2e3db25 commit 8f8e8b9

46 files changed

Lines changed: 2208 additions & 281 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/web/article/lib/helpers.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,12 @@ func sourceFromElement(ctx context.Context, el htmldrivers.HTMLElement) (core.So
9292
}
9393

9494
func snapshotElementHTML(ctx context.Context, el htmldrivers.HTMLElement) (string, error) {
95-
innerHTML, err := el.GetInnerHTML(ctx)
95+
content, err := htmldrivers.ToContentTarget(el)
96+
if err != nil {
97+
return "", err
98+
}
99+
100+
innerHTML, err := content.GetInnerHTML(ctx)
96101
if err != nil {
97102
return "", err
98103
}
@@ -107,7 +112,12 @@ func snapshotElementHTML(ctx context.Context, el htmldrivers.HTMLElement) (strin
107112
return innerHTML.String(), nil
108113
}
109114

110-
attrs, err := el.GetAttributes(ctx)
115+
attributes, err := htmldrivers.ToAttributeTarget(el)
116+
if err != nil {
117+
return "", err
118+
}
119+
120+
attrs, err := attributes.GetAttributes(ctx)
111121
if err != nil {
112122
return "", err
113123
}

modules/web/html/README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ RETURN sections[* RETURN {
283283

284284
## Reading And Mutating DOM Content
285285

286-
HTML page, document, and element values expose a read-only dot-access surface for convenient reads. Use explicit module functions for mutations.
286+
HTML page, document, and element values expose a dot-access surface for convenient reads. Static/memory-backed values are read-only through dot access. CDP-backed elements also support a small write-through assignment surface.
287287

288288
```fql
289289
LET page = DOCUMENT($url)
@@ -307,10 +307,10 @@ Common readable properties include:
307307
| --- | --- |
308308
| `HTMLPage` | `response`, `mainFrame`, `document`, `frames`, `url`, `URL`, `cookies`, `title`, `isClosed`, plus document properties through the main frame. |
309309
| `HTMLDocument` | `url`, `URL`, `name`, `title`, `parent`, `body`, `head`, `innerHTML`, `innerText`, plus node properties. |
310-
| `HTMLElement` | `innerText`, `innerHTML`, `value`, `attributes`, `style`, `previousElementSibling`, `nextElementSibling`, `parentElement`, plus node properties. |
310+
| `HTMLElement` | `innerText`, `innerHTML`, `textContent`, `value`, `checked` (CDP), `disabled` (CDP), `selected` (CDP), `attributes`, `style`, `classes` (CDP), `dataset` (CDP), `previousElementSibling`, `nextElementSibling`, `parentElement`, plus node properties. |
311311
| HTML node values | integer child indexes, `nodeType`, `nodeName`, `children`, `length`. |
312312

313-
Dot access does not write back to HTML values. Use the mutation module functions instead:
313+
Use the mutation module functions for driver-portable writes:
314314

315315
```fql
316316
LET page = DOCUMENT($url, { driver: "cdp" })
@@ -324,6 +324,27 @@ STYLE_SET(preview, "display", "block")
324324
RETURN preview.innerHTML
325325
```
326326

327+
CDP-backed elements can also be mutated with normal assignment. Top-level assignment supports content/value properties plus `attributes`, `style`, `classes`, and `dataset`; nested assignment writes through snapshot views returned by those collection properties. A captured view keeps its read snapshot while writes through that view update the browser:
328+
329+
```fql
330+
LET page = DOCUMENT($url, { driver: "cdp" })
331+
LET button = ELEMENT(page, "button[type=submit]")
332+
333+
button.textContent = "Continue"
334+
button.innerHTML = "<strong>Continue</strong>"
335+
button.disabled = FALSE
336+
button.attributes = { "aria-label": "Continue" }
337+
button.attributes["data-state"] = "ready"
338+
button.style.display = "block"
339+
button.classes.active = TRUE
340+
button.dataset.productId = "123"
341+
342+
button.attributes["data-state"] = NONE
343+
button.style.display = NONE
344+
button.classes.active = FALSE
345+
button.dataset.productId = NONE
346+
```
347+
327348
Attribute and style module functions can work with individual names or maps depending on the function:
328349

329350
```fql

modules/web/html/drivers/capabilities_test.go

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"testing"
99

1010
"github.com/PuerkitoBio/goquery"
11+
cdpruntime "github.com/mafredri/cdp/protocol/runtime"
12+
"github.com/rs/zerolog"
1113

1214
"github.com/MontFerret/contrib/modules/web/html/drivers"
1315
"github.com/MontFerret/contrib/modules/web/html/drivers/cdp"
@@ -61,12 +63,9 @@ var (
6163
_ drivers.NodeInspector = (*cdpdom.HTMLElement)(nil)
6264
_ drivers.QueryTarget = (*cdpdom.HTMLElement)(nil)
6365
_ drivers.ContentTarget = (*cdpdom.HTMLElement)(nil)
64-
_ drivers.AttributeTarget = (*cdpdom.HTMLElement)(nil)
65-
_ drivers.StyleTarget = (*cdpdom.HTMLElement)(nil)
6666
_ drivers.ValueTarget = (*cdpdom.HTMLElement)(nil)
6767
_ drivers.RelationTarget = (*cdpdom.HTMLElement)(nil)
6868
_ drivers.InteractionTarget = (*cdpdom.HTMLElement)(nil)
69-
_ drivers.WaitTarget = (*cdpdom.HTMLElement)(nil)
7069
_ runtime.Dispatchable = (*cdpdom.HTMLElement)(nil)
7170
)
7271

@@ -168,12 +167,9 @@ func TestBackendCapabilityMatrix(t *testing.T) {
168167
"NodeInspector": true,
169168
"QueryTarget": true,
170169
"ContentTarget": true,
171-
"AttributeTarget": true,
172-
"StyleTarget": true,
173170
"ValueTarget": true,
174171
"RelationTarget": true,
175172
"InteractionTarget": true,
176-
"WaitTarget": true,
177173
},
178174
},
179175
}
@@ -200,6 +196,87 @@ func TestBackendCapabilityMatrix(t *testing.T) {
200196
}
201197
}
202198

199+
func TestCDPElementCapabilityResolversUseProviders(t *testing.T) {
200+
t.Parallel()
201+
202+
element := cdpdom.NewHTMLElement(zerolog.Nop(), nil, nil, nil, nil, cdpruntime.RemoteObjectID("node"))
203+
204+
if reflect.TypeOf((*cdpdom.HTMLElement)(nil)).Implements(reflect.TypeOf((*drivers.AttributeTarget)(nil)).Elem()) {
205+
t.Fatal("expected cdp element to stop directly implementing AttributeTarget")
206+
}
207+
208+
if reflect.TypeOf((*cdpdom.HTMLElement)(nil)).Implements(reflect.TypeOf((*drivers.StyleTarget)(nil)).Elem()) {
209+
t.Fatal("expected cdp element to stop directly implementing StyleTarget")
210+
}
211+
212+
if reflect.TypeOf((*cdpdom.HTMLElement)(nil)).Implements(reflect.TypeOf((*drivers.WaitTarget)(nil)).Elem()) {
213+
t.Fatal("expected cdp element to stop directly implementing WaitTarget")
214+
}
215+
216+
if _, err := drivers.ToAttributeTarget(element); err != nil {
217+
t.Fatalf("expected cdp element attribute provider: %v", err)
218+
}
219+
220+
if _, err := drivers.ToStyleTarget(element); err != nil {
221+
t.Fatalf("expected cdp element style provider: %v", err)
222+
}
223+
224+
if _, err := drivers.ToWaitTarget(element); err != nil {
225+
t.Fatalf("expected cdp element wait provider: %v", err)
226+
}
227+
}
228+
229+
func TestCapabilityResolversUseProviderMethods(t *testing.T) {
230+
t.Parallel()
231+
232+
base := newMemoryDocument(t, `<html><body><button id="cta">go</button></body></html>`).GetElement()
233+
attrs, err := drivers.ToAttributeTarget(base)
234+
if err != nil {
235+
t.Fatalf("base attribute target: %v", err)
236+
}
237+
238+
styles, err := drivers.ToStyleTarget(base)
239+
if err != nil {
240+
t.Fatalf("base style target: %v", err)
241+
}
242+
243+
wait := &providerWaitTarget{}
244+
element := &providerElement{
245+
HTMLNode: base,
246+
attrs: attrs,
247+
styles: styles,
248+
wait: wait,
249+
}
250+
251+
if reflect.TypeOf(element).Implements(reflect.TypeOf((*drivers.AttributeTarget)(nil)).Elem()) {
252+
t.Fatal("provider element should not directly implement AttributeTarget")
253+
}
254+
255+
resolvedAttrs, err := drivers.ToAttributeTarget(element)
256+
if err != nil {
257+
t.Fatalf("resolve attribute provider: %v", err)
258+
}
259+
if resolvedAttrs != attrs {
260+
t.Fatal("expected attribute resolver to return provider target")
261+
}
262+
263+
resolvedStyles, err := drivers.ToStyleTarget(element)
264+
if err != nil {
265+
t.Fatalf("resolve style provider: %v", err)
266+
}
267+
if resolvedStyles != styles {
268+
t.Fatal("expected style resolver to return provider target")
269+
}
270+
271+
resolvedWait, err := drivers.ToWaitTarget(element)
272+
if err != nil {
273+
t.Fatalf("resolve wait provider: %v", err)
274+
}
275+
if resolvedWait != wait {
276+
t.Fatal("expected wait resolver to return provider target")
277+
}
278+
}
279+
203280
func TestRoleResolversAreExact(t *testing.T) {
204281
t.Parallel()
205282

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package drivers_test
2+
3+
import "github.com/MontFerret/contrib/modules/web/html/drivers"
4+
5+
type providerElement struct {
6+
drivers.HTMLNode
7+
attrs drivers.AttributeTarget
8+
styles drivers.StyleTarget
9+
wait drivers.WaitTarget
10+
}
11+
12+
func (el *providerElement) AsAttributeTarget() drivers.AttributeTarget {
13+
return el.attrs
14+
}
15+
16+
func (el *providerElement) AsStyleTarget() drivers.StyleTarget {
17+
return el.styles
18+
}
19+
20+
func (el *providerElement) AsWaitTarget() drivers.WaitTarget {
21+
return el.wait
22+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package drivers_test
2+
3+
import (
4+
"context"
5+
6+
"github.com/MontFerret/contrib/modules/web/html/drivers"
7+
"github.com/MontFerret/ferret/v2/pkg/runtime"
8+
)
9+
10+
type providerWaitTarget struct{}
11+
12+
func (target *providerWaitTarget) WaitForElement(context.Context, drivers.QuerySelector, drivers.WaitEvent) error {
13+
return nil
14+
}
15+
16+
func (target *providerWaitTarget) WaitForElementAll(context.Context, drivers.QuerySelector, drivers.WaitEvent) error {
17+
return nil
18+
}
19+
20+
func (target *providerWaitTarget) WaitForAttribute(context.Context, runtime.String, runtime.Value, drivers.WaitEvent) error {
21+
return nil
22+
}
23+
24+
func (target *providerWaitTarget) WaitForAttributeBySelector(context.Context, drivers.QuerySelector, runtime.String, runtime.Value, drivers.WaitEvent) error {
25+
return nil
26+
}
27+
28+
func (target *providerWaitTarget) WaitForAttributeBySelectorAll(context.Context, drivers.QuerySelector, runtime.String, runtime.Value, drivers.WaitEvent) error {
29+
return nil
30+
}
31+
32+
func (target *providerWaitTarget) WaitForStyle(context.Context, runtime.String, runtime.Value, drivers.WaitEvent) error {
33+
return nil
34+
}
35+
36+
func (target *providerWaitTarget) WaitForStyleBySelector(context.Context, drivers.QuerySelector, runtime.String, runtime.Value, drivers.WaitEvent) error {
37+
return nil
38+
}
39+
40+
func (target *providerWaitTarget) WaitForStyleBySelectorAll(context.Context, drivers.QuerySelector, runtime.String, runtime.Value, drivers.WaitEvent) error {
41+
return nil
42+
}
43+
44+
func (target *providerWaitTarget) WaitForClass(context.Context, runtime.String, drivers.WaitEvent) error {
45+
return nil
46+
}
47+
48+
func (target *providerWaitTarget) WaitForClassBySelector(context.Context, drivers.QuerySelector, runtime.String, drivers.WaitEvent) error {
49+
return nil
50+
}
51+
52+
func (target *providerWaitTarget) WaitForClassBySelectorAll(context.Context, drivers.QuerySelector, runtime.String, drivers.WaitEvent) error {
53+
return nil
54+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dom
2+
3+
import (
4+
"context"
5+
6+
"github.com/MontFerret/ferret/v2/pkg/runtime"
7+
)
8+
9+
type attributeView struct {
10+
*elementMapView
11+
}
12+
13+
func newAttributeView(ctx context.Context, attrs *elementAttributes) (*attributeView, error) {
14+
snapshot, err := attrs.GetAttributes(ctx)
15+
if err != nil {
16+
return nil, err
17+
}
18+
19+
return &attributeView{
20+
elementMapView: newElementMapView(
21+
snapshot,
22+
func(ctx context.Context, key, value runtime.Value) (runtime.Value, bool, error) {
23+
name := runtime.ToString(key)
24+
if value == runtime.None {
25+
return runtime.None, true, attrs.RemoveAttribute(ctx, name)
26+
}
27+
28+
next := runtime.ToString(value)
29+
30+
return next, false, attrs.SetAttribute(ctx, name, next)
31+
},
32+
func(ctx context.Context, key runtime.Value) error {
33+
return attrs.RemoveAttribute(ctx, runtime.ToString(key))
34+
},
35+
),
36+
}, nil
37+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dom
2+
3+
import (
4+
"context"
5+
6+
"github.com/MontFerret/ferret/v2/pkg/runtime"
7+
)
8+
9+
type classListView struct {
10+
*elementMapView
11+
}
12+
13+
func newClassListView(ctx context.Context, classes *elementClasses) (*classListView, error) {
14+
snapshot, err := classes.GetClassList(ctx)
15+
if err != nil {
16+
return nil, err
17+
}
18+
19+
return &classListView{
20+
elementMapView: newElementMapView(
21+
snapshot,
22+
func(ctx context.Context, key, value runtime.Value) (runtime.Value, bool, error) {
23+
enabled, err := runtime.CastBoolean(value)
24+
if err != nil {
25+
return runtime.None, false, err
26+
}
27+
28+
name := runtime.ToString(key)
29+
if err := classes.SetClass(ctx, name, enabled); err != nil {
30+
return runtime.None, false, err
31+
}
32+
33+
if !enabled {
34+
return runtime.None, true, nil
35+
}
36+
37+
return runtime.True, false, nil
38+
},
39+
func(ctx context.Context, key runtime.Value) error {
40+
return classes.SetClass(ctx, runtime.ToString(key), runtime.False)
41+
},
42+
),
43+
}, nil
44+
}

0 commit comments

Comments
 (0)