|
|
|
@@ -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.
|
|
|
|
|