//go:build browser package calendartest import ( "context" "fmt" "io" "log/slog" "net/http" "path/filepath" "testing" "time" "amadan.net/rastrillo/rastrillo/harness" "github.com/chromedp/cdproto/emulation" "github.com/chromedp/chromedp" calendar "amadan.net/oficina/calendar/internal/calendar" ) // TestFormFootOnANarrowScreen: rastrillo's form-foot draws its two // buttons at the lg size (v0.26). On a phone the booker's pair, "Hold // this time" and "Pick another time", no longer fit side by side, and the // row squeezed them instead of wrapping: two lines of label each at 390 // wide, up to three at 320 (where the old size already broke them over // two). calendar.css lets that row wrap. This checks // at 320 and 390 that no form-foot label breaks across lines and no // button leaves the form, on the stranger's booking form, the one // most often opened on a phone. func TestFormFootOnANarrowScreen(t *testing.T) { var app *calendar.App rig := harness.New(t, func(origin string) http.Handler { a, err := calendar.New(calendar.Config{ DBPath: filepath.Join(t.TempDir(), "calendar.db"), Env: "development", Origin: origin, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatalf("calendar.New: %v", err) } t.Cleanup(func() { a.Close() }) app = a return a.Handler() }) signInOverHTTP(t, app, rig.Origin, "host@example.com") ctx := context.Background() st := app.StoreForTest() host := app.PersonByAddressForTest(t, "host@example.com") if err := st.SaveHandle(ctx, host.ID, "paul"); err != nil { t.Fatal(err) } ws, _ := calendar.ParseAvailability("every day 9-5") page, err := st.CreateBookingPage(ctx, host.ID, calendar.BookingPageInput{ Title: "Quick chat", Description: "Half an hour.", Availability: "every day 9-5", DurationMin: 30, HorizonDays: 30}, ws) if err != nil { t.Fatal(err) } if err := st.PublishBookingPage(ctx, host.ID, page.ID, true); err != nil { t.Fatal(err) } public := "/book/paul/" + page.Code for _, width := range []int64{320, 390} { var day string var problems []string ctx, cancel := context.WithTimeout(rig.Context(), 20*time.Second) err := chromedp.Run(ctx, emulation.SetDeviceMetricsOverride(width, 844, 1, true), chromedp.Navigate(rig.Origin+public), chromedp.WaitVisible(`select[name="zone"]`, chromedp.ByQuery), chromedp.Evaluate(`(document.querySelector('a[href*="day="]') || {}).href || ''`, &day), ) if err == nil && day == "" { err = fmt.Errorf("the booking page offers no day to pick") } if err == nil { err = chromedp.Run(ctx, chromedp.Navigate(day), chromedp.WaitVisible(`[cal-book-slot]`, chromedp.ByQuery), chromedp.Click(`[cal-book-slot]`, chromedp.ByQuery), chromedp.WaitVisible(`[cal-book-details] [rst-form-foot]`, chromedp.ByQuery), chromedp.Evaluate(footProblems, &problems), ) } cancel() if err != nil { t.Fatalf("%dpx: %v", width, err) } for _, p := range problems { t.Errorf("%dpx, the booker's form: %s", width, p) } } } // footProblems lists every form-foot button whose label breaks across // lines, or which reaches past its form's edge. const footProblems = `[...document.querySelectorAll('[rst-form-foot] [rst-btn]')].flatMap(b => { const out = [], r = document.createRange() r.selectNodeContents(b) const lines = new Set([...r.getClientRects()].filter(x => x.width > 0).map(x => Math.round(x.top))).size if (lines > 1) out.push('"' + b.textContent.trim() + '" breaks over ' + lines + ' lines') const f = b.closest('form').getBoundingClientRect(), bb = b.getBoundingClientRect() if (bb.right > f.right + 0.5 || bb.left < f.left - 0.5) out.push('"' + b.textContent.trim() + '" spans ' + Math.round(bb.left) + '-' + Math.round(bb.right) + ', its form ' + Math.round(f.left) + '-' + Math.round(f.right)) return out })`