Skip to content
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

Implement drag and drop functionality #1290

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion browser/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func mapBrowserToGoja(vu moduleVU) *goja.Object {
// mapLocator API to the JS module.
func mapLocator(vu moduleVU, lo *common.Locator) mapping {
return mapping{
"@@locator": lo,
"clear": func(opts goja.Value) error {
ctx := vu.Context()

Expand All @@ -63,6 +64,7 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
}), nil
},
"dblclick": lo.Dblclick,
"dragTo": lo.DragTo,
"check": lo.Check,
"uncheck": lo.Uncheck,
"isChecked": lo.IsChecked,
Expand Down Expand Up @@ -94,6 +96,14 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
}
}

func parseDragAndDropOptions(ctx context.Context, opts goja.Value, defaultTimeout time.Duration) (*common.FrameDragAndDropOptions, error) {
copts := common.NewFrameDragAndDropOptions(defaultTimeout)
if err := copts.Parse(ctx, opts); err != nil {
return nil, fmt.Errorf("parsing drag and drop options: %w", err)
}
return copts, nil
}

func parseFrameClickOptions(
ctx context.Context, opts goja.Value, defaultTimeout time.Duration,
) (*common.FrameClickOptions, error) {
Expand Down Expand Up @@ -385,6 +395,17 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping {
}
return f.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck
},
"dragAndDrop": func(sourceSelector string, targetSelector string, opts goja.Value) (*goja.Promise, error) {
popts, err := parseDragAndDropOptions(vu.Context(), opts, f.Timeout())
if err != nil {
return nil, err
}

return k6ext.Promise(vu.Context(), func() (any, error) {
err := f.DragAndDrop(sourceSelector, targetSelector, popts)
return nil, err //nolint:wrapcheck
}), nil
},
"evaluate": func(pageFunction goja.Value, gargs ...goja.Value) any {
return f.Evaluate(pageFunction.String(), exportArgs(gargs)...)
},
Expand Down Expand Up @@ -577,7 +598,16 @@ func mapPage(vu moduleVU, p *common.Page) mapping {
}
return p.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck
},
"dragAndDrop": p.DragAndDrop,
"dragAndDrop": func(sourceSelector, targetSelector string, opts goja.Value) (*goja.Promise, error) {
popts, err := parseDragAndDropOptions(vu.Context(), opts, p.Timeout())
if err != nil {
return nil, err
}
return k6ext.Promise(vu.Context(), func() (any, error) {
err := p.DragAndDrop(sourceSelector, targetSelector, popts)
return nil, err //nolint:wrapcheck
}), nil
},
"emulateMedia": p.EmulateMedia,
"emulateVisionDeficiency": p.EmulateVisionDeficiency,
"evaluate": func(pageFunction goja.Value, gargs ...goja.Value) any {
Expand Down
63 changes: 63 additions & 0 deletions common/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,73 @@ func (f *Frame) Click(selector string, opts *FrameClickOptions) error {
return nil
}

func (f *Frame) DragAndDrop(sourceSelector string, targetSelector string, opts *FrameDragAndDropOptions) error {
getPosition := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
return p, nil
}

sourceOpts := &ElementHandleBasePointerOptions{
ElementHandleBaseOptions: opts.ElementHandleBaseOptions,
Position: opts.SourcePosition,
Trial: opts.Trial,
}

act := f.newPointerAction(
sourceSelector, DOMElementStateVisible, opts.Strict, getPosition, sourceOpts,
)

sourcePosAny, err := call(f.ctx, act, opts.Timeout)

if err != nil {
return errorFromDOMError(err)
}

targetOps := &ElementHandleBasePointerOptions{
ElementHandleBaseOptions: opts.ElementHandleBaseOptions,
Position: opts.SourcePosition,
Trial: opts.Trial,
}

act = f.newPointerAction(
targetSelector, DOMElementStateVisible, opts.Strict, getPosition, targetOps,
)

targetPosAny, err := call(f.ctx, act, opts.Timeout)

if err != nil {
return errorFromDOMError(err)
}

sourcePos := &Position{}
convert(sourcePosAny, sourcePos)

targetPos := &Position{}
convert(targetPosAny, targetPos)

if err := f.page.Mouse.move(sourcePos.X, sourcePos.Y, NewMouseMoveOptions()); err != nil {
return errorFromDOMError(err)
}

if err := f.page.Mouse.down(sourcePos.X, sourcePos.Y, NewMouseDownUpOptions()); err != nil {
return errorFromDOMError(err)
}

if err := f.page.Mouse.move(targetPos.X, targetPos.Y, NewMouseMoveOptions()); err != nil {
return errorFromDOMError(err)
}

if err := f.page.Mouse.up(targetPos.X, targetPos.Y, NewMouseDownUpOptions()); err != nil {
return errorFromDOMError(err)
}

return nil
}

