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 1 commit
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
3 changes: 3 additions & 0 deletions 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 @@ -385,6 +387,7 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping {
}
return f.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck
},
"dragAndDrop": f.DragAndDrop,
"evaluate": func(pageFunction goja.Value, gargs ...goja.Value) any {
return f.Evaluate(pageFunction.String(), exportArgs(gargs)...)
},
Expand Down
65 changes: 65 additions & 0 deletions common/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,75 @@ func (f *Frame) Click(selector string, opts *FrameClickOptions) error {
return nil
}

func (f *Frame) DragAndDrop(sourceSelector string, targetSelector string, opts goja.Value) error {
popts := FrameDragAndDropOptions{}

getPosition := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
return p, nil
}

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

act := f.newPointerAction(
sourceSelector, DOMElementStateAttached, popts.Strict, getPosition, sourceOpts,
)

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

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

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

act = f.newPointerAction(
targetSelector, DOMElementStateAttached, popts.Strict, getPosition, targetOps,
)

targetPosAny, err := call(f.ctx, act, popts.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, &MouseMoveOptions{}); err != nil {
return errorFromDOMError(err)
}

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

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

if err := f.page.Mouse.up(targetPos.X, targetPos.Y, &MouseDownUpOptions{}); 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
8 changes: 8 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
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 goja.Value) 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
64 changes: 64 additions & 0 deletions examples/dragAndDrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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) => {
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>
`);

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

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

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

page.close();
}
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
109 changes: 82 additions & 27 deletions tests/static/locators.html
Original file line number Diff line number Diff line change
@@ -1,46 +1,101 @@
<!DOCTYPE html>
<html>

<head>
<head>
<title>Clickable link test</title>
</head>
</head>

<body>
<body>
<a id="link" href="#" onclick="event.preventDefault()">Click</a>
<a id="linkdbl" href="#" ondblclick="event.preventDefault()">Dblclick</a>
<a href="#" onclick="event.preventDefault()">Click</a>
<input id="inputCheckbox" type="checkbox" />
<input type="checkbox" />
<input id="inputText" type="text" value="something" />
<input id="inputHiddenText" type="text" hidden="true">
<input id="inputHiddenText" type="text" hidden="true" />
<div id="divHello"><span>hello</span></div>
<div><span>bye</span></div>
<textarea>text area</textarea>
<p id="firstParagraph" contenteditable="true">original text</p>
<p id="secondParagraph">original text</p>
<select id="selectElement"><option value="option text"></option><option value="option text 2"></option></select>
<select id="selectElement">
<option value="option text"></option>
<option value="option text 2"></option>
</select>
<select></select>
<div id="dragSource" draggable="true">Drag me</div>
<div id="dropTarget">Drop here</div>
<script>
window.result = false;
window.dblclick = false;
window.check = false;

document.querySelector('#link').addEventListener(
'click', e => { window.result = true; }, false
);
document.querySelector('#linkdbl').addEventListener(
'dblclick', e => { window.dblclick = true; }, false
);
document.querySelector('#inputCheckbox').addEventListener(
'change', e => { window.check = e.currentTarget.checked; }, false
);
document.querySelector('#inputText').addEventListener(
'mousemove', e => { window.result = true; }, false
);
document.querySelector('#inputText').addEventListener(
'touchstart', e => { window.result = true; }, false
);
</script>
</body>
window.result = false;
window.dblclick = false;
window.check = false;

window.drag = false;
window.dragend = false;
window.dragenter = false;
window.dragleave = false;
window.dragover = false;
window.drop = false;

document.querySelector("#link").addEventListener("click", (e) => {
window.result = true;
});

document.querySelector("#linkdbl").addEventListener("dblclick", (e) => {
window.dblclick = true;
});

document
.querySelector("#inputCheckbox")
.addEventListener("change", (e) => {
window.check = e.currentTarget.checked;
});

document
.querySelector("#inputText")
.addEventListener("mousemove", (e) => {
window.result = true;
});

document
.querySelector("#inputText")
.addEventListener("touchstart", (e) => {
window.result = true;
});

document.getElementById("dragSource").addEventListener("drag", (e) => {
window.drag = true;

e.dataTransfer.setData("text", "some data");
});

document.getElementById("dragSource").addEventListener("dragend", (e) => {
window.dragend = true;
});

document
.getElementById("dragTarget")
.addEventListener("dragenter", (e) => {
window.dragenter = true;
});

document
.getElementById("dragTarget")
.addEventListener("dragleave", (e) => {
window.dragleave = true;
});

document
.getElementById("dragTarget")
.addEventListener("dragover", (e) => {
// We need to prevent default for drop event to fire.
e.preventDefault();

window.dragover = true;
});

document.getElementById("dragTarget").addEventListener("drop", (e) => {
window.drop = true;
});
</script>
</body>
</html>
Loading