Event log: an event's ID can be selected without toggling it (closes #348)
check / check (push) Successful in 3m19s
check / check (push) Successful in 3m19s
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
This commit is contained in:
@@ -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
|
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
|
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
|
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
|
event's own page; an event in the event log expands and collapses when its
|
||||||
delivery's attempts inside it; and at phone width the menu button opens and
|
row's caret or its ID is clicked, and from the keyboard, but not when its ID is
|
||||||
closes the mobile menu. It also fails if the browser reports a console warning
|
selected with the mouse, and a delivery's attempts inside it expand and
|
||||||
or error, an uncaught exception, or anything the policy refused. `make check`
|
collapse; and at phone width the menu button opens and closes the mobile menu.
|
||||||
and the image build lint it but do not run it, and `make test` leaves it out
|
It also fails if the browser reports a console warning or error, an uncaught
|
||||||
(its file is built only with the `browser` build tag). Run it with
|
exception, or anything the policy refused. `make check` and the image build lint
|
||||||
`make test-browser` after changing `templates/` or `static/js/`: that builds
|
it but do not run it, and `make test` leaves it out (its file is built only with
|
||||||
`Dockerfile.browser`, which runs the test in a digest-pinned headless browser
|
the `browser` build tag). Run it with `make test-browser` after changing
|
||||||
image, so the host needs no browser.
|
`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
|
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
|
byte as the npm registry publishes it. It is a dependency, not this repo's build
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/browser"
|
"github.com/chromedp/cdproto/browser"
|
||||||
|
"github.com/chromedp/cdproto/dom"
|
||||||
|
"github.com/chromedp/cdproto/input"
|
||||||
"github.com/chromedp/cdproto/log"
|
"github.com/chromedp/cdproto/log"
|
||||||
"github.com/chromedp/cdproto/network"
|
"github.com/chromedp/cdproto/network"
|
||||||
"github.com/chromedp/cdproto/runtime"
|
"github.com/chromedp/cdproto/runtime"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/chromedp/chromedp/kb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gorm.io/gorm/clause"
|
"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")
|
"the event's own page does not show its body")
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkEventLog loads the event log and checks that clicking an event's
|
// checkEventLog loads the event log and checks an event's row. Clicking
|
||||||
// row expands it, that in there clicking its delivery shows the
|
// its ID expands the event, and in there clicking its delivery shows the
|
||||||
// delivery's attempts and clicking again hides them, and that clicking
|
// delivery's attempts and clicking again hides them. Clicking the row's
|
||||||
// the event's row again collapses it.
|
// 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(
|
func checkEventLog(
|
||||||
ctx context.Context, t *testing.T, url, eventID, targetName string,
|
ctx context.Context, t *testing.T, url, eventID, targetName string,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// The event's row shows its ID, and its Resubmit form is in the part
|
// The event's row shows its ID and ends with its caret, and its
|
||||||
// that expands. The delivery's row there shows the target's name.
|
// Resubmit form is in the part that expands. The delivery's row
|
||||||
eventRow := `//span[text()="` + eventID + `"]`
|
// 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"]`
|
expanded := `form[action$="/` + eventID + `/resubmit"]`
|
||||||
deliveryRow := `//span[text()="` + targetName + `"]`
|
deliveryRow := `//span[text()="` + targetName + `"]`
|
||||||
attempt := `//span[text()="Attempt 1"]`
|
attempt := `//span[text()="Attempt 1"]`
|
||||||
@@ -512,8 +521,9 @@ func checkEventLog(
|
|||||||
|
|
||||||
assert.True(t, hidden(ctx, expanded), "the event starts expanded")
|
assert.True(t, hidden(ctx, expanded), "the event starts expanded")
|
||||||
|
|
||||||
click(ctx, t, eventRow)
|
click(ctx, t, id)
|
||||||
assert.True(t, shown(ctx, expanded), "clicking the event does not expand it")
|
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")
|
assert.True(t, hidden(ctx, attempt), "the delivery's attempts start shown")
|
||||||
|
|
||||||
@@ -525,9 +535,71 @@ func checkEventLog(
|
|||||||
assert.True(t, hidden(ctx, attempt),
|
assert.True(t, hidden(ctx, attempt),
|
||||||
"clicking the delivery again does not hide its attempts")
|
"clicking the delivery again does not hide its attempts")
|
||||||
|
|
||||||
click(ctx, t, eventRow)
|
click(ctx, t, caret)
|
||||||
assert.True(t, hidden(ctx, expanded),
|
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
|
// checkMobileMenu loads a page in a phone-sized window and checks that
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ document.addEventListener("alpine:init", function () {
|
|||||||
toggle() {
|
toggle() {
|
||||||
this.open = !this.open;
|
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() {
|
get closed() {
|
||||||
return !this.open;
|
return !this.open;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
<div class="divide-y divide-gray-100">
|
<div class="divide-y divide-gray-100">
|
||||||
{{range .Events}}
|
{{range .Events}}
|
||||||
<div class="p-4" x-data="collapsible">
|
<div class="p-4" x-data="collapsible">
|
||||||
<button type="button" class="btn-small w-full flex flex-wrap justify-between gap-2 text-left" @click="toggle">
|
<!-- Not a button element: browsers do not let a button's text be selected, and an event's ID must be. -->
|
||||||
|
<div role="button" tabindex="0" class="btn-small w-full flex flex-wrap justify-between gap-2" :aria-expanded="open" @click="toggleUnlessSelecting" @keydown.enter.prevent="toggle" @keydown.space.prevent="toggle">
|
||||||
<span class="flex flex-wrap items-center gap-3">
|
<span class="flex flex-wrap items-center gap-3">
|
||||||
<span class="badge-info">{{.Method}}</span>
|
<span class="badge-info">{{.Method}}</span>
|
||||||
<span class="text-sm font-mono text-gray-700">{{.ID}}</span>
|
<span class="text-sm font-mono text-gray-700">{{.ID}}</span>
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<div x-show="open" x-cloak class="mt-3 p-3 bg-gray-50 rounded-md">
|
<div x-show="open" x-cloak class="mt-3 p-3 bg-gray-50 rounded-md">
|
||||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user