func (f *Frame) click(selector string, opts *FrameClickOptions) error {
click := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
return nil, handle.click(p, opts.ToMouseClickOptions())
}

act := f.newPointerAction(
selector, DOMElementStateAttached, opts.Strict, click, &opts.ElementHandleBasePointerOptions,
)
Expand Down
29 changes: 29 additions & 0 deletions common/frame_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ type FrameDblclickOptions struct {
Strict bool `json:"strict"`
}

type FrameDragAndDropOptions struct {
ElementHandleBaseOptions
SourcePosition *Position `json:"sourcePosition"`
TargetPosition *Position `json:"targetPosition"`
Trial bool `json:"trial"`
Strict bool `json:"strict"`
}

type FrameFillOptions struct {
ElementHandleBaseOptions
Strict bool `json:"strict"`
Expand Down Expand Up @@ -266,6 +274,27 @@ func (o *FrameDblclickOptions) Parse(ctx context.Context, opts goja.Value) error
return nil
}

func NewFrameDragAndDropOptions(defaultTimeout time.Duration) *FrameDragAndDropOptions {
return &FrameDragAndDropOptions{
ElementHandleBaseOptions: *NewElementHandleBaseOptions(defaultTimeout),
SourcePosition: nil,
TargetPosition: nil,
Trial: false,
Strict: false,
}
}

func (o *FrameDragAndDropOptions) Parse(ctx context.Context, opts goja.Value) error {
if err := o.ElementHandleBaseOptions.Parse(ctx, opts); err != nil {
return err
}

// TODO: parse additional options

o.Strict = parseStrict(ctx, opts)
return nil
}

