fyne-io / fyne

Cross platform GUI toolkit in Go inspired by Material Design
https://fyne.io/
Other
24.58k stars 1.37k forks source link

Testing: dialogs #2771

Open e1fueg0 opened 2 years ago

e1fueg0 commented 2 years ago

Is your feature request related to a problem? Please describe:

Let's consider a simple dialog like this:

func exampleDialog(w fyne.Window) {
    name := widget.NewEntry()
    eName := widget.NewFormItem("Name", name)
    d := dialog.NewForm("A dialog", "OK", "Cancel", []*widget.FormItem{eName}, func(b bool) {
        _ = os.WriteFile("tmp", []byte(name.Text), 0644)
    }, w)
    d.Show()
 }

How do I test full dialog cycle: 1) to fill the field with a test value, 2) to press the submit button so that the assigned function works, and 3) to check whether file with given test value has been written. And, in opposite, 1) to fill the field, 2) to press the cancel button, 3) to check the file has not been written.

Is it possible to construct a solution with the existing API?

No.

Describe the solution you'd like to see:

I did a simple kinda framework to test dialogs for my app. Hope this will help you.

Instead of calling dialog.NewForm directly I do this:

var newForm = dialog.NewForm
var newFileOpen = dialog.NewFileOpen

and afterwards I call newForm with the same arguments, as follows:

    name := widget.NewEntry()
    eName := widget.NewFormItem("Name", name)
    active := widget.NewCheck()
    eActive := widget.NewFormItem("Active", active)
    d := newForm("A dialog", "OK", "Cancel", []*widget.FormItem{eName, eActive}, func(b bool) {}, w)

A test looks like this:

    newForm = testNewForm

    assert.Equal(t, "A dialog", lastTestDialog.getTitle())
    assert.Equal(t, "OK", lastTestDialog.getConfirm())
    assert.Equal(t, "Cancel", lastTestDialog.getDismiss())

    lastTestDialog.setText(t, "Name", "some name")
    lastTestDialog.setCheck(t, "Active", true)
    lastTestDialog.tapOk()
    assert.Equal(t, false, lastTestDialog.isValid())
    // other checks

And here's the code of the implementation:

package main

import (
    "fmt"
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/dialog"
    "fyne.io/fyne/v2/storage/repository"
    "fyne.io/fyne/v2/widget"
    "testing"
)

type testDialog struct {
    title    string
    confirm  string
    dismiss  string
    widgets  map[string]fyne.CanvasObject
    callback func(bool)
    invalid  bool
}

func (d *testDialog) Show() {}

func (d *testDialog) Hide() {}

func (d *testDialog) SetDismissText(string) {}

func (d *testDialog) SetOnClosed(func()) {}

func (d *testDialog) Refresh() {}

func (d *testDialog) Resize(fyne.Size) {}

func (d *testDialog) MinSize() fyne.Size {
    return fyne.Size{}
}

func (d *testDialog) getTitle() string {
    return d.title
}

func (d *testDialog) getConfirm() string {
    return d.confirm
}

func (d *testDialog) getDismiss() string {
    return d.dismiss
}

func (d *testDialog) isValid() bool {
    return !d.invalid
}

func (d *testDialog) tapOk() {
    d.invalid = false
    for _, wi := range d.widgets {
        if w, ok := wi.(fyne.Validatable); ok {
            if e := w.Validate(); e != nil {
                d.invalid = true
                break
            }
        }
    }
    if !d.invalid {
        d.callback(true)
    }
}

func (d *testDialog) tapCancel() {
    d.callback(false)
}

func (d *testDialog) setText(t *testing.T, name string, text string) {
    wi, ok := d.widgets[name]
    if !ok {
        t.Errorf("there's no widget with name '%s'", name)
        return
    }
    e, ok := wi.(*widget.Entry)
    if ok {
        e.SetText(text)
        return
    }
    e1, ok := wi.(*widget.SelectEntry)
    if ok {
        e1.SetText(text)
        return
    }
    t.Errorf("widget '%s' isn't a text or select entry", name)
}

