Break long values on the webhook page and event log at phone width (closes #391)
check / check (push) Successful in 3m39s

An event's ID or content type, or a name with no place to break, ran
past its row and was cut off by the card at 390 pixels, and a long
webhook name made the webhook page scroll sideways. Both pages' outer
containers now carry wrap-anywhere, so such a value breaks only where
it must; the event log's title row wraps like the webhook page's.

The browser test gains a 390-pixel check of both pages: neither scrolls
sideways and nothing shown runs past the page's or its card's edge. Its
seeded webhook name and newer event's content type are long enough to
fail without the change.

Model: opus-5-5
This commit is contained in:
2026-10-03 03:52:09 +00:00
parent 7e779f7fce
commit 329f265e30
5 changed files with 81 additions and 10 deletions
+4 -3
View File
@@ -251,8 +251,9 @@ func TestHandleSourceDetail_RendersNamedTargetFields(
// page's maximum width at 108rem (1728 px), half again the 72rem of
// max-w-6xl that the webhook list and the event log use, so an
// entrypoint URL fits on one line in a 1920-pixel window; and the
// wrapping of its title row, so the buttons beside the title do not
// push a phone-width window into scrolling sideways.
// wrapping of its title row and of long values, so neither the buttons
// beside the title nor a long name push a phone-width window into
// scrolling sideways.
func TestHandleSourceDetail_FitsWideAndNarrowWindows(t *testing.T) {
t.Parallel()
@@ -273,7 +274,7 @@ func TestHandleSourceDetail_FitsWideAndNarrowWindows(t *testing.T) {
assert.Contains(
t, body,
`<div class="mx-auto px-6 py-8" style="max-width: 108rem"`,
`<div class="mx-auto px-6 py-8 wrap-anywhere" style="max-width: 108rem"`,
)
assert.Contains(
t, body,
+67 -2
View File
@@ -98,20 +98,25 @@ func TestAlpineRunsUnderTheSecurityPolicy(t *testing.T) {
checkRefusedNewWebhook(ctx, t, srv.URL+"/hooks/new")
checkEventLog(ctx, t, page+"/events", event.ID, older.ID, target.Name)
checkMobileMenu(ctx, t, page)
checkPhoneWidth(ctx, t, page, page+"/events", target.Name)
assert.Empty(t, problems(), "the browser reported problems")
}
// seedBrowserWebhook seeds the webhook the browser test loads, owned by
// userID: an entrypoint, two events, and a target whose delivery of the
// newer event failed once with a 502. It returns the webhook, the older
// and the newer event, and the target.
// newer event failed once with a 502. The webhook's name and the newer
// event's content type are each too long for one line on a phone. It
// returns the webhook, the older and the newer event, and the target.
func seedBrowserWebhook(
t *testing.T, env *testEnv, userID string,
) (*database.Webhook, *database.Event, *database.Event, *database.Target) {
t.Helper()
webhook := env.seedWebhook(t, userID)
require.NoError(t, env.db.DB().Model(webhook).Update(
"name", "payment_provider_production_notifications",
).Error)
require.NoError(t, env.db.DB().Omit(clause.Associations).Create(
&database.Entrypoint{
WebhookID: webhook.ID,
@@ -126,6 +131,9 @@ func seedBrowserWebhook(
webhookDB, err := env.dbMgr.GetDB(webhook.ID)
require.NoError(t, err)
require.NoError(t, webhookDB.Model(event).Update(
"content_type", "application/vnd.paymentprovider.event+json",
).Error)
require.NoError(t, webhookDB.Omit(clause.Associations).Create(
&database.DeliveryResult{
DeliveryID: dlv.ID,
@@ -1292,3 +1300,60 @@ func checkMobileMenu(ctx context.Context, t *testing.T, url string) {
click(ctx, t, button)
assert.True(t, hidden(ctx, menu), "the menu button does not close the menu")
}
// scrollsSideways reports whether the page is wider than the window. A
// page's clientWidth is the window's width less its scroll bar.
const scrollsSideways = `document.documentElement.scrollWidth >
document.documentElement.clientWidth`
// cutOffElements lists each element, without elements inside it, that
// is shown but runs past the page's edge or its card's, by more than a
// pixel of rounding. A card hides what runs past its edge.
const cutOffElements = `[...document.querySelectorAll("body *")]
.filter((el) => {
const box = el.getBoundingClientRect();
const card = el.closest(".card")?.getBoundingClientRect();
const left = card ? card.left : 0;
const right = card ? card.right : document.documentElement.clientWidth;
return el.children.length === 0 && box.width > 0 &&
(box.left < left - 1 || box.right > right + 1);
})
.map((el) => el.outerHTML.slice(0, 120))`
// checkPhoneWidth loads the webhook page, url, and its event log,
// eventLog, in a phone-sized window, the event log with the attempts of
// the newest event's delivery to targetName shown. It checks that
// neither page scrolls sideways and that nothing shown on either, no
// status, time or control, is cut off at the page's or its card's edge.
func checkPhoneWidth(
ctx context.Context, t *testing.T, url, eventLog, targetName string,
) {
t.Helper()
var (
sideways bool
cutOff []string
)
measure := chromedp.Tasks{
chromedp.Evaluate(scrollsSideways, &sideways),
chromedp.Evaluate(cutOffElements, &cutOff),
}
require.NoError(t, chromedp.Run(
ctx,
chromedp.EmulateViewport(phoneWidth, phoneHeight),
loadPage(url),
measure,
))
assert.False(t, sideways, "the webhook page scrolls sideways on a phone")
assert.Empty(t, cutOff, "the webhook page cuts these off on a phone")
require.NoError(t, chromedp.Run(ctx, loadPage(eventLog)))
click(ctx, t, `//span[text()="`+targetName+`"]`)
require.True(t, shown(ctx, `//span[text()="Attempt 1"]`),
"clicking the delivery does not show its attempts")
require.NoError(t, chromedp.Run(ctx, measure))
assert.False(t, sideways, "the event log scrolls sideways on a phone")
assert.Empty(t, cutOff, "the event log cuts these off on a phone")
}
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -6,8 +6,10 @@
<!-- 108rem, half again the 72rem (max-w-6xl) of the webhook list, the
event log, the navbar and the footer, so an entrypoint URL fits on
one line. An inline style, because the committed tailwind.css has
no class this wide. -->
<div class="mx-auto px-6 py-8" style="max-width: 108rem">
no class this wide. wrap-anywhere: a value with no place to break,
such as a long name, breaks where it must instead of running past
the edge of a phone's screen. -->
<div class="mx-auto px-6 py-8 wrap-anywhere" style="max-width: 108rem">
<div class="mb-6">
<a href="/hooks" class="btn-small">&larr; Back to webhooks</a>
<div class="flex flex-wrap justify-between items-center gap-2 mt-2">
+5 -2
View File
@@ -3,10 +3,13 @@
{{define "title"}}Full Event Log - {{.Webhook.Name}} - Webhooker{{end}}
{{define "content"}}
<div class="max-w-6xl mx-auto px-6 py-8">
<!-- wrap-anywhere: a value with no place to break, such as an event's
ID or content type, breaks where it must instead of running past
the edge of a phone's screen. -->
<div class="max-w-6xl mx-auto px-6 py-8 wrap-anywhere">
<div class="mb-6">
<a href="/hook/{{.Webhook.ID}}" class="btn-small">&larr; Back to {{.Webhook.Name}}</a>
<div class="flex justify-between items-center mt-2">
<div class="flex flex-wrap justify-between items-center gap-2 mt-2">
<h1 class="text-2xl font-medium text-gray-900">Full Event Log</h1>
<!-- Under a filter, this counts the events the filter lists. -->
<span class="text-sm text-gray-500">{{if gt .TotalEvents (len .Events)}}{{len .Events}} most recent of {{.TotalEvents}} events{{else}}{{.TotalEvents}}{{if not .Show}} total{{end}} event{{if ne .TotalEvents 1}}s{{end}}{{end}}{{if eq .Show "failed"}} with a failed delivery{{else if eq .Show "pending"}} with a delivery pending or retrying{{end}}</span>