func NewFrameFillOptions(defaultTimeout time.Duration) *FrameFillOptions {
return &FrameFillOptions{
ElementHandleBaseOptions: *NewElementHandleBaseOptions(defaultTimeout),
Expand Down
14 changes: 14 additions & 0 deletions common/locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ func (l *Locator) dblclick(opts *FrameDblclickOptions) error {
return l.frame.dblclick(l.selector, opts)
}

func (l *Locator) DragTo(target *Locator) error {
l.log.Debugf("Locator:DragTo", "fid:%s furl:%q sel:%q target:%q", l.frame.ID(), l.frame.URL(), l.selector, target.selector)

if err := l.dragTo(target); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this just call l.frame.DragAndDrop(l.selector, ...)?

Why do we need "@@locator" in mapping.go?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this just call l.frame.DragAndDrop(l.selector, ...)?

I started out by implementing dragTo and then discovered the dragAndDrop functions on Page and Frame. I've implemented those functions, but this initial attempt hanged around. Now that those are working, I'm back to implementing dragTo on top of those functions.

Why do we need "@@locator" in mapping.go?

Since dragTo accepts a locator instance as its first argument, we need to get the selector of that other instance. I'm going to change this to be the selector and not the whole locator.

return fmt.Errorf("dragging %q to %q: %w", l.selector, target.selector, err)
}

return nil
}

func (l *Locator) dragTo(target *Locator) error {
panic("not implemented")
}

// Check on an element using locator's selector with strict mode on.
func (l *Locator) Check(opts goja.Value) {
l.log.Debugf("Locator:Check", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts)
Expand Down
11 changes: 6 additions & 5 deletions common/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,11 +723,6 @@ func (p *Page) DispatchEvent(selector string, typ string, eventInit any, opts *F
return p.MainFrame().DispatchEvent(selector, typ, eventInit, opts)
}

// DragAndDrop is not implemented.
func (p *Page) DragAndDrop(source string, target string, opts goja.Value) {
k6ext.Panic(p.ctx, "Page.DragAndDrop(source, target, opts) has not been implemented yet")
}

func (p *Page) EmulateMedia(opts goja.Value) {
p.logger.Debugf("Page:EmulateMedia", "sid:%v", p.sessionID())

Expand Down Expand Up @@ -1257,6 +1252,12 @@ func (p *Page) Type(selector string, text string, opts goja.Value) {
p.MainFrame().Type(selector, text, opts)
}

func (p *Page) DragAndDrop(sourceSelector string, targetSelector string, opts *FrameDragAndDropOptions) error {
p.logger.Debugf("Page:DragAndDrop", "sid:%v source selector:%s, target selector: %s", p.sessionID(), sourceSelector, targetSelector)

return p.MainFrame().DragAndDrop(sourceSelector, targetSelector, opts)
}

// Unroute is not implemented.
func (p *Page) Unroute(url goja.Value, handler goja.Callable) {
k6ext.Panic(p.ctx, "Page.unroute(url, handler) has not been implemented yet")
Expand Down
70 changes: 70 additions & 0 deletions examples/dragAndDrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { check } from "k6";
import { browser } from "k6/x/browser";

export const options = {
scenarios: {
ui: {
executor: "shared-iterations",
options: {
browser: {
type: "chromium",
},
},
},
},
thresholds: {
checks: ["rate==1.0"],
},
};

export default async function () {
const page = browser.newPage();

page.setContent(`
<html>
<head>
<style></style>
</head>
<body>
<div id="drag-source" draggable="true">Drag me!</div>
<div id="drop-target">Drop here!</div>

<script>
const dragSource = document.getElementById('drag-source');
const dropTarget = document.getElementById('drop-target');

dragSource.addEventListener('dragstart', (event) => {
console.log('dragstart');

event.dataTransfer.setData('text/plain', 'Something dropped!');
});

dropTarget.addEventListener('dragover', (event) => {
console.log("dragover");

event.preventDefault();
});

dropTarget.addEventListener('drop', (event) => {
console.log("drop");

event.preventDefault();
const data = event.dataTransfer.getData('text/plain');
event.target.innerText = data;
});
</script>
</body>
</html>
`);

await page.dragAndDrop("#drag-source", "#drop-target");

const dropEl = await page.waitForSelector("#drop-target");

check(dropEl, {
"source was dropped on target": (e) =>
e.innerText() === "Something dropped!",
});

page.close();
}
46 changes: 46 additions & 0 deletions tests/frame_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,49 @@ func TestFrameTitle(t *testing.T) {
p.SetContent(`<html><head><title>Some title</title></head></html>`, nil)
assert.Equal(t, "Some title", p.MainFrame().Title())
}

func TestFrameDragAndDrop(t *testing.T) {
t.Parallel()

p := newTestBrowser(t).NewPage(nil)

p.SetContent(`
<html>
<head>
<style></style>
</head>
<body>
<div id="drag-source" draggable="true">Drag me!</div>
<div id="drop-target">Drop here!</div>

<script>
const dragSource = document.getElementById('drag-source');
const dropTarget = document.getElementById('drop-target');

dragSource.addEventListener('dragstart', (event) => {
event.dataTransfer.setData('text/plain', 'Something dropped!');
});

dropTarget.addEventListener('dragover', (event) => {
event.preventDefault();
});

dropTarget.addEventListener('drop', (event) => {
event.preventDefault();
const data = event.dataTransfer.getData('text/plain');
event.target.innerText = data;
});
</script>
</body>
</html>
`, nil)

p.MainFrame().DragAndDrop("#drag-source", "#drop-target", nil)

p.MainFrame().WaitForTimeout(2000)
h, err := p.WaitForSelector("#drop-target", nil)

require.NoError(t, err)

assert.Equal(t, "Something dropped!", h.InnerText())
}
22 changes: 22 additions & 0 deletions tests/locator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ func TestLocator(t *testing.T) {
require.True(t, asBool(t, v), "cannot not double click the link")
},
},
{
"DragTo", func(tb *testBrowser, p *common.Page) {
source := p.Locator("#dragSource", nil)
target := p.Locator("#dragTarget", nil)

err := source.DragTo(target)
require.NoError(t, err)
drag := p.Evaluate(`() => window.drag`)
dragend := p.Evaluate(`() => window.dragend`)
dragover := p.Evaluate(`() => window.dragover`)
dragenter := p.Evaluate(`() => window.dragenter`)
dragleave := p.Evaluate(`() => window.dragleave`)
drop := p.Evaluate(`() => window.drop`)

require.True(t, asBool(t, drag), "cannot not drag the source")
require.True(t, asBool(t, dragend), "cannot not dragend the source")
require.True(t, asBool(t, dragover), "cannot not dragover the target")
require.True(t, asBool(t, dragenter), "cannot not dragenter the target")
require.True(t, asBool(t, dragleave), "cannot not dragleave the target")
require.True(t, asBool(t, drop), "cannot not drop the target")
},
},
{
"DispatchEvent", func(tb *testBrowser, p *common.Page) {
result := func() bool {
Expand Down
Loading
Loading