rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3package check
4
5import (
6 "context"
7 "net/http"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/chromedp/chromedp"
13
14 "amadan.net/rastrillo/fiesta"
15 "amadan.net/rastrillo/fiesta/example"
16 "amadan.net/rastrillo/fiesta/internal/harness"
17)
18
19// The seam between datetime.js and calendar.js is one name on the
20// window, fiestaCalendar, looked up at enhance time. Nothing breaks
21// loudly if it moves: the button quietly falls back to the browser's
22// own picker, which is the control the overlay replaced. This drive
23// holds the rename end to end, and driveCalendarOpen is its body,
24// standalone so the release build's tests can run it against the
25// stripped scripts on a rig of their own built from calendarPages.
26
27// calendarPages is the drive's two pages: "/" is field-date's default
28// state, from the manifest, on the shared fixture page, which links
29// calendar.js and datetime.js; "/nocal" is the same page with
30// calendar.js's tag left out, so the button has only the native
31// picker to fall back to.
32func calendarPages(t *testing.T) map[string]string {
33 t.Helper()
34 m, err := example.Load(fiesta.FS())
35 if err != nil {
36 t.Fatal(err)
37 }
38 state, ok := m.Default("field-date")
39 if !ok {
40 t.Fatal("the manifest has no field-date example")
41 }
42 markup, err := example.Markup(fiesta.FS(), "field-date", state)
43 if err != nil {
44 t.Fatal(err)
45 }
46 if !strings.Contains(markup, "data-rst-date") {
47 t.Fatalf("field-date's default state %q is not an armed date field:\n%s", state, markup)
48 }
49 with := sizingDoc("calendar", markup)
50 const tag = `<script defer src="/calendar.js"></script>`
51 without := strings.Replace(with, tag, "", 1)
52 if without == with {
53 t.Fatalf("the fixture page does not link %s; the fallback leg has nothing to leave out", tag)
54 }
55 return map[string]string{"/": with, "/nocal": without}
56}
57
58// calendarOpening is what the page says once the button is pressed.
59type calendarOpening struct {
60 Seam bool // window.fiestaCalendar is a function
61 Haspopup string // the button's aria-haspopup, "" for none
62 Dialogs int // [role=dialog] elements on the page
63 Grids int // [role=grid] inside the dialog
64 Cells int // [role=gridcell] inside the dialog
65 Heads int // [role=columnheader] inside the dialog
66}
67
68const calendarOpeningJS = `JSON.stringify({
69 Seam: typeof window.fiestaCalendar === "function",
70 Haspopup: document.querySelector('[rst-dtp-pick]')?.getAttribute('aria-haspopup') ?? '',
71 Dialogs: document.querySelectorAll('[role="dialog"]').length,
72 Grids: document.querySelectorAll('[role="dialog"] [role="grid"]').length,
73 Cells: document.querySelectorAll('[role="dialog"] [role="gridcell"]').length,
74 Heads: document.querySelectorAll('[role="dialog"] [role="columnheader"]').length
75})`
76
77// driveCalendarOpen is TestTheCalendarOpensThroughFiestaCalendar's
78// body, on whatever rig serves calendarPages: with calendar.js on the
79// page, pressing the calendar button opens a dialog holding a month
80// grid of forty-two days under seven column headers; with it left
81// out, the button is the plain fallback to the native picker, with no
82// popup of its own and no dialog on the page after a press.
83func driveCalendarOpen(t *testing.T, rig *harness.Rig) {
84 t.Helper()
85 ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second)
86 defer cancel()
87
88 mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible(`input[role="combobox"]`, chromedp.ByQuery))
89 var before calendarOpening
90 at(t, ctx, calendarOpeningJS, &before)
91 if !before.Seam {
92 t.Fatal("calendar.js is on the page, but window.fiestaCalendar is not a function; datetime.js has no grid to ask for")
93 }
94 if before.Haspopup != "dialog" {
95 t.Errorf("with calendar.js, the button's aria-haspopup is %q, want dialog", before.Haspopup)
96 }
97 mustRun(t, ctx, chromedp.Click(`[rst-dtp-pick]`, chromedp.ByQuery),
98 chromedp.WaitVisible(`[role="dialog"] [role="gridcell"]`, chromedp.ByQuery))
99 var opened calendarOpening
100 at(t, ctx, calendarOpeningJS, &opened)
101 if opened.Dialogs != 1 || opened.Grids != 1 || opened.Cells != 42 || opened.Heads != 7 {
102 t.Errorf("after the press: %d dialogs, %d grids, %d cells, %d headers; want 1, 1, 42, 7", opened.Dialogs, opened.Grids, opened.Cells, opened.Heads)
103 }
104 rig.Screen("body", "the calendar open through fiestaCalendar")
105
106 mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/nocal"), chromedp.WaitVisible(`input[role="combobox"]`, chromedp.ByQuery))
107 var fallback calendarOpening
108 at(t, ctx, calendarOpeningJS, &fallback)
109 if fallback.Seam {
110 t.Fatal("/nocal still has window.fiestaCalendar; the fallback leg is not running without calendar.js")
111 }
112 if fallback.Haspopup != "" {
113 t.Errorf("without calendar.js, the button's aria-haspopup is %q, want none: it hands over to the browser's own panel", fallback.Haspopup)
114 }
115 // The press reaches native.showPicker(), the browser's own panel,
116 // and that is the only thing it reaches: a counting spy stands in
117 // for the method (the handler calls it synchronously, so there is
118 // nothing to wait for), and the page draws no dialog of its own. A
119 // handler that returned early would leave the count at zero.
120 mustRun(t, ctx, chromedp.Evaluate(`(() => { window.__fiestaPicked = 0; HTMLInputElement.prototype.showPicker = function () { window.__fiestaPicked++; }; return true; })()`, nil))
121 mustRun(t, ctx, chromedp.Click(`[rst-dtp-pick]`, chromedp.ByQuery))
122 var picked int
123 mustRun(t, ctx, chromedp.Evaluate(`window.__fiestaPicked`, &picked))
124 if picked != 1 {
125 t.Errorf("without calendar.js, the press called showPicker %d times; want once, the fallback to the native picker", picked)
126 }
127 var pressed calendarOpening
128 at(t, ctx, calendarOpeningJS, &pressed)
129 if pressed.Dialogs != 0 || pressed.Cells != 0 {
130 t.Errorf("without calendar.js, the press drew %d dialogs with %d cells; want none", pressed.Dialogs, pressed.Cells)
131 }
132}
133
134// TestTheCalendarOpensThroughFiestaCalendar: see driveCalendarOpen.
135func TestTheCalendarOpensThroughFiestaCalendar(t *testing.T) {
136 pages := calendarPages(t)
137 rig := harness.New(t, func(string) http.Handler { return sizingMux(t, pages) })
138 driveCalendarOpen(t, rig)
139}
140