-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add frameLocator
, frameLocator.locator
and locator.contentFrame
#5075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2fcae61
5f7972d
a1ec95a
e9c3bad
221ef76
fb9c30a
7823e26
d15b7e8
d6ed0da
dc30dc5
2e6d56d
70a7770
fb5c07e
98a5604
69cd17e
42f33dd
abc7bad
03a93c2
da98d4d
fd7396d
199f0b2
c916673
dedb00f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package browser | ||
|
||
import ( | ||
"github.com/grafana/sobek" | ||
"go.k6.io/k6/internal/js/modules/k6/browser/common" | ||
) | ||
|
||
// mapFrameLocator API to the JS module. | ||
func mapFrameLocator(vu moduleVU, fl *common.FrameLocator) mapping { | ||
rt := vu.Runtime() | ||
return mapping{ | ||
"locator": func(selector string) *sobek.Object { | ||
ml := mapLocator(vu, fl.Locator(selector)) | ||
return rt.ToValue(ml).ToObject(rt) | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -708,13 +708,68 @@ func (h *ElementHandle) waitForElementState( | |
"waiting for states %v of element %q", states, reflect.TypeOf(result)) | ||
} | ||
|
||
// stepIntoFrame steps into an iframe/frame. Due to CORS, we need to perform this | ||
// step outside of the browser (chromium). It returns the frame that it has stepped | ||
// into and the selector to use within that frame. | ||
func (h *ElementHandle) stepIntoFrame( | ||
apiCtx context.Context, parsedSelector *Selector, frameNavIndex int, opts *FrameWaitForSelectorOptions, | ||
) (*Frame, string, error) { | ||
// Split selector at frame navigation boundary | ||
beforeFrame, afterFrame := h.splitSelectorAtFrame(parsedSelector, frameNavIndex) | ||
|
||
// Find the iframe element using the "before frame" selector | ||
iframeSelector := h.reconstructSelector(beforeFrame) | ||
|
||
iframeHandle, err := h.waitForSelector(apiCtx, iframeSelector, opts) | ||
if err != nil { | ||
return nil, "", fmt.Errorf("finding iframe with selector %q: %w", iframeSelector, err) | ||
} | ||
|
||
// This is a valid response from waitForSelector. It means that the element | ||
// was either hidden or detached. | ||
if iframeHandle == nil { | ||
return nil, "", errors.New("check if element is visible") | ||
} | ||
|
||
frame, err := iframeHandle.ContentFrame() | ||
if err != nil { | ||
return nil, "", fmt.Errorf("getting iframe frame: %w", err) | ||
} | ||
|
||
// Wait for selector in the iframe using the "after frame" selector | ||
afterFrameSelector := h.reconstructSelector(afterFrame) | ||
|
||
return frame, afterFrameSelector, nil | ||
} | ||
|
||
func (h *ElementHandle) waitForSelector( | ||
apiCtx context.Context, selector string, opts *FrameWaitForSelectorOptions, | ||
) (*ElementHandle, error) { | ||
parsedSelector, err := NewSelector(selector) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Check for frame navigation in the selector | ||
frameNavIndex := h.findFrameNavigationIndex(parsedSelector) | ||
if frameNavIndex != -1 { | ||
// Strict is true because we assume the user is interested in the element | ||
// in the frame and not the frame it is in. | ||
opts := &FrameWaitForSelectorOptions{ | ||
State: DOMElementStateAttached, | ||
Timeout: opts.Timeout, | ||
Strict: true, | ||
} | ||
|
||
frame, afterFrameSelector, err := h.stepIntoFrame(apiCtx, parsedSelector, frameNavIndex, opts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
Comment on lines
+754
to
+767
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about extracting this to another function as it's used a lot throughout the file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I was hoping to do a better job at abstracting this. I managed to abstract most of the core logic into I won't completely disregard this, and think it through again. Let me know if you spot anything obvious though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had an idea but when trying to implement it, it's too complicated and doesn't help with readability. |
||
|
||
return frame.waitForSelector(afterFrameSelector, opts) | ||
} | ||
|
||
// No frame navigation - proceed with normal waitForSelector logic | ||
fn := ` | ||
(node, injected, selector, strict, state, timeout, ...args) => { | ||
return injected.waitForSelector(selector, node, strict, state, 'raf', timeout, ...args); | ||
|
@@ -736,6 +791,8 @@ func (h *ElementHandle) waitForSelector( | |
case *ElementHandle: | ||
return r, nil | ||
default: | ||
// This is a valid response, which means that the element was either | ||
// hidden or detached. | ||
return nil, nil //nolint:nilnil | ||
} | ||
} | ||
|
@@ -746,6 +803,26 @@ func (h *ElementHandle) count(apiCtx context.Context, selector string) (int, err | |
return 0, err | ||
} | ||
|
||
// Check for frame navigation in the selector | ||
frameNavIndex := h.findFrameNavigationIndex(parsedSelector) | ||
ankur22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if frameNavIndex != -1 { | ||
// Strict is true because we assume the user is interested in the element | ||
// in the frame and not the frame it is in. | ||
opts := &FrameWaitForSelectorOptions{ | ||
State: DOMElementStateAttached, | ||
Timeout: h.frame.defaultTimeout(), | ||
Strict: true, | ||
} | ||
|
||
frame, afterFrameSelector, err := h.stepIntoFrame(apiCtx, parsedSelector, frameNavIndex, opts) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
return frame.count(afterFrameSelector) | ||
} | ||
|
||
// No frame navigation - proceed with normal count logic | ||
fn := ` | ||
(node, injected, selector) => { | ||
return injected.count(selector, node); | ||
|
@@ -773,6 +850,51 @@ func (h *ElementHandle) count(apiCtx context.Context, selector string) (int, err | |
} | ||
} | ||
|
||
// findFrameNavigationIndex finds the index of the first internal:control=enter-frame directive | ||
func (h *ElementHandle) findFrameNavigationIndex(selector *Selector) int { | ||
for i, part := range selector.Parts { | ||
if part.Name == "internal:control" && part.Body == "enter-frame" { | ||
return i | ||
} | ||
} | ||
return -1 | ||
} | ||
|
||
// splitSelectorAtFrame splits a selector at the frame navigation boundary | ||
func (h *ElementHandle) splitSelectorAtFrame(selector *Selector, frameIndex int) (*Selector, *Selector) { | ||
beforeFrame := &Selector{ | ||
Selector: selector.Selector, // Keep original for reference | ||
Parts: selector.Parts[:frameIndex], | ||
Capture: selector.Capture, | ||
} | ||
|
||
afterFrame := &Selector{ | ||
Selector: selector.Selector, // Keep original for reference | ||
Parts: selector.Parts[frameIndex+1:], | ||
Capture: selector.Capture, | ||
} | ||
|
||
return beforeFrame, afterFrame | ||
} | ||
|
||
// reconstructSelector rebuilds a selector string from selector parts | ||
func (h *ElementHandle) reconstructSelector(selector *Selector) string { | ||
if len(selector.Parts) == 0 { | ||
return "" | ||
} | ||
|
||
parts := make([]string, len(selector.Parts)) | ||
for i, part := range selector.Parts { | ||
if part.Name == "css" { | ||
parts[i] = part.Body | ||
} else { | ||
parts[i] = part.Name + "=" + part.Body | ||
} | ||
} | ||
|
||
return strings.Join(parts, " >> ") | ||
} | ||
|
||
// AsElement returns this element handle. | ||
func (h *ElementHandle) AsElement() *ElementHandle { | ||
return h | ||
|
@@ -1151,6 +1273,27 @@ func (h *ElementHandle) Query(selector string, strict bool) (_ *ElementHandle, r | |
if err != nil { | ||
return nil, fmt.Errorf("parsing selector %q: %w", selector, err) | ||
} | ||
|
||
// Check for frame navigation in the selector | ||
frameNavIndex := h.findFrameNavigationIndex(parsedSelector) | ||
ankur22 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be added to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did originally consider it but decided against it. The reason for not wanting to add to I added it to |
||
if frameNavIndex != -1 { | ||
// Strict is true because we assume the user is interested in the element | ||
// in the frame and not the frame it is in. | ||
opts := &FrameWaitForSelectorOptions{ | ||
State: DOMElementStateAttached, | ||
Timeout: h.frame.defaultTimeout(), | ||
Strict: true, | ||
} | ||
|
||
frame, afterFrameSelector, err := h.stepIntoFrame(h.ctx, parsedSelector, frameNavIndex, opts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return frame.Query(afterFrameSelector, strict) | ||
} | ||
|
||
// No frame navigation - proceed with normal Query logic | ||
querySelector := ` | ||
(node, injected, selector, strict) => { | ||
return injected.querySelector(selector, strict, node || document); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package common | ||
|
||
import ( | ||
"context" | ||
|
||
"go.k6.io/k6/internal/js/modules/k6/browser/log" | ||
) | ||
|
||
// FrameLocator represent a way to find element(s) in an iframe. | ||
type FrameLocator struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's OK, but I'd expect to find this in the same There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is that? It's a new type, which does work like a locator, but it's use case is specific to when working with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
selector string | ||
|
||
frame *Frame | ||
|
||
ctx context.Context | ||
log *log.Logger | ||
} | ||
|
||
// NewFrameLocator creates and returns a new frame locator. | ||
func NewFrameLocator(ctx context.Context, selector string, f *Frame, l *log.Logger) *FrameLocator { | ||
return &FrameLocator{ | ||
selector: selector, | ||
frame: f, | ||
ctx: ctx, | ||
log: l, | ||
} | ||
} | ||
|
||
// Locator creates and returns a new locator chained/relative to the current FrameLocator. | ||
func (fl *FrameLocator) Locator(selector string) *Locator { | ||
// Add frame navigation marker to indicate we need to enter the frame's contentDocument | ||
frameNavSelector := fl.selector + " >> internal:control=enter-frame >> " + selector | ||
return NewLocator(fl.ctx, nil, frameNavSelector, fl.frame, fl.log) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Use new visible error from #5111