//go:build browser package check import ( "context" "net/http" "strings" "testing" "time" "github.com/chromedp/chromedp" "amadan.net/rastrillo/fiesta" "amadan.net/rastrillo/fiesta/example" "amadan.net/rastrillo/fiesta/internal/harness" ) // The seam between datetime.js and calendar.js is one name on the // window, fiestaCalendar, looked up at enhance time. Nothing breaks // loudly if it moves: the button quietly falls back to the browser's // own picker, which is the control the overlay replaced. This drive // holds the rename end to end, and driveCalendarOpen is its body, // standalone so the release build's tests can run it against the // stripped scripts on a rig of their own built from calendarPages. // calendarPages is the drive's two pages: "/" is field-date's default // state, from the manifest, on the shared fixture page, which links // calendar.js and datetime.js; "/nocal" is the same page with // calendar.js's tag left out, so the button has only the native // picker to fall back to. func calendarPages(t *testing.T) map[string]string { t.Helper() m, err := example.Load(fiesta.FS()) if err != nil { t.Fatal(err) } state, ok := m.Default("field-date") if !ok { t.Fatal("the manifest has no field-date example") } markup, err := example.Markup(fiesta.FS(), "field-date", state) if err != nil { t.Fatal(err) } if !strings.Contains(markup, "data-rst-date") { t.Fatalf("field-date's default state %q is not an armed date field:\n%s", state, markup) } with := sizingDoc("calendar", markup) const tag = `` without := strings.Replace(with, tag, "", 1) if without == with { t.Fatalf("the fixture page does not link %s; the fallback leg has nothing to leave out", tag) } return map[string]string{"/": with, "/nocal": without} } // calendarOpening is what the page says once the button is pressed. type calendarOpening struct { Seam bool // window.fiestaCalendar is a function Haspopup string // the button's aria-haspopup, "" for none Dialogs int // [role=dialog] elements on the page Grids int // [role=grid] inside the dialog Cells int // [role=gridcell] inside the dialog Heads int // [role=columnheader] inside the dialog } const calendarOpeningJS = `JSON.stringify({ Seam: typeof window.fiestaCalendar === "function", Haspopup: document.querySelector('[rst-dtp-pick]')?.getAttribute('aria-haspopup') ?? '', Dialogs: document.querySelectorAll('[role="dialog"]').length, Grids: document.querySelectorAll('[role="dialog"] [role="grid"]').length, Cells: document.querySelectorAll('[role="dialog"] [role="gridcell"]').length, Heads: document.querySelectorAll('[role="dialog"] [role="columnheader"]').length })` // driveCalendarOpen is TestTheCalendarOpensThroughFiestaCalendar's // body, on whatever rig serves calendarPages: with calendar.js on the // page, pressing the calendar button opens a dialog holding a month // grid of forty-two days under seven column headers; with it left // out, the button is the plain fallback to the native picker, with no // popup of its own and no dialog on the page after a press. func driveCalendarOpen(t *testing.T, rig *harness.Rig) { t.Helper() ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second) defer cancel() mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible(`input[role="combobox"]`, chromedp.ByQuery)) var before calendarOpening at(t, ctx, calendarOpeningJS, &before) if !before.Seam { t.Fatal("calendar.js is on the page, but window.fiestaCalendar is not a function; datetime.js has no grid to ask for") } if before.Haspopup != "dialog" { t.Errorf("with calendar.js, the button's aria-haspopup is %q, want dialog", before.Haspopup) } mustRun(t, ctx, chromedp.Click(`[rst-dtp-pick]`, chromedp.ByQuery), chromedp.WaitVisible(`[role="dialog"] [role="gridcell"]`, chromedp.ByQuery)) var opened calendarOpening at(t, ctx, calendarOpeningJS, &opened) if opened.Dialogs != 1 || opened.Grids != 1 || opened.Cells != 42 || opened.Heads != 7 { 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) } rig.Screen("body", "the calendar open through fiestaCalendar") mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/nocal"), chromedp.WaitVisible(`input[role="combobox"]`, chromedp.ByQuery)) var fallback calendarOpening at(t, ctx, calendarOpeningJS, &fallback) if fallback.Seam { t.Fatal("/nocal still has window.fiestaCalendar; the fallback leg is not running without calendar.js") } if fallback.Haspopup != "" { 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) } // The press reaches native.showPicker(), the browser's own panel, // and that is the only thing it reaches: a counting spy stands in // for the method (the handler calls it synchronously, so there is // nothing to wait for), and the page draws no dialog of its own. A // handler that returned early would leave the count at zero. mustRun(t, ctx, chromedp.Evaluate(`(() => { window.__fiestaPicked = 0; HTMLInputElement.prototype.showPicker = function () { window.__fiestaPicked++; }; return true; })()`, nil)) mustRun(t, ctx, chromedp.Click(`[rst-dtp-pick]`, chromedp.ByQuery)) var picked int mustRun(t, ctx, chromedp.Evaluate(`window.__fiestaPicked`, &picked)) if picked != 1 { t.Errorf("without calendar.js, the press called showPicker %d times; want once, the fallback to the native picker", picked) } var pressed calendarOpening at(t, ctx, calendarOpeningJS, &pressed) if pressed.Dialogs != 0 || pressed.Cells != 0 { t.Errorf("without calendar.js, the press drew %d dialogs with %d cells; want none", pressed.Dialogs, pressed.Cells) } } // TestTheCalendarOpensThroughFiestaCalendar: see driveCalendarOpen. func TestTheCalendarOpensThroughFiestaCalendar(t *testing.T) { pages := calendarPages(t) rig := harness.New(t, func(string) http.Handler { return sizingMux(t, pages) }) driveCalendarOpen(t, rig) }