func (d *testDialog) setCheck(t *testing.T, name string, check bool) {
    wi, ok := d.widgets[name]
    if !ok {
        t.Errorf("there's no widget with name '%s'", name)
        return
    }
    c, ok := wi.(*widget.Check)
    if !ok {
        t.Errorf("widget '%s' isn't a check", name)
        return
    }
    c.Checked = check
}

func (d *testDialog) tapButton(t *testing.T, name string) {
    t.Helper()
    wi, ok := d.widgets[name]
    if !ok {
        t.Errorf("there's no widget with name '%s'", name)
        return
    }
    b, ok := wi.(*widget.Button)
    if !ok {
        t.Errorf("widget '%s' isn't a button", name)
        return
    }
    b.OnTapped()
}

var lastTestDialog *testDialog = nil

func testNewForm(title, confirm, dismiss string, items []*widget.FormItem, callback func(bool), _ fyne.Window) dialog.Dialog {
    widgets := make(map[string]fyne.CanvasObject)
    for _, i := range items {
        widgetsForItem := digWidgets(i.Widget)
        l := len(widgetsForItem)
        if l < 1 {
            continue
        }
        if l == 1 {
            widgets[i.Text] = widgetsForItem[0]
            continue
        }
        for x, wi := range widgetsForItem {
            widgets[fmt.Sprintf("%s-%d", i.Text, x)] = wi
        }
    }
    lastTestDialog = &testDialog{title: title, confirm: confirm, dismiss: dismiss, widgets: widgets, callback: callback}
    return lastTestDialog
}

func digWidgets(root fyne.CanvasObject) []fyne.CanvasObject {
    if cnt, ok := root.(*fyne.Container); ok {
        var widgets []fyne.CanvasObject
        for _, o := range cnt.Objects {
            widgets = append(widgets, digWidgets(o)...)
        }
        return widgets
    }
    return []fyne.CanvasObject{root}
}

type testUriFile struct {
    path string
    uri  fyne.URI
}

func (t *testUriFile) Read(_ []byte) (n int, err error) {
    return 0, nil
}

func (t *testUriFile) Close() error {
    return nil
}

func (t *testUriFile) URI() fyne.URI {
    if t.uri == nil {
        t.uri = repository.NewFileURI(t.path)
    }
    return t.uri
}

type testFileOpen struct {
    callback func(fyne.URIReadCloser, error)
    path     string
    e        error
}

func (d *testFileOpen) Show() {
    if d.path != "" {
        d.callback(&testUriFile{path: d.path}, d.e)
    } else {
        d.callback(nil, d.e)
    }
}

func (d *testFileOpen) Hide() {
}

func (d *testFileOpen) SetDismissText(string) {
}

func (d *testFileOpen) SetOnClosed(func()) {
}

func (d *testFileOpen) Refresh() {
}

func (d *testFileOpen) Resize(fyne.Size) {
}

func (d *testFileOpen) MinSize() fyne.Size {
    return fyne.Size{}
}

func testNewFileOpen(callback func(fyne.URIReadCloser, error), _ fyne.Window) dialog.Dialog {
    return &testFileOpen{callback: callback, path: "/tmp/private.key"}
}

func testNewFileOpenFailed(callback func(fyne.URIReadCloser, error), _ fyne.Window) dialog.Dialog {
    return &testFileOpen{callback: callback}
}
andydotxyz commented 2 years ago

I cannot quite decide which is the better approach here:

  1. Add some dialog helpers to the test package that manage user interaction for dialog using internal code
  2. Expose a Confirm and Dismiss functions to dialog that could then be called by developer code (and possible test helpers).

This may require a little feedback from the community. Either way it needs new APIs so adding to Bowmore release.