//go:build browser package check import ( "context" "encoding/json" "math" "regexp" "strconv" "strings" "testing" "time" "github.com/chromedp/cdproto/emulation" "github.com/chromedp/cdproto/input" "github.com/chromedp/chromedp" "amadan.net/rastrillo/fiesta/internal/harness" ) // switchJS reads one switch: its checkbox, the events it fired since the // page counted, where the thumb is drawn (the ::after's translateX, in // px, signed as the screen has it), the track's transition and // touch-action, its box (W by H, K the knob's width, Run the knob's // travel: the track's width less its height, since the knob is the // height less the 2px inset each side), the two numbers the shim // writes (P the knob's share of the travel, On the on colour's share) // and the track's painted colour. const switchJS = `((id) => { const l = document.getElementById(id), t = l.querySelector("[rst-switch-track]"), a = getComputedStyle(t, "::after"), ts = getComputedStyle(t); const m = new DOMMatrix(a.transform === "none" ? "" : a.transform), r = t.getBoundingClientRect(); return JSON.stringify({Checked: l.querySelector("input").checked, Changes: (window.changes || {})[id] || 0, Inputs: (window.inputs || {})[id] || 0, X: m.m41, Duration: ts.transitionDuration, Easing: ts.transitionTimingFunction, TouchAction: ts.touchAction, Dragging: l.hasAttribute("data-rst-drag"), Settling: l.hasAttribute("data-rst-settle"), L: r.left, R: r.right, Y: r.top + r.height / 2, W: r.width, H: r.height, K: parseFloat(a.width), Run: r.width - r.height, P: ts.getPropertyValue("--rst-switch-p").trim(), On: ts.getPropertyValue("--rst-switch-on").trim(), Bg: ts.backgroundColor}); })` type switchReading struct { Checked, Dragging, Settling bool Changes, Inputs int X, L, R, Y, W, H, K, Run float64 Duration, Easing, TouchAction string P, On, Bg string } // thumbAt is the screen x of the knob's centre at rest at the inline // start (off), 2px in and half a knob across; toward the inline end is // +x in LTR and -x in RTL. func thumbAt(s switchReading, sign float64) float64 { if sign > 0 { return s.L + 2 + s.K/2 } return s.R - 2 - s.K/2 } // read runs switchJS on the switch with this id. func read(t *testing.T, ctx context.Context, id string) switchReading { t.Helper() var raw string mustRun(t, ctx, chromedp.Evaluate(switchJS+`("`+id+`")`, &raw)) var s switchReading if err := json.Unmarshal([]byte(raw), &s); err != nil { t.Fatal(err) } return s } // TestASwitchSlidesAndSnaps: the thumb can be dragged. It follows the // pointer along the track, and on release snaps to the nearer end; the // checkbox toggles, once, with one input and one change event, only when // that end is the other state. A drag that comes back before release // changes nothing, a tap toggles once as it always has (the label's own // behaviour, so never twice), a disabled switch does not move (nor one // in a disabled fieldset), and in // RTL the track runs the other way. The thumb snaps with the CSS // transition, which reduced motion turns off; the track lets a vertical // swipe scroll the page and keeps a horizontal one for the drag. With a // mouse, and with touch. The journey is driveSwitchDrag, so another rig // serving switchDragPages can be held to the same drive. func TestASwitchSlidesAndSnaps(t *testing.T) { driveSwitchDrag(t, sizingRig(t, false, switchDragPages)) } // switchDragPages is the pair of pages driveSwitchDrag navigates to: six // switches (one to drag on, one to drag out and back, one to tap, a // disabled one, one in a disabled fieldset, one for a finger) with the // counters switchJS reads, in LTR at / and in RTL at /rtl. var switchDragPages = func() map[string]string { sw := func(id, extra string) string { return `

