//go:build browser // A modal dialog owns the keyboard. package slidestest import ( "fmt" "strings" "testing" "time" "github.com/chromedp/cdproto/input" "github.com/chromedp/chromedp" "github.com/chromedp/chromedp/kb" ) // modKey presses key with a modifier held, the way a person does: // keydown and keyup through the browser's own input pipeline, so the // event starts at the focused element and travels the page. func modKey(key string, mods input.Modifier) chromedp.Action { return chromedp.KeyEvent(key, chromedp.KeyModifiers(mods)) } // TestBrowserModalOwnsTheKeyboard (Sheets' #39 finding 1, checked here): // with the link dialog open, the editor's document-level shortcuts — // undo, redo, duplicate slide, new slide — do nothing. Before the fix // Ctrl+D duplicated the slide behind the dialog and Ctrl+Z undid an // edit nobody could see. func TestBrowserModalOwnsTheKeyboard(t *testing.T) { rig, _, _ := openEditor(t) const heading = `[slides-stage] [data-block="heading"]` const saved = `document.querySelector('[estilo-save-status]')?.dataset.state === 'saved'` // Something to undo, saved. rig.Run( chromedp.Click(heading, chromedp.ByQuery), chromedp.SendKeys(heading, string(kb.End)+" undoable", chromedp.ByQuery), waitFor(`document.querySelector('`+heading+`').textContent.includes(' undoable')`), chromedp.Sleep(600*time.Millisecond), waitFor(saved), ) var slides int var title string rig.Run( chromedp.Evaluate(`document.querySelectorAll('[slides-strip-item]').length`, &slides), chromedp.Evaluate(`document.querySelector('`+heading+`').textContent`, &title), ) // Count every update the tab sends from here on. rig.Run(chromedp.Evaluate(`(() => { window.__updatePosts = 0 const f = window.fetch window.fetch = (u, o) => { if (String(u).endsWith('/updates') && o && o.method === 'POST') window.__updatePosts++ return f(u, o) } const b = navigator.sendBeacon.bind(navigator) navigator.sendBeacon = (u, d) => { if (String(u).endsWith('/updates')) window.__updatePosts++; return b(u, d) } })()`, nil)) // Open the link dialog the way a person does: Mod+K in the heading. var mac bool rig.Run(chromedp.Evaluate(`/Mac|iP(hone|[oa]d)/.test(navigator.platform)`, &mac)) mod := input.ModifierCtrl if mac { mod = input.ModifierMeta } rig.Run( chromedp.Focus(heading, chromedp.ByQuery), modKey("k", mod), waitFor(`document.querySelector('[slides-link-dialog]').open`), waitFor(`document.activeElement?.closest('[slides-link-dialog]') !== null`), ) rig.Screen(`body`, "the link dialog is open") for _, m := range []input.Modifier{input.ModifierCtrl, input.ModifierMeta} { rig.Run( modKey("z", m), modKey("z", m|input.ModifierShift), modKey("y", m), modKey("d", m), ) } // Typing in the dialog's own field still works: stopping the // event's propagation is not preventing its default. rig.Run(chromedp.SendKeys(`[slides-link-dialog] input[name="href"]`, "https://example.com", chromedp.ByQuery)) rig.Run(chromedp.Sleep(1500 * time.Millisecond)) var posts, slidesAfter int var titleAfter, field string var open bool rig.Run( chromedp.Evaluate(`window.__updatePosts`, &posts), chromedp.Evaluate(`document.querySelectorAll('[slides-strip-item]').length`, &slidesAfter), chromedp.Evaluate(`document.querySelector('`+heading+`').textContent`, &titleAfter), chromedp.Evaluate(`document.querySelector('[slides-link-dialog]').open`, &open), chromedp.Evaluate(`document.querySelector('[slides-link-dialog] input[name="href"]').value`, &field), ) if posts != 0 { t.Errorf("with the link dialog open, shortcuts sent %d update(s) behind it, want 0", posts) } if slidesAfter != slides { t.Errorf("with the link dialog open, the strip went from %d slides to %d", slides, slidesAfter) } if titleAfter != title { t.Errorf("with the link dialog open, the heading went from %q to %q", title, titleAfter) } if !open { t.Error("a shortcut closed the link dialog") } if !strings.Contains(field, "example.com") { t.Errorf("typing in the dialog's field did not reach it: %q", field) } // Escape still closes it, and the shortcuts are live again after. rig.Run( chromedp.KeyEvent(kb.Escape), waitFor(`!document.querySelector('[slides-link-dialog]').open`), chromedp.Evaluate(`document.activeElement.blur()`, nil), modKey("d", input.ModifierCtrl), waitFor(fmt.Sprintf(`document.querySelectorAll('[slides-strip-item]').length === %d`, slides+1)), ) }