Event log: an event's ID can be selected without toggling it (closes #348)
check / check (push) Successful in 3m13s
check / check (push) Successful in 3m13s
An event's row in the event log was a button element, whose text a browser does not let be selected. 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, and a single click toggles only after 500 ms, which the second click of a double- or triple-click cancels, so the event does not move under the pointer while its ID is selected. The browser test now also clicks the caret, selects the ID by dragging and by a triple-click, and uses the keyboard. Model: opus-5-5
This commit is contained in:
@@ -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"
|
||||
@@ -40,6 +43,11 @@ const (
|
||||
phoneWidth = 390
|
||||
phoneHeight = 844
|
||||
|
||||
// A window short enough that the event log scrolls with its last
|
||||
// event expanded, and tall enough to show all of that event.
|
||||
shortWidth = 1024
|
||||
shortHeight = 450
|
||||
|
||||
// olderBody is the body of the event received before the newest.
|
||||
olderBody = "the older event"
|
||||
)
|
||||
@@ -67,7 +75,7 @@ func TestAlpineRunsUnderTheSecurityPolicy(t *testing.T) {
|
||||
Active: true,
|
||||
},
|
||||
).Error)
|
||||
env.seedEvent(t, webhook.ID, olderBody)
|
||||
older := env.seedEvent(t, webhook.ID, olderBody)
|
||||
event := env.seedEvent(t, webhook.ID, `{"hello":"browser"}`)
|
||||
target := env.seedTarget(t, webhook.ID)
|
||||
dlv := env.seedFailedDelivery(t, webhook.ID, event.ID, target.ID)
|
||||
@@ -125,10 +133,10 @@ func TestAlpineRunsUnderTheSecurityPolicy(t *testing.T) {
|
||||
checkCopy(ctx, t, page)
|
||||
checkEntrypointEdit(ctx, t, page, page+"/events")
|
||||
checkRecentEvents(ctx, t, page)
|
||||
checkEventLog(ctx, t, page+"/events", event.ID, target.Name)
|
||||
checkArchiveChoice(ctx, t, srv.URL+"/hooks/new", page)
|
||||
checkNewWebhookTargets(ctx, t, env, srv.URL+"/hooks/new")
|
||||
checkRefusedNewWebhook(ctx, t, srv.URL+"/hooks/new")
|
||||
checkEventLog(ctx, t, page+"/events", event.ID, older.ID, target.Name)
|
||||
checkMobileMenu(ctx, t, page)
|
||||
|
||||
assert.Empty(t, problems(), "the browser reported problems")
|
||||
@@ -633,18 +641,30 @@ 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, clicking it again expands it, and clicking
|
||||
// the ID again collapses it. While the event is expanded the row says so
|
||||
// and its caret is turned up, and while it is collapsed neither. It then
|
||||
// runs checkEventSelection on the log's last event, lastEventID, and
|
||||
// checkEventKeyboard on eventID.
|
||||
func checkEventLog(
|
||||
ctx context.Context, t *testing.T, url, eventID, targetName string,
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
url, eventID, lastEventID, 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, which turns
|
||||
// up with Tailwind's rotate-180 class, 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"]`
|
||||
caretUp := caret + `[contains(@class, "rotate-180")]`
|
||||
caretDown := caret + `[not(contains(@class, "rotate-180"))]`
|
||||
expanded := `form[action$="/` + eventID + `/resubmit"]`
|
||||
deliveryRow := `//span[text()="` + targetName + `"]`
|
||||
attempt := `//span[text()="Attempt 1"]`
|
||||
@@ -653,8 +673,13 @@ 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, shown(ctx, row+`[@aria-expanded="true"]`),
|
||||
"the expanded event's row does not say it is expanded")
|
||||
assert.True(t, shown(ctx, caretUp),
|
||||
"the expanded event's caret does not turn up")
|
||||
|
||||
assert.True(t, hidden(ctx, attempt), "the delivery's attempts start shown")
|
||||
|
||||
@@ -666,9 +691,169 @@ 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")
|
||||
assert.True(t, shown(ctx, row+`[@aria-expanded="false"]`),
|
||||
"the collapsed event's row does not say it is collapsed")
|
||||
assert.True(t, shown(ctx, caretDown),
|
||||
"the collapsed event's caret stays turned up")
|
||||
|
||||
click(ctx, t, caret)
|
||||
assert.True(t, shown(ctx, expanded),
|
||||
"clicking the caret again 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")
|
||||
|
||||
checkEventSelection(ctx, t, url, lastEventID)
|
||||
checkEventKeyboard(ctx, t, url, eventID)
|
||||
}
|
||||
|
||||
// checkEventSelection loads the event log in a short window and checks
|
||||
// that selecting the ID of its last event, eventID, with the mouse leaves
|
||||
// the event as it was: dragging over the ID leaves it collapsed, the
|
||||
// caret's next click still expands it, and with the page then scrolled to
|
||||
// its end, a triple-click on the ID leaves it expanded and selects that
|
||||
// ID. Had the triple-click's first click collapsed the event, the page
|
||||
// would have got shorter and moved under the pointer.
|
||||
func checkEventSelection(
|
||||
ctx context.Context, t *testing.T, url, eventID string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
id := `//span[text()="` + eventID + `"]`
|
||||
row := id + `/ancestor::div[@role="button"]`
|
||||
caret := row + `//*[local-name()="svg"]`
|
||||
expanded := `form[action$="/` + eventID + `/resubmit"]`
|
||||
|
||||
var (
|
||||
selected, state string
|
||||
hasState bool
|
||||
scrolled float64
|
||||
)
|
||||
|
||||
// A single click toggles the event half a second later, so this waits
|
||||
// a second before reading the selection and whether it is expanded.
|
||||
read := chromedp.Tasks{
|
||||
chromedp.Sleep(time.Second),
|
||||
chromedp.Evaluate(`window.getSelection().toString()`, &selected),
|
||||
chromedp.AttributeValue(
|
||||
row, "aria-expanded", &state, &hasState, chromedp.BySearch,
|
||||
),
|
||||
}
|
||||
|
||||
require.NoError(t, chromedp.Run(
|
||||
ctx, chromedp.EmulateViewport(shortWidth, shortHeight), loadPage(url),
|
||||
))
|
||||
|
||||
selectText(ctx, t, id)
|
||||
require.NoError(t, chromedp.Run(ctx, read))
|
||||
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")
|
||||
|
||||
// The caret's click also clears the selection, so the triple-click's
|
||||
// first click finds nothing selected, as a person's would.
|
||||
click(ctx, t, caret)
|
||||
assert.True(t, shown(ctx, expanded),
|
||||
"clicking the caret after selecting the ID does not expand the event")
|
||||
|
||||
require.NoError(t, chromedp.Run(ctx, chromedp.Evaluate(
|
||||
`window.scrollTo(0, document.body.scrollHeight); window.scrollY`,
|
||||
&scrolled,
|
||||
)))
|
||||
require.Positive(t, scrolled, "the event log does not scroll")
|
||||
|
||||
tripleClick(ctx, t, id)
|
||||
require.NoError(t, chromedp.Run(ctx, read))
|
||||
assert.Contains(t, selected, eventID,
|
||||
"a triple-click does not select the event's ID")
|
||||
assert.Equal(t, "true", state,
|
||||
"a triple-click selecting the event's ID collapses it")
|
||||
}
|
||||
|
||||
// checkEventKeyboard loads the event log and checks that Tab from the
|
||||
// page's Back link reaches the event's row, the first after it, and that
|
||||
// Enter then expands the event and Space collapses it.
|
||||
func checkEventKeyboard(
|
||||
ctx context.Context, t *testing.T, url, eventID string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
back := `//a[contains(text(), "Back to")]`
|
||||
expanded := `form[action$="/` + eventID + `/resubmit"]`
|
||||
|
||||
var focused string
|
||||
|
||||
require.NoError(t, chromedp.Run(
|
||||
ctx,
|
||||
loadPage(url),
|
||||
chromedp.Focus(back, chromedp.BySearch),
|
||||
chromedp.KeyEvent(kb.Tab),
|
||||
chromedp.Evaluate(`document.activeElement.textContent`, &focused),
|
||||
))
|
||||
require.Contains(t, focused, eventID,
|
||||
"Tab from the Back link does not reach the event's row")
|
||||
|
||||
require.NoError(t, chromedp.Run(ctx, 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),
|
||||
))
|
||||
}
|
||||
|
||||
// tripleClick clicks three times in a row in the middle of the element
|
||||
// matching an XPath expression, as a person does to select a whole line
|
||||
// of text. The browser selects a word on the second click and the whole
|
||||
// paragraph on the third.
|
||||
func tripleClick(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.
|
||||
x := (box.Content[0] + box.Content[2]) / 2
|
||||
y := (box.Content[1] + box.Content[5]) / 2
|
||||
|
||||
require.NoError(t, chromedp.Run(
|
||||
ctx,
|
||||
chromedp.MouseClickXY(x, y, chromedp.ClickCount(1)),
|
||||
chromedp.MouseClickXY(x, y, chromedp.ClickCount(2)),
|
||||
chromedp.MouseClickXY(x, y, chromedp.ClickCount(3)),
|
||||
))
|
||||
}
|
||||
|
||||
// The parts of the new webhook page the checks below find and click.
|
||||
|
||||
Reference in New Issue
Block a user