Add a Dialog an ExApp page can open for a message, confirm, or prompt.
The page waits for the choice. App navigation includes the same piece.
This commit is contained in:
+511
@@ -0,0 +1,511 @@
|
||||
package gonexapp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/chromedp/chromedp/kb"
|
||||
|
||||
"gitea.neitzel.de/konrad/go-nc-exapp"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
srv := httptest.NewServer(dialogFixture())
|
||||
defer srv.Close()
|
||||
fixtureURL = srv.URL
|
||||
|
||||
path := browserExecutable()
|
||||
if path == "" {
|
||||
fmt.Fprintln(os.Stderr, "dialog tests need Chrome, Edge, or Chromium (set EXAPP_BROWSER)")
|
||||
os.Exit(1)
|
||||
}
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.WindowSize(1200, 800),
|
||||
)
|
||||
var cancel context.CancelFunc
|
||||
allocCtx, cancel = chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
code := m.Run()
|
||||
cancel()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
var (
|
||||
fixtureURL string
|
||||
allocCtx context.Context
|
||||
)
|
||||
|
||||
func dialogFixture() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /bare", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(w, "<!DOCTYPE html><html><head><meta charset=utf-8><title>Dialog</title></head><body>",
|
||||
gonexapp.DialogHTML(), fixtureControls(), "</body></html>")
|
||||
})
|
||||
nav := gonexapp.AppNavigation{
|
||||
DefaultID: "home",
|
||||
Items: func(*http.Request) []gonexapp.Item {
|
||||
return []gonexapp.Item{{ID: "home", Label: "Home"}}
|
||||
},
|
||||
Page: func(*http.Request, string) (string, bool) {
|
||||
return fixtureControls(), true
|
||||
},
|
||||
}
|
||||
mux.Handle("GET /shell", nav.Handler())
|
||||
return mux
|
||||
}
|
||||
|
||||
func fixtureControls() string {
|
||||
return `<button type="button" id="open-info">Open info</button>
|
||||
<button type="button" id="open-warning">Open warning</button>
|
||||
<button type="button" id="open-error">Open error</button>
|
||||
<button type="button" id="open-plain">Open plain</button>
|
||||
<button type="button" id="open-confirm">Open confirm</button>
|
||||
<button type="button" id="open-delete">Open delete</button>
|
||||
<button type="button" id="open-html-button">Open html button</button>
|
||||
<button type="button" id="open-prompt">Open prompt</button>
|
||||
<button type="button" id="open-prefill">Open prefill</button>
|
||||
<button type="button" id="open-blank">Open blank</button>
|
||||
<button type="button" id="open-inner">Open inner</button>
|
||||
<button type="button" id="open-three">Open three</button>
|
||||
<pre id="log"></pre>
|
||||
<script>
|
||||
function log(line) { document.getElementById("log").textContent += line + "\n"; }
|
||||
document.getElementById("open-info").onclick = function () {
|
||||
exappDialog.message({ severity: "info", heading: "Notice", text: "Hello <b>x</b>", agree: "Nope" }).then(function () { log("dismissed"); });
|
||||
};
|
||||
document.getElementById("open-warning").onclick = function () {
|
||||
exappDialog.message({ severity: "warning", text: "Careful" }).then(function () { log("dismissed"); });
|
||||
};
|
||||
document.getElementById("open-error").onclick = function () {
|
||||
exappDialog.message({ severity: "error", heading: "<b>Nope</b>", text: "Failed" }).then(function () { log("dismissed"); });
|
||||
};
|
||||
document.getElementById("open-plain").onclick = function () {
|
||||
exappDialog.message({ severity: "info", text: "Only the message" }).then(function () { log("dismissed"); });
|
||||
};
|
||||
document.getElementById("open-confirm").onclick = function () {
|
||||
exappDialog.confirm({ text: "Proceed?" }).then(function (yes) { log(yes ? "yes" : "no"); });
|
||||
};
|
||||
document.getElementById("open-delete").onclick = function () {
|
||||
exappDialog.confirm({ heading: "Remove domain", text: "Gone", agree: "Delete", destructive: true }).then(function (yes) { log(yes ? "yes" : "no"); });
|
||||
};
|
||||
document.getElementById("open-html-button").onclick = function () {
|
||||
exappDialog.confirm({ text: "Go?", agree: "<i>Go</i>" }).then(function (yes) { log(yes ? "yes" : "no"); });
|
||||
};
|
||||
document.getElementById("open-prompt").onclick = function () {
|
||||
exappDialog.prompt({ heading: "New folder", text: "Folder name" }).then(function (value) {
|
||||
log(value === null ? "cancelled" : "value:" + JSON.stringify(value));
|
||||
});
|
||||
};
|
||||
document.getElementById("open-prefill").onclick = function () {
|
||||
exappDialog.prompt({ text: "Name", value: " hi " }).then(function (value) {
|
||||
log(value === null ? "cancelled" : "value:" + JSON.stringify(value));
|
||||
});
|
||||
};
|
||||
document.getElementById("open-blank").onclick = function () {
|
||||
exappDialog.prompt({ text: "Name", value: " " }).then(function (value) {
|
||||
log(value === null ? "cancelled" : "value:" + JSON.stringify(value));
|
||||
});
|
||||
};
|
||||
document.getElementById("open-inner").onclick = function () {
|
||||
exappDialog.prompt({ text: "Name", value: "a b" }).then(function (value) {
|
||||
log(value === null ? "cancelled" : "value:" + JSON.stringify(value));
|
||||
});
|
||||
};
|
||||
document.getElementById("open-three").onclick = function () {
|
||||
exappDialog.message({ severity: "info", text: "First" }).then(function () { log("1"); });
|
||||
exappDialog.message({ severity: "warning", text: "Second" }).then(function () { log("2"); });
|
||||
exappDialog.message({ severity: "error", text: "Third" }).then(function () { log("3"); });
|
||||
};
|
||||
</script>`
|
||||
}
|
||||
|
||||
func browserExecutable() string {
|
||||
if p := os.Getenv("EXAPP_BROWSER"); p != "" {
|
||||
return p
|
||||
}
|
||||
candidates := []string{
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func drive(t *testing.T, path string, actions ...chromedp.Action) {
|
||||
t.Helper()
|
||||
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||
t.Cleanup(cancel)
|
||||
ctx, cancel = context.WithTimeout(ctx, 25*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
steps := append([]chromedp.Action{chromedp.Navigate(fixtureURL + path)}, actions...)
|
||||
if err := chromedp.Run(ctx, steps...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func clickButton(label string) chromedp.Action {
|
||||
expr := fmt.Sprintf(`(() => {
|
||||
const btn = [...document.querySelectorAll("#exapp-dialog button")].find((b) => b.textContent.trim() === %q && !b.hidden);
|
||||
if (!btn) return "missing";
|
||||
btn.click();
|
||||
return "clicked";
|
||||
})()`, label)
|
||||
return chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var result string
|
||||
if err := chromedp.Evaluate(expr, &result).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if result != "clicked" {
|
||||
return fmt.Errorf("button %q: %s", label, result)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func waitJS(expr, want string, contains bool) chromedp.Action {
|
||||
return chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
deadline = time.Now().Add(5 * time.Second)
|
||||
}
|
||||
var last string
|
||||
for {
|
||||
var got string
|
||||
err := chromedp.Evaluate(expr, &got).Do(ctx)
|
||||
if err == nil {
|
||||
last = got
|
||||
if contains && strings.Contains(got, want) {
|
||||
return nil
|
||||
}
|
||||
if !contains && strings.TrimSpace(got) == want {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("%s = %q, want %q", expr, last, want)
|
||||
}
|
||||
if err := chromedp.Sleep(40 * time.Millisecond).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func waitDialogText(id, want string) chromedp.Action {
|
||||
return waitJS(fmt.Sprintf(`document.getElementById("%s").textContent`, id), want, false)
|
||||
}
|
||||
|
||||
func waitLog(want string) chromedp.Action {
|
||||
return waitJS(`document.getElementById("log").textContent`, want, true)
|
||||
}
|
||||
|
||||
func openShows(t *testing.T, path, button, severity, heading, message string) {
|
||||
t.Helper()
|
||||
drive(t, path,
|
||||
chromedp.WaitVisible("#"+button, chromedp.ByQuery),
|
||||
chromedp.Click("#"+button, chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-severity", severity),
|
||||
waitDialogText("exapp-dialog-message", message),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var head string
|
||||
var hidden bool
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-heading").textContent`, &head).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-heading").hidden`, &hidden).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if heading == "" {
|
||||
if !hidden {
|
||||
return fmt.Errorf("heading visible %q", head)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if hidden || strings.TrimSpace(head) != heading {
|
||||
return fmt.Errorf("heading %q hidden %v, want %q", head, hidden, heading)
|
||||
}
|
||||
var bold int
|
||||
if err := chromedp.Evaluate(`document.querySelectorAll("#exapp-dialog b, #exapp-dialog i").length`, &bold).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if bold != 0 {
|
||||
return fmt.Errorf("dialog interpreted HTML, %d elements", bold)
|
||||
}
|
||||
var label string
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").textContent`, &label).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(label) != "OK" {
|
||||
return fmt.Errorf("message button %q, want OK", label)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func TestMessageOnBarePage(t *testing.T) {
|
||||
openShows(t, "/bare", "open-info", "info", "Notice", "Hello <b>x</b>")
|
||||
}
|
||||
|
||||
func TestMessageOnAppNavigation(t *testing.T) {
|
||||
openShows(t, "/shell", "open-info", "info", "Notice", "Hello <b>x</b>")
|
||||
}
|
||||
|
||||
func TestMessageSeveritiesAndDismiss(t *testing.T) {
|
||||
cases := []struct {
|
||||
button string
|
||||
severity string
|
||||
how string
|
||||
}{
|
||||
{"open-warning", "warning", "ok"},
|
||||
{"open-error", "error", "enter"},
|
||||
{"open-info", "info", "escape"},
|
||||
{"open-plain", "info", "backdrop"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.how, func(t *testing.T) {
|
||||
var dismiss chromedp.Action
|
||||
switch tc.how {
|
||||
case "ok":
|
||||
dismiss = clickButton("OK")
|
||||
case "enter":
|
||||
dismiss = chromedp.KeyEvent(kb.Enter)
|
||||
case "escape":
|
||||
dismiss = chromedp.KeyEvent(kb.Escape)
|
||||
case "backdrop":
|
||||
dismiss = chromedp.MouseClickXY(8, 8)
|
||||
}
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#"+tc.button, chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-severity", tc.severity),
|
||||
dismiss,
|
||||
waitLog("dismissed"),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageWithoutHeading(t *testing.T) {
|
||||
openShows(t, "/bare", "open-plain", "info", "", "Only the message")
|
||||
}
|
||||
|
||||
func TestConfirmYesNoAndDestructive(t *testing.T) {
|
||||
t.Run("cancel", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-confirm", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Proceed?"),
|
||||
clickButton("Cancel"),
|
||||
waitLog("no"),
|
||||
)
|
||||
})
|
||||
t.Run("escape", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-confirm", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Proceed?"),
|
||||
chromedp.KeyEvent(kb.Escape),
|
||||
waitLog("no"),
|
||||
)
|
||||
})
|
||||
t.Run("backdrop", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-confirm", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Proceed?"),
|
||||
chromedp.MouseClickXY(8, 8),
|
||||
waitLog("no"),
|
||||
)
|
||||
})
|
||||
t.Run("enter", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-confirm", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Proceed?"),
|
||||
chromedp.KeyEvent(kb.Enter),
|
||||
waitLog("yes"),
|
||||
)
|
||||
})
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-delete", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-heading", "Remove domain"),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var plain, destructive string
|
||||
if err := chromedp.Evaluate(`getComputedStyle(document.getElementById("exapp-dialog-cancel")).backgroundColor`, &plain).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := chromedp.Evaluate(`getComputedStyle(document.getElementById("exapp-dialog-agree")).backgroundColor`, &destructive).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if plain == destructive {
|
||||
return fmt.Errorf("destructive button color %q matches Cancel", destructive)
|
||||
}
|
||||
var label string
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").textContent`, &label).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(label) != "Delete" {
|
||||
return fmt.Errorf("agree label %q", label)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
clickButton("Delete"),
|
||||
waitLog("yes"),
|
||||
)
|
||||
})
|
||||
t.Run("button word is plain text", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-html-button", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Go?"),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var n int
|
||||
var label string
|
||||
if err := chromedp.Evaluate(`document.querySelectorAll("#exapp-dialog i").length`, &n).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 0 {
|
||||
return fmt.Errorf("button word was interpreted as HTML")
|
||||
}
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").textContent`, &label).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(label) != "<i>Go</i>" {
|
||||
return fmt.Errorf("agree label %q", label)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPromptValueAndCancel(t *testing.T) {
|
||||
t.Run("empty stays inactive", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-prompt", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Folder name"),
|
||||
waitDialogText("exapp-dialog-heading", "New folder"),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var disabled bool
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").disabled`, &disabled).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if !disabled {
|
||||
return fmt.Errorf("agree button active on an empty Prompt")
|
||||
}
|
||||
return chromedp.KeyEvent(kb.Enter).Do(ctx)
|
||||
}),
|
||||
chromedp.Sleep(200*time.Millisecond),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var open bool
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog").open`, &open).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if !open {
|
||||
return fmt.Errorf("Enter closed an empty Prompt")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
clickButton("Cancel"),
|
||||
waitLog("cancelled"),
|
||||
)
|
||||
})
|
||||
t.Run("spaces only stay inactive", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-blank", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Name"),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var disabled bool
|
||||
var raw string
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").disabled`, &disabled).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-value").value`, &raw).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw != " " {
|
||||
return fmt.Errorf("starting value %q", raw)
|
||||
}
|
||||
if !disabled {
|
||||
return fmt.Errorf("agree button active for spaces")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
})
|
||||
t.Run("prefill trims on agree", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-prefill", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Name"),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var raw string
|
||||
if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-value").value`, &raw).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw != " hi " {
|
||||
return fmt.Errorf("starting value %q", raw)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
clickButton("OK"),
|
||||
waitLog(`value:"hi"`),
|
||||
)
|
||||
})
|
||||
t.Run("inner spaces stay", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-inner", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Name"),
|
||||
clickButton("OK"),
|
||||
waitLog(`value:"a b"`),
|
||||
)
|
||||
})
|
||||
t.Run("escape cancels", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-prompt", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Folder name"),
|
||||
chromedp.KeyEvent(kb.Escape),
|
||||
waitLog("cancelled"),
|
||||
)
|
||||
})
|
||||
t.Run("typed value", func(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-prompt", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "Folder name"),
|
||||
chromedp.SendKeys("#exapp-dialog-value", " mail ", chromedp.ByQuery),
|
||||
chromedp.KeyEvent(kb.Enter),
|
||||
waitLog(`value:"mail"`),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOneDialogAtATime(t *testing.T) {
|
||||
drive(t, "/bare",
|
||||
chromedp.Click("#open-three", chromedp.ByQuery),
|
||||
waitDialogText("exapp-dialog-message", "First"),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var n int
|
||||
if err := chromedp.Evaluate(`document.querySelectorAll("dialog[open]").length`, &n).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return fmt.Errorf("open dialogs %d, want 1", n)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
clickButton("OK"),
|
||||
waitDialogText("exapp-dialog-message", "Second"),
|
||||
clickButton("OK"),
|
||||
waitDialogText("exapp-dialog-message", "Third"),
|
||||
clickButton("OK"),
|
||||
waitLog("1\n2\n3"),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user