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

Add setInputFiles to ElementHandle #1097

Merged
merged 8 commits into from
Mar 7, 2024
Merged
77 changes: 73 additions & 4 deletions common/element_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package common

import (
"context"
"encoding/base64"
"errors"
"fmt"
"math"
"mime"
"os"
"path/filepath"
"reflect"
"strings"
"time"
Expand Down Expand Up @@ -1238,10 +1242,74 @@ func (h *ElementHandle) SelectText(opts goja.Value) {
applySlowMo(h.ctx)
}

// SetInputFiles is not implemented.
func (h *ElementHandle) SetInputFiles(files goja.Value, opts goja.Value) {
// TODO: implement
k6ext.Panic(h.ctx, "ElementHandle.setInputFiles() has not been implemented yet")
// SetInputFiles sets the given files into the input file element.
func (h *ElementHandle) SetInputFiles(files goja.Value, opts goja.Value) error {
actionOpts := NewElementHandleSetInputFilesOptions(h.defaultTimeout())
if err := actionOpts.Parse(h.ctx, opts); err != nil {
return fmt.Errorf("parsing setInputFiles options: %w", err)
}

actionParam := &Files{}

if err := actionParam.Parse(h.ctx, files); err != nil {
return fmt.Errorf("parsing setInputFiles parameter: %w", err)
}

fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) {
return nil, handle.setInputFiles(apiCtx, actionParam.Payload)
}
actFn := h.newAction([]string{}, fn, actionOpts.Force, actionOpts.NoWaitAfter, actionOpts.Timeout)
_, err := call(h.ctx, actFn, actionOpts.Timeout)
if err != nil {
return fmt.Errorf("setting input files: %w", err)
}

return nil
}

func (h *ElementHandle) resolveFiles(payload []*File) error {
for _, file := range payload {
if strings.TrimSpace(file.Path) != "" {
buffer, err := os.ReadFile(file.Path)
if err != nil {
return fmt.Errorf("reading file: %w", err)
}
file.Buffer = base64.StdEncoding.EncodeToString(buffer)
file.Name = filepath.Base(file.Path)
file.Mimetype = mime.TypeByExtension(filepath.Ext(file.Path))
}
}

return nil
}

func (h *ElementHandle) setInputFiles(apiCtx context.Context, payload []*File) error {
fn := `
(node, injected, payload) => {
return injected.setInputFiles(node, payload);
}
`
evalOpts := evalOptions{
forceCallable: true,
returnByValue: true,
}
err := h.resolveFiles(payload)
if err != nil {
return err
}
result, err := h.evalWithScript(apiCtx, evalOpts, fn, payload)
if err != nil {
return err
}
v, ok := result.(string)
if !ok {
return fmt.Errorf("unexpected type %T", result)
}
if v != "done" {
return errorFromDOMError(v)
}

return nil
}

func (h *ElementHandle) Tap(opts goja.Value) {
Expand Down Expand Up @@ -1542,6 +1610,7 @@ func errorFromDOMError(v any) error {
"error:notfillablenumberinput": "cannot type text into input[type=number]",
"error:notvaliddate": "malformed value",
"error:notinput": "node is not an HTMLInputElement",
"error:notfile": "node is not an input[type=file] element",
"error:hasnovalue": "node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement",
"error:notselect": "element is not a <select> element",
"error:notcheckbox": "not a checkbox or radio button",
Expand Down
85 changes: 85 additions & 0 deletions common/element_handle_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package common

import (
"context"
"fmt"
"reflect"
"strings"
"time"

Expand Down Expand Up @@ -74,6 +76,24 @@ type ElementHandleHoverOptions struct {
Modifiers []string `json:"modifiers"`
}

// File is the descriptor of a single file.
type File struct {
Path string `json:"-"`
Name string `json:"name"`
Mimetype string `json:"mimeType"`
Buffer string `json:"buffer"`
}

// Files is the input parameter for ElementHandle.SetInputFiles.
type Files struct {
Payload []*File `json:"payload"`
}

// ElementHandleSetInputFilesOptions are options for ElementHandle.SetInputFiles.
type ElementHandleSetInputFilesOptions struct {
ElementHandleBaseOptions
}

type ElementHandlePressOptions struct {
Delay int64 `json:"delay"`
NoWaitAfter bool `json:"noWaitAfter"`
Expand Down Expand Up @@ -177,6 +197,71 @@ func (o *ElementHandleCheckOptions) Parse(ctx context.Context, opts goja.Value)
return o.ElementHandleBasePointerOptions.Parse(ctx, opts)
}

// NewElementHandleSetInputFilesOptions creates a new ElementHandleSetInputFilesOption.
func NewElementHandleSetInputFilesOptions(defaultTimeout time.Duration) *ElementHandleSetInputFilesOptions {
return &ElementHandleSetInputFilesOptions{
ElementHandleBaseOptions: *NewElementHandleBaseOptions(defaultTimeout),
}
}

// addFile to the struct. Input value can be a path, or a file descriptor object.
func (f *Files) addFile(ctx context.Context, file goja.Value) error {
if !gojaValueExists(file) {
return nil
}
rt := k6ext.Runtime(ctx)
fileType := file.ExportType()
switch fileType.Kind() { //nolint:exhaustive
case reflect.Map: // file descriptor object
var parsedFile File
if err := rt.ExportTo(file, &parsedFile); err != nil {
return fmt.Errorf("parsing file descriptor: %w", err)
}
f.Payload = append(f.Payload, &parsedFile)
case reflect.String: // file path
if v, ok := file.Export().(string); ok {
f.Payload = append(f.Payload, &File{Path: v})
}
ankur22 marked this conversation as resolved.
Show resolved Hide resolved
default:
return fmt.Errorf("invalid parameter type : %s", fileType.Kind().String())
}

return nil
}

// Parse parses the Files struct from the given goja.Value.
func (f *Files) Parse(ctx context.Context, files goja.Value) error {
rt := k6ext.Runtime(ctx)
if !gojaValueExists(files) {
return nil
}

optsType := files.ExportType()
switch optsType.Kind() { //nolint:exhaustive
case reflect.Slice: // array of filePaths or array of file descriptor objects
gopts := files.ToObject(rt)
for _, k := range gopts.Keys() {
err := f.addFile(ctx, gopts.Get(k))
if err != nil {
return err
}
}
default: // filePath or file descriptor object
return f.addFile(ctx, files)
}

return nil
}

// Parse parses the ElementHandleSetInputFilesOption from the given opts.
func (o *ElementHandleSetInputFilesOptions) Parse(ctx context.Context, opts goja.Value) error {
if err := o.ElementHandleBaseOptions.Parse(ctx, opts); err != nil {
return err
}

return nil
}

func NewElementHandleClickOptions(defaultTimeout time.Duration) *ElementHandleClickOptions {
return &ElementHandleClickOptions{
ElementHandleBasePointerOptions: *NewElementHandleBasePointerOptions(defaultTimeout),
Expand Down
42 changes: 38 additions & 4 deletions common/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -1525,10 +1525,27 @@ func (f *Frame) SetContent(html string, opts goja.Value) {
applySlowMo(f.ctx)
}

// SetInputFiles is not implemented.
func (f *Frame) SetInputFiles(selector string, files goja.Value, opts goja.Value) {
k6ext.Panic(f.ctx, "Frame.setInputFiles(selector, files, opts) has not been implemented yet")
// TODO: needs slowMo
// SetInputFiles sets input files for the selected element.
func (f *Frame) SetInputFiles(selector string, files goja.Value, opts goja.Value) error {
f.log.Debugf("Frame:SetInputFiles", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector)

popts := NewFrameSetInputFilesOptions(f.defaultTimeout())
if err := popts.Parse(f.ctx, opts); err != nil {
return fmt.Errorf("parsing setInputFiles options: %w", err)
}

pfiles := &Files{}
if err := pfiles.Parse(f.ctx, files); err != nil {
return fmt.Errorf("parsing setInputFiles parameter: %w", err)
}

if err := f.setInputFiles(selector, pfiles, popts); err != nil {
return fmt.Errorf("setting input files on %q: %w", selector, err)
}

applySlowMo(f.ctx)

return nil
}

// Tap the first element that matches the selector.
Expand All @@ -1546,6 +1563,23 @@ func (f *Frame) Tap(selector string, opts goja.Value) {
applySlowMo(f.ctx)
}

func (f *Frame) setInputFiles(selector string, files *Files, opts *FrameSetInputFilesOptions) error {
setInputFiles := func(apiCtx context.Context, handle *ElementHandle) (any, error) {
return nil, handle.setInputFiles(apiCtx, files.Payload)
}
act := f.newAction(
selector, DOMElementStateAttached, opts.Strict,
setInputFiles, []string{},
opts.Force, opts.NoWaitAfter, opts.Timeout,
)

if _, err := call(f.ctx, act, opts.Timeout); err != nil {
return errorFromDOMError(err)
}

return nil
}

func (f *Frame) tap(selector string, opts *FrameTapOptions) error {
tap := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
return nil, handle.tap(apiCtx, p)
Expand Down
22 changes: 22 additions & 0 deletions common/frame_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ type FrameSetContentOptions struct {
WaitUntil LifecycleEvent `json:"waitUntil" js:"waitUntil"`
}

// FrameSetInputFilesOptions are options for Frame.setInputFiles.
type FrameSetInputFilesOptions struct {
ElementHandleSetInputFilesOptions
Strict bool `json:"strict"`
}

type FrameTapOptions struct {
ElementHandleBasePointerOptions
Modifiers []string `json:"modifiers"`
Expand Down Expand Up @@ -486,6 +492,22 @@ func (o *FrameSetContentOptions) Parse(ctx context.Context, opts goja.Value) err
return nil
}

// NewFrameSetInputFilesOptions creates a new FrameSetInputFilesOptions.
func NewFrameSetInputFilesOptions(defaultTimeout time.Duration) *FrameSetInputFilesOptions {
return &FrameSetInputFilesOptions{
ElementHandleSetInputFilesOptions: *NewElementHandleSetInputFilesOptions(defaultTimeout),
Strict: false,
}
}

// Parse parses FrameSetInputFilesOptions from goja.Value.
func (o *FrameSetInputFilesOptions) Parse(ctx context.Context, opts goja.Value) error {
if err := o.ElementHandleSetInputFilesOptions.Parse(ctx, opts); err != nil {
return err
}
return nil
}

func NewFrameTapOptions(defaultTimeout time.Duration) *FrameTapOptions {
return &FrameTapOptions{
ElementHandleBasePointerOptions: *NewElementHandleBasePointerOptions(defaultTimeout),
Expand Down
24 changes: 24 additions & 0 deletions common/js/injected_script.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,30 @@ class InjectedScript {
node.dispatchEvent(event);
}

setInputFiles(node, payloads) {
if (node.nodeType !== Node.ELEMENT_NODE)
return "error:notelement";
bandorko marked this conversation as resolved.
Show resolved Hide resolved
if (node.nodeName.toLowerCase() !== "input")
return 'error:notinput';
const type = (node.getAttribute('type') || '').toLowerCase();
if (type !== 'file')
return 'error:notfile';

const dt = new DataTransfer();
if (payloads) {
const files = payloads.map(file => {
const bytes = Uint8Array.from(atob(file.buffer), c => c.charCodeAt(0));
return new File([bytes], file.name, { type: file.mimeType, lastModified: file.lastModifiedMs });
});
for (const file of files)
dt.items.add(file);
}
node.files = dt.files;
node.dispatchEvent(new Event('input', { 'bubbles': true }));
node.dispatchEvent(new Event('change', { 'bubbles': true }));
return "done";
}

getElementBorderWidth(node) {
if (
node.nodeType !== 1 /*Node.ELEMENT_NODE*/ ||
Expand Down
9 changes: 5 additions & 4 deletions common/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -1169,10 +1169,11 @@ func (p *Page) SetExtraHTTPHeaders(headers map[string]string) {
p.updateExtraHTTPHeaders()
}

// SetInputFiles is not implemented.
func (p *Page) SetInputFiles(selector string, files goja.Value, opts goja.Value) {
k6ext.Panic(p.ctx, "Page.textContent(selector, opts) has not been implemented yet")
// TODO: needs slowMo
// SetInputFiles sets input files for the selected element.
func (p *Page) SetInputFiles(selector string, files goja.Value, opts goja.Value) error {
p.logger.Debugf("Page:SetInputFiles", "sid:%v selector:%s", p.sessionID(), selector)

return p.MainFrame().SetInputFiles(selector, files, opts)
}

// SetViewportSize will update the viewport width and height.
Expand Down
Loading
Loading