` } body := `
` + sw("drag", "") + sw("back", "") + sw("tap", "") + sw("off", " disabled") + `
` + sw("fieldset", "") + `
` + sw("touch", "") + `
` + `` return map[string]string{"/": sizingDoc("switch", body), "/rtl": strings.Replace(sizingDoc("switch", body), `dir="ltr"`, `dir="rtl"`, 1)} }() // driveSwitchDrag is the drag journey of TestASwitchSlidesAndSnaps on a // rig that serves switchDragPages: LTR, RTL and LTR under reduced // motion, each a subtest on the one browser. A leg leaves the browser // as it found it, touch emulation off and the emulated media cleared, // so the next leg's mouse drags read the page the leg before read. The // reset runs on a context of its own, so a leg that timed out does not // fail a second time on its reset, and a leg already red says nothing // more; only a green leg whose reset fails reports it. func driveSwitchDrag(t *testing.T, rig *harness.Rig) { t.Helper() for _, leg := range []struct { name, path string reduce bool }{{"ltr", "/", false}, {"rtl", "/rtl", false}, {"ltr reduced motion", "/", true}} { t.Run(leg.name, func(t *testing.T) { ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) defer cancel() defer func() { rctx, rcancel := context.WithTimeout(rig.Context(), 10*time.Second) defer rcancel() if err := chromedp.Run(rctx, emulation.SetTouchEmulationEnabled(false), emulation.SetEmulatedMedia()); err != nil && !t.Failed() { t.Errorf("resetting the browser after the leg: %v", err) } }() if leg.reduce { mustRun(t, ctx, emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{{Name: "prefers-reduced-motion", Value: "reduce"}})) } mustRun(t, ctx, chromedp.EmulateViewport(800, 700), chromedp.Navigate(rig.Origin+leg.path), chromedp.WaitReady("#touch", chromedp.ByQuery), chromedp.Evaluate(`new Promise(r => addEventListener("load", r, {once: true}) || document.readyState === "complete" && r())`, nil, awaitPromise)) // sign turns "toward on" into screen pixels; the thumb starts // at the track's inline start, 2px in, a knob across. Offsets // are shares of the knob's travel (Run), read from the page, // so the drive holds for any theme's size. sign := 1.0 if leg.path == "/rtl" { sign = -1 } thumb := func(s switchReading) float64 { return thumbAt(s, sign) } mouse := func(typ input.MouseType, x, y float64) chromedp.Action { return input.DispatchMouseEvent(typ, x, y).WithButton(input.Left).WithClickCount(1) } // drag presses on the thumb, moves through each offset (toward // on, as a share of the travel) and releases at the last. drag := func(id string, shares ...float64) (mid switchReading) { s := read(t, ctx, id) x := thumb(s) mustRun(t, ctx, mouse(input.MousePressed, x, s.Y)) for _, o := range shares { mustRun(t, ctx, mouse(input.MouseMoved, x+sign*o*s.Run, s.Y)) } mid = read(t, ctx, id) mustRun(t, ctx, mouse(input.MouseReleased, x+sign*shares[len(shares)-1]*s.Run, s.Y), chromedp.Sleep(350*time.Millisecond)) return mid } run := read(t, ctx, "drag").Run if s := read(t, ctx, "drag"); s.TouchAction != "pan-y" { t.Errorf("the track's touch-action is %q; want pan-y, so a vertical swipe scrolls and a horizontal one drags", s.TouchAction) } if s := read(t, ctx, "drag"); leg.reduce && s.Duration != "0s" || !leg.reduce && s.Duration == "0s" { t.Errorf("the track's transition is %s with reduced motion %v; the knob should settle with a transition, and without one under reduced motion", s.Duration, leg.reduce) } mid := drag("drag", 0.2, 0.4, 0.6, 0.8) if !mid.Dragging || math.Abs(mid.X-sign*0.8*run) > 1 { t.Errorf("80%% into a drag the thumb is at %.1f (dragging %v); it should follow the pointer to %.1f", mid.X, mid.Dragging, sign*0.8*run) } if s := read(t, ctx, "drag"); !s.Checked || s.Changes != 1 || s.Inputs != 1 || math.Abs(s.X-sign*run) > 0.5 || s.Dragging { t.Errorf("released past the middle: checked %v, %d change and %d input events, thumb at %.1f; want on, one of each, at %.1f", s.Checked, s.Changes, s.Inputs, s.X, sign*run) } drag("back", 0.25, 0.5, 0.8, 0.25, 0.1) if s := read(t, ctx, "back"); s.Checked || s.Changes != 0 || math.Abs(s.X) > 0.5 { t.Errorf("dragged out and back before release: checked %v, %d change events, thumb at %.1f; want nothing changed", s.Checked, s.Changes, s.X) } s := read(t, ctx, "tap") mustRun(t, ctx, mouse(input.MousePressed, thumb(s), s.Y), mouse(input.MouseReleased, thumb(s), s.Y), chromedp.Sleep(350*time.Millisecond)) if s := read(t, ctx, "tap"); !s.Checked || s.Changes != 1 { t.Errorf("a tap: checked %v, %d change events; want on, once", s.Checked, s.Changes) } // Disabled itself, and disabled by its fieldset, where the // checkbox's own disabled property is false but it matches // :disabled and a click does nothing. for _, id := range []string{"off", "fieldset"} { mid = drag(id, 0.25, 0.5, 0.8) if s := read(t, ctx, id); s.Checked || s.Changes != 0 || mid.Dragging || math.Abs(mid.X) > 0.5 || s.Dragging { t.Errorf("#%s, disabled, dragged: checked %v, %d change events, knob at %.1f mid-drag (dragging %v); want it not to move", id, s.Checked, s.Changes, mid.X, mid.Dragging) } } // Touch: the same drag with a finger. Read after touch is on: // a coarse pointer grows the rows, and the switch moves. mustRun(t, ctx, emulation.SetTouchEmulationEnabled(true)) s = read(t, ctx, "touch") x := thumb(s) mustRun(t, ctx, chromedp.ActionFunc(func(ctx context.Context) error { if err := input.DispatchTouchEvent(input.TouchStart, []*input.TouchPoint{{X: x, Y: s.Y}}).Do(ctx); err != nil { return err } for _, o := range []float64{0.2, 0.4, 0.6, 0.8, 0.93} { if err := input.DispatchTouchEvent(input.TouchMove, []*input.TouchPoint{{X: x + sign*o*s.Run, Y: s.Y}}).Do(ctx); err != nil { return err } } return input.DispatchTouchEvent(input.TouchEnd, []*input.TouchPoint{}).Do(ctx) }), chromedp.Sleep(350*time.Millisecond)) if s := read(t, ctx, "touch"); !s.Checked || s.Changes != 1 { t.Errorf("a finger dragged past the middle: checked %v, %d change events; want on, once", s.Checked, s.Changes) } }) } } // TestASwitchFollowsItsOwnDirection: the knob's travel comes from the // switch's own direction, not from an ancestor's dir attribute. An LTR // section inside an RTL page and an RTL one inside an LTR page each // draw their checked knob at their own inline end, inside the track, // and a drag toward that end turns them on. func TestASwitchFollowsItsOwnDirection(t *testing.T) { sw := func(id, checked string) string { return `` } body := func(inner string) string { return `

