From 50661e060ca32802e960ab1df4061c07023a5916 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 20:17:07 +0000 Subject: [PATCH] Event log: an event's ID can be selected without toggling it (closes #348) An event's row in the event log was a button element, whose text a browser does not let be selected, and a drag over the event's ID toggled the event. The row is now an element with the button role: focusable, toggled by Enter and Space, and saying whether it is expanded. A click that ends a text selection leaves the event as it is. The browser test now also clicks the row's caret, selects the ID with the mouse, and uses the keyboard. Model: opus-5-5 --- README.md | 19 +++--- internal/server/alpine_browser_test.go | 94 +++++++++++++++++++++++--- static/js/app.js | 7 ++ templates/source_logs.html | 5 +- 4 files changed, 103 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 57fb9c4..0b730c0 100644 --- a/README.md +++ b/README.md @@ -1353,15 +1353,16 @@ of its description and hides until the form closes, Cancel hides the form and drops what was typed, as does leaving the page and going back to it, and Save changes the description; of the recent events on the webhook page only the newest starts expanded, each expands and collapses, and Open leads to the -event's own page; an event in the event log expands and collapses, and so do a -delivery's attempts inside it; and at phone width the menu button opens and -closes the mobile menu. It also fails if the browser reports a console warning -or error, an uncaught exception, or anything the policy refused. `make check` -and the image build lint it but do not run it, and `make test` leaves it out -(its file is built only with the `browser` build tag). Run it with -`make test-browser` after changing `templates/` or `static/js/`: that builds -`Dockerfile.browser`, which runs the test in a digest-pinned headless browser -image, so the host needs no browser. +event's own page; an event in the event log expands and collapses when its +row's caret or its ID is clicked, and from the keyboard, but not when its ID is +selected with the mouse, and a delivery's attempts inside it expand and +collapse; and at phone width the menu button opens and closes the mobile menu. +It also fails if the browser reports a console warning or error, an uncaught +exception, or anything the policy refused. `make check` and the image build lint +it but do not run it, and `make test` leaves it out (its file is built only with +the `browser` build tag). Run it with `make test-browser` after changing +`templates/` or `static/js/`: that builds `Dockerfile.browser`, which runs the +test in a digest-pinned headless browser image, so the host needs no browser. The package's tarball is committed as `3p/alpinejs-csp-3.14.9.tgz`, byte for byte as the npm registry publishes it. It is a dependency, not this repo's build diff --git a/internal/server/alpine_browser_test.go b/internal/server/alpine_browser_test.go index f4459e5..f372f01 100644 --- a/internal/server/alpine_browser_test.go +++ b/internal/server/alpine_browser_test.go @@ -18,10 +18,13 @@ import ( "time" "github.com/chromedp/cdproto/browser" + "github.com/chromedp/cdproto/dom" + "github.com/chromedp/cdproto/input" "github.com/chromedp/cdproto/log" "github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" + "github.com/chromedp/chromedp/kb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm/clause" @@ -492,18 +495,24 @@ func checkRecentEvents(ctx context.Context, t *testing.T, url string) { "the event's own page does not show its body") } -// checkEventLog loads the event log and checks that clicking an event's -// row expands it, that in there clicking its delivery shows the -// delivery's attempts and clicking again hides them, and that clicking -// the event's row again collapses it. +// checkEventLog loads the event log and checks an event's row. Clicking +// its ID expands the event, and in there clicking its delivery shows the +// delivery's attempts and clicking again hides them. Clicking the row's +// caret collapses the event. Selecting the ID with the mouse leaves the +// event collapsed, and the caret's next click expands it. Clicking the +// ID again collapses it. From the keyboard, Enter on the row expands +// the event and Space collapses it. func checkEventLog( ctx context.Context, t *testing.T, url, eventID, targetName string, ) { t.Helper() - // The event's row shows its ID, and its Resubmit form is in the part - // that expands. The delivery's row there shows the target's name. - eventRow := `//span[text()="` + eventID + `"]` + // The event's row shows its ID and ends with its caret, and its + // Resubmit form is in the part that expands. The delivery's row + // there shows the target's name. + id := `//span[text()="` + eventID + `"]` + row := id + `/ancestor::div[@role="button"]` + caret := row + `//*[local-name()="svg"]` expanded := `form[action$="/` + eventID + `/resubmit"]` deliveryRow := `//span[text()="` + targetName + `"]` attempt := `//span[text()="Attempt 1"]` @@ -512,8 +521,9 @@ func checkEventLog( assert.True(t, hidden(ctx, expanded), "the event starts expanded") - click(ctx, t, eventRow) - assert.True(t, shown(ctx, expanded), "clicking the event does not expand it") + click(ctx, t, id) + assert.True(t, shown(ctx, expanded), + "clicking the event's ID does not expand it") assert.True(t, hidden(ctx, attempt), "the delivery's attempts start shown") @@ -525,9 +535,71 @@ func checkEventLog( assert.True(t, hidden(ctx, attempt), "clicking the delivery again does not hide its attempts") - click(ctx, t, eventRow) + click(ctx, t, caret) assert.True(t, hidden(ctx, expanded), - "clicking the event again does not collapse it") + "clicking the caret does not collapse the event") + + var ( + selected, state string + hasState bool + ) + + selectText(ctx, t, id) + require.NoError(t, chromedp.Run( + ctx, + chromedp.Evaluate(`window.getSelection().toString()`, &selected), + chromedp.AttributeValue( + row, "aria-expanded", &state, &hasState, chromedp.BySearch, + ), + )) + assert.Equal(t, eventID, selected, "the event's ID cannot be selected") + require.True(t, hasState, "the event's row does not say if it is expanded") + assert.Equal(t, "false", state, "selecting the event's ID expands it") + + click(ctx, t, caret) + assert.True(t, shown(ctx, expanded), + "clicking the caret after selecting the ID does not expand the event") + + click(ctx, t, id) + assert.True(t, hidden(ctx, expanded), + "clicking the event's ID again does not collapse it") + + require.NoError(t, chromedp.Run( + ctx, + chromedp.Focus(row, chromedp.BySearch), + chromedp.KeyEvent(kb.Enter), + )) + assert.True(t, shown(ctx, expanded), "Enter does not expand the event") + + require.NoError(t, chromedp.Run(ctx, chromedp.KeyEvent(" "))) + assert.True(t, hidden(ctx, expanded), "Space does not collapse the event") +} + +// selectText selects the text of the element matching an XPath +// expression as a person does with the mouse: pressing the button at the +// text's start, moving to its end and releasing it there. +func selectText(ctx context.Context, t *testing.T, xpath string) { + t.Helper() + + var box *dom.BoxModel + + require.NoError(t, chromedp.Run( + ctx, chromedp.Dimensions(xpath, &box, chromedp.BySearch), + )) + + // The content box's corners, clockwise from its top left. + left, right := box.Content[0]+1, box.Content[2]-1 + middle := (box.Content[1] + box.Content[5]) / 2 + + require.NoError(t, chromedp.Run( + ctx, + input.DispatchMouseEvent(input.MousePressed, left, middle). + WithButton(input.Left).WithButtons(1).WithClickCount(1), + input.DispatchMouseEvent(input.MouseMoved, right, middle). + WithButton(input.Left).WithButtons(1), + input.DispatchMouseEvent(input.MouseReleased, right, middle). + WithButton(input.Left).WithClickCount(1), + )) } // checkMobileMenu loads a page in a phone-sized window and checks that diff --git a/static/js/app.js b/static/js/app.js index 87e4bf4..ea66581 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -82,6 +82,13 @@ document.addEventListener("alpine:init", function () { toggle() { this.open = !this.open; }, + // Toggles, except on a click that ends a text selection, such + // as selecting an event's ID to copy it. + toggleUnlessSelecting() { + if (window.getSelection().toString() === "") { + this.toggle(); + } + }, get closed() { return !this.open; }, diff --git a/templates/source_logs.html b/templates/source_logs.html index d900661..856bb65 100644 --- a/templates/source_logs.html +++ b/templates/source_logs.html @@ -16,7 +16,8 @@