` + sw("on", " checked") + `

` + sw("drag", "") + `

` + `` } pages := map[string]string{ "/ltr-in-rtl": strings.Replace(sizingDoc("switch", body("ltr")), `dir="ltr"`, `dir="rtl"`, 1), "/rtl-in-ltr": sizingDoc("switch", body("rtl")), } rig := sizingRig(t, false, pages) ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) defer cancel() for path, sign := range map[string]float64{"/ltr-in-rtl": 1, "/rtl-in-ltr": -1} { mustRun(t, ctx, chromedp.EmulateViewport(800, 700), chromedp.Navigate(rig.Origin+path), chromedp.WaitReady("#drag", chromedp.ByQuery), chromedp.Evaluate(`new Promise(r => document.readyState === "complete" ? r() : addEventListener("load", r, {once: true}))`, nil, awaitPromise)) if s := read(t, ctx, "on"); math.Abs(s.X-sign*s.Run) > 0.5 { t.Errorf("%s: the checked knob is moved %.1fpx; want %.1f, toward the switch's own inline end", path, s.X, sign*s.Run) } s := read(t, ctx, "drag") x := thumbAt(s, sign) mouse := func(typ input.MouseType, x float64) chromedp.Action { return input.DispatchMouseEvent(typ, x, s.Y).WithButton(input.Left).WithClickCount(1) } mustRun(t, ctx, mouse(input.MousePressed, x)) for _, o := range []float64{0.2, 0.4, 0.6, 0.8} { mustRun(t, ctx, mouse(input.MouseMoved, x+sign*o*s.Run)) } mustRun(t, ctx, mouse(input.MouseReleased, x+sign*0.8*s.Run), chromedp.Sleep(350*time.Millisecond)) if s := read(t, ctx, "drag"); !s.Checked || s.Changes != 1 || math.Abs(s.X-sign*s.Run) > 0.5 { t.Errorf("%s: dragged toward its inline end: checked %v, %d change events, knob at %.1f; want on, once, at %.1f", path, s.Checked, s.Changes, s.X, sign*s.Run) } } } // TestASwitchTakesOneDragAtATime: a second finger on another switch // while the first is dragging is ignored, and when both lift, neither // switch is left mid-drag. The second press overwrote the first drag, // which kept its drag mark and its knob position for good, where a // keyboard toggle or a form reset could no longer move it. func TestASwitchTakesOneDragAtATime(t *testing.T) { sw := func(id string) string { return `

` } body := `
` + sw("a") + sw("b") + `
` rig := sizingRig(t, true, map[string]string{"/": sizingDoc("switch", body)}) ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) defer cancel() mustRun(t, ctx, chromedp.EmulateViewport(390, 700), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitReady("#b", chromedp.ByQuery), chromedp.Evaluate(`new Promise(r => document.readyState === "complete" ? r() : addEventListener("load", r, {once: true}))`, nil, awaitPromise)) a, b := read(t, ctx, "a"), read(t, ctx, "b") ax, bx := thumbAt(a, 1), thumbAt(b, 1) touch := func(typ input.TouchType, pts ...*input.TouchPoint) chromedp.Action { return input.DispatchTouchEvent(typ, pts) } // Offsets are shares of the knob's travel. pa := func(o float64) *input.TouchPoint { return &input.TouchPoint{X: ax + o*a.Run, Y: a.Y, ID: 1} } pb := func(o float64) *input.TouchPoint { return &input.TouchPoint{X: bx + o*b.Run, Y: b.Y, ID: 2} } mustRun(t, ctx, touch(input.TouchStart, pa(0)), touch(input.TouchMove, pa(0.4)), touch(input.TouchMove, pa(0.8)), touch(input.TouchStart, pa(0.8), pb(0)), touch(input.TouchMove, pa(0.85), pb(0.4)), touch(input.TouchMove, pa(0.9), pb(0.8)), touch(input.TouchEnd), chromedp.Sleep(350*time.Millisecond)) if s := read(t, ctx, "a"); !s.Checked || s.Changes != 1 || s.Dragging || math.Abs(s.X-a.Run) > 0.5 { t.Errorf("the first switch: checked %v, %d change events, still dragging %v, knob at %.1f; want on, once, settled at %.1f", s.Checked, s.Changes, s.Dragging, s.X, a.Run) } if s := read(t, ctx, "b"); s.Checked || s.Changes != 0 || s.Dragging || math.Abs(s.X) > 0.5 { t.Errorf("the second switch, pressed mid-drag: checked %v, %d change events, dragging %v, knob at %.1f; want it untouched", s.Checked, s.Changes, s.Dragging, s.X) } } // switchSheen reads the on track's sheen position as the browser // resolved it: the x offset of the 40px radial gradient relative to the // track's centre, in px, signed as the screen has it. The gradient is // written as calc(50% + dir * (x - 7.5px)); Chromium serialises the // resolved calc as "calc(50% + Npx)" or "calc(50% - Npx)". func switchSheen(t *testing.T, ctx context.Context, sel string) (present bool, offset float64) { t.Helper() var img string mustRun(t, ctx, chromedp.Evaluate(`getComputedStyle(document.querySelector(`+jsStr(sel)+` + " [rst-switch-track]")).backgroundImage`, &img)) m := regexp.MustCompile(`radial-gradient\(40px at calc\(50% ([+-]) ([0-9.]+)px\)`).FindStringSubmatch(img) if m == nil { return false, 0 } v, _ := strconv.ParseFloat(m[2], 64) if m[1] == "-" { v = -v } return true, v } // TestTheKnobIsLitAndTheTrackSheenFollowsIt (spec ยง3): the // knob is a white-topped gradient in both schemes; the off track is // --rst-track-off and the on track the accent fill; hovered, the on // track's sheen sits over the knob, at the inline end in LTR and the // inline start in RTL; the tblock variant does the same; and the // sheen's position is pure CSS: no script writes on a switch. The knob // carries its highlight, its contact shadow and the knob shadow, and // the track sits in with an inset contact shadow; a disabled switch // takes no hover, as a disabled button takes none. func TestTheKnobIsLitAndTheTrackSheenFollowsIt(t *testing.T) { sw := `

` + `

` + `

` + `
body
` doc := sizingDoc("knob", `
`+sw+`
`) pages := map[string]string{"/": doc, "/rtl": strings.Replace(doc, `dir="ltr"`, `dir="rtl"`, 1)} for _, leg := range []struct{ name, path, scheme string }{{"ltr light", "/", "light"}, {"rtl light", "/rtl", "light"}, {"ltr dark", "/", "dark"}} { t.Run(leg.name, func(t *testing.T) { rig := sizingRig(t, false, pages, harness.WithHover()) ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) defer cancel() mustRun(t, ctx, chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+leg.path), chromedp.WaitVisible("#tb", chromedp.ByQuery), chromedp.Evaluate(`document.documentElement.setAttribute("data-theme", "`+leg.scheme+`"), true`, nil)) still(t, ctx) wantOff, wantOn := "rgb(138, 138, 149)", "rgb(32, 89, 221)" if leg.scheme == "dark" { wantOff, wantOn = "rgb(113, 115, 124)", "rgb(49, 96, 234)" } var knob, offTrack, onTrack string mustRun(t, ctx, chromedp.Evaluate(`getComputedStyle(document.querySelector("#off [rst-switch-track]"), "::after").backgroundImage`, &knob), chromedp.Evaluate(`getComputedStyle(document.querySelector("#off [rst-switch-track]")).backgroundColor`, &offTrack), chromedp.Evaluate(`getComputedStyle(document.querySelector("#s [rst-switch-track]")).backgroundColor`, &onTrack)) // The on track is day's derived fill, which serialises as // oklch(); the literal above is its eight-bit triple. offTrack, onTrack = canonColours(offTrack), canonColours(onTrack) if offTrack != wantOff || !strings.Contains(knob, "linear-gradient(rgb(255, 255, 255)") { t.Errorf("off switch: track %s (want %s), knob %q (want a white-topped gradient)", offTrack, wantOff, knob) } if onTrack != wantOn { t.Errorf("on switch: track %s, want %s", onTrack, wantOn) } if present, _ := switchSheen(t, ctx, "#s"); present { t.Error("the on track has a sheen at rest; the sheen is a hover") } var knobShadow string mustRun(t, ctx, chromedp.Evaluate(`getComputedStyle(document.querySelector("#off [rst-switch-track]"), "::after").boxShadow`, &knobShadow)) if n := len(layers(knobShadow)); n < 3 || !strings.HasSuffix(layers(knobShadow)[0], "inset") { t.Errorf("knob box-shadow %q: want the inset highlight first, then its contact shadow and the theme's knob shadow", knobShadow) } if sh := computed(t, ctx, "#off [rst-switch-track]", "box-shadow"); !strings.HasSuffix(layers(sh)[0], "0px 1px 2px 0px inset") { t.Errorf("track box-shadow %q, want an inset contact shadow first", sh) } var restKnob, hoverKnob string knobOf := `getComputedStyle(document.querySelector("#dis [rst-switch-track]"), "::after").backgroundImage` mustRun(t, ctx, chromedp.Evaluate(knobOf, &restKnob)) forceState(t, ctx, "#dis", "hover") present, _ := switchSheen(t, ctx, "#dis") mustRun(t, ctx, chromedp.Evaluate(knobOf, &hoverKnob)) forceState(t, ctx, "#dis") if present || hoverKnob != restKnob { t.Errorf("a disabled switch hovered: sheen %v, knob changed %v; want neither", present, hoverKnob != restKnob) } for _, sel := range []string{"#s", "#head"} { forceState(t, ctx, sel, "hover") present, off := switchSheen(t, ctx, sel) forceState(t, ctx, sel) if !present { t.Errorf("%s hovered has no 40px sheen", sel) continue } // On: the knob's centre is half the travel past the // middle toward the inline end, which is +x in LTR and // -x in RTL. half := read(t, ctx, sel[1:]).Run / 2 if (leg.path == "/rtl") != (off < 0) || math.Abs(math.Abs(off)-half) > 0.5 { t.Errorf("%s hovered: sheen offset %v on %s; want %.2fpx toward the inline end (C13)", sel, off, leg.path, half) } } if got := computed(t, ctx, "#head", "-webkit-tap-highlight-color"); got != "rgba(0, 0, 0, 0)" { t.Errorf("tblock head tap highlight %s, want transparent", got) } }) } } // TestTheSheenFollowsADraggedKnob (C13): with a finger (touch // emulation on the mouse rig, as TestASwitchSlidesAndSnaps drags; no // hover rig, so nothing hovers), the knob is dragged part way and the // sheen sits over it, written by fiesta.js on the track and read by // the gradient; the drag is cancelled and the knob snaps back with no // sheen; a second drag loses its capture (a lostpointercapture with // the drag's own pointer id, which the shim checks) and the knob snaps // back the same way. In LTR and RTL, and a third time on the LTR page // with light.js linked, where an observer on the switch records every // style write during the drags: each must be the shim's own // --rst-switch-p and --rst-switch-on (or their removal) and nothing else, because light.js // writes nothing on a switch while fiesta.js owns the drag (C13). // The fixture initialises the counters switchJS reads, as the other // switch fixtures do. func TestTheSheenFollowsADraggedKnob(t *testing.T) { sw := `

` + `` doc := sizingDoc("drag", `
`+sw+`
`) pages := map[string]string{"/": doc, "/rtl": strings.Replace(doc, `dir="ltr"`, `dir="rtl"`, 1), "/light": strings.Replace(doc, ``, ``, 1)} for _, path := range []string{"/", "/rtl", "/light"} { t.Run(path, func(t *testing.T) { rig := sizingRig(t, false, pages) ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) defer cancel() mustRun(t, ctx, chromedp.EmulateViewport(390, 700), chromedp.Navigate(rig.Origin+path), chromedp.WaitVisible("#s", chromedp.ByQuery), chromedp.Evaluate(`new Promise(r => addEventListener("load", r, {once: true}) || document.readyState === "complete" && r())`, nil, awaitPromise), emulation.SetTouchEmulationEnabled(true)) if path == "/light" { mustRun(t, ctx, chromedp.Evaluate(`window.__sw = []; new MutationObserver(m => m.forEach(r => window.__sw.push(r.target.getAttribute("style") || ""))).observe(document.getElementById("s"), {attributes: true, subtree: true, attributeFilter: ["style"], attributeOldValue: true}); true`, nil)) } s := read(t, ctx, "s") sign := 1.0 if path == "/rtl" { sign = -1 } // Start at the on end, drag 9px toward off in three steps // (the shim needs 4px before it takes the drag): the knob's // x becomes the travel less 9px, so the sheen sits that far // past the middle toward the inline end (negative: short of // it, on day's 17px travel). startX := thumbAt(s, sign) + sign*s.Run knobX, sheenAt := sign*(s.Run-9), sign*(s.Run/2-9) drag := func() { mustRun(t, ctx, input.DispatchTouchEvent(input.TouchStart, []*input.TouchPoint{{X: startX, Y: s.Y}}), input.DispatchTouchEvent(input.TouchMove, []*input.TouchPoint{{X: startX - 3*sign, Y: s.Y}}), input.DispatchTouchEvent(input.TouchMove, []*input.TouchPoint{{X: startX - 6*sign, Y: s.Y}}), input.DispatchTouchEvent(input.TouchMove, []*input.TouchPoint{{X: startX - 9*sign, Y: s.Y}}), chromedp.Sleep(100*time.Millisecond)) } drag() if mid := read(t, ctx, "s"); !mid.Dragging || math.Abs(mid.X-knobX) > 0.5 { t.Fatalf("mid-drag: dragging %v, knob at %v; want a drag with the knob at %v", mid.Dragging, mid.X, knobX) } if present, off := switchSheen(t, ctx, "#s"); !present || math.Abs(off-sheenAt) > 0.6 { t.Errorf("mid-drag sheen offset %v (present %v), want %v: the sheen follows the knob", off, present, sheenAt) } mustRun(t, ctx, input.DispatchTouchEvent(input.TouchCancel, nil), chromedp.Sleep(350*time.Millisecond)) if after := read(t, ctx, "s"); after.Dragging || math.Abs(after.X-s.Run*sign) > 0.5 || !after.Checked { t.Errorf("after a cancelled drag: dragging %v, knob at %v, checked %v; want the knob back at %v, still on", after.Dragging, after.X, after.Checked, s.Run*sign) } if present, _ := switchSheen(t, ctx, "#s"); present { t.Error("after a cancelled drag the sheen is still there; nothing hovers a finger's switch") } drag() if !read(t, ctx, "s").Dragging { t.Fatal("the second drag did not take") } mustRun(t, ctx, chromedp.Evaluate(`document.querySelector("#s [rst-switch-track]").dispatchEvent(new PointerEvent("lostpointercapture", {pointerId: window.__pid, bubbles: true})), true`, nil), chromedp.Sleep(350*time.Millisecond)) if after := read(t, ctx, "s"); after.Dragging || math.Abs(after.X-s.Run*sign) > 0.5 { t.Errorf("after lostpointercapture: dragging %v, knob at %v; want the drag dropped and the knob back at %v", after.Dragging, after.X, s.Run*sign) } if path == "/light" { var seen []string mustRun(t, ctx, chromedp.Evaluate(`window.__sw`, &seen)) if len(seen) == 0 { t.Error("the observer saw no style write on the switch during two drags; the shim writes --rst-switch-p and --rst-switch-on there, so the leg proves nothing") } own := regexp.MustCompile(`^(--rst-switch-(p|on):\s*[^;]*;?\s*)*$`) for _, v := range seen { if v != "" && !own.MatchString(v) { t.Errorf("a style write on the switch during a drag left %q; only the shim's --rst-switch-p and --rst-switch-on may be written there (C13)", v) } } } mustRun(t, ctx, input.DispatchTouchEvent(input.TouchEnd, nil)) }) } } // TestATblockReachesOnlyItsOwnHeadSwitch (#104 item 1): the toggle // block's checked, focus and reveal rules once read ":has a checked // input in a head, anywhere below" and painted "every track below", so // an unchecked switch in a block's body drew as on, a checked block // nested in an unchecked one flipped the outer head and opened its // body, and a focused head outlined every track in the block. The // rules are scoped to the block's own head: the head switch is as it // was, a body switch answers to itself, and a nested block to itself. // In day, plain and signal, each theme's stylesheet swapped in, with // the stand-alone switches on the page as the reference for on and // off. Frozen first, for forceState. func TestATblockReachesOnlyItsOwnHeadSwitch(t *testing.T) { const track = " [rst-switch-track]" body := `
` + `

` + `

` + `
` + `
` + `
` + `
Inner body
` + `

` rig := sizingRig(t, false, map[string]string{"/": sizingDoc("tblock", body)}, harness.WithHover()) ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) defer cancel() open(t, ctx, rig, "/", false) still(t, ctx) face := func(sel string) string { return computed(t, ctx, sel+track, "background-color") + " knob " + computed(t, ctx, sel+track, "--rst-switch-p") } outline := func(sel string) string { return computed(t, ctx, sel+track, "outline-style") } for _, theme := range []string{"day", "plain", "signal"} { swapTheme(t, ctx, theme) on, off := face("#ref-on"), face("#ref-off") if on == off { t.Fatalf("%s: a stand-alone switch draws %s on and off alike; the drive would prove nothing", theme, on) } for _, c := range []struct{ what, sel, want string }{ {"the open block's head switch", "#open-head", on}, {"an off switch in the open block's body", "#body-sw", off}, {"the shut block's head switch", "#shut-head", off}, {"the on block nested in the shut one", "#inner-head", on}, } { if got := face(c.sel); got != c.want { t.Errorf("%s: %s draws %s, want %s", theme, c.what, got, c.want) } } if got := computed(t, ctx, "#shut > [rst-tblock-body]", "display"); got != "none" { t.Errorf("%s: the shut block's body is display %s; a checked block nested inside opened it", theme, got) } if got := computed(t, ctx, "#inner > [rst-tblock-body]", "display"); got != "block" { t.Errorf("%s: the nested on block's body is display %s, want block", theme, got) } forceState(t, ctx, "#open-in", "focus-visible") if got := outline("#open-head"); got == "none" { t.Errorf("%s: the focused head's switch has no outline", theme) } if got := outline("#body-sw"); got != "none" { t.Errorf("%s: the head is focused and the body switch is outlined too (%s)", theme, got) } forceState(t, ctx, "#open-in") forceState(t, ctx, "#inner-in", "focus-visible") if got := outline("#inner-head"); got == "none" { t.Errorf("%s: the focused nested head's switch has no outline", theme) } if got := outline("#shut-head"); got != "none" { t.Errorf("%s: the nested head is focused and the outer head's switch is outlined too (%s)", theme, got) } forceState(t, ctx, "#inner-in") } }