Make notification navigation reliable
This commit is contained in:
parent
ead8a67d0f
commit
5e5478389d
|
|
@ -1037,39 +1037,6 @@ export const mountStaticAidMaps = root => {
|
|||
}
|
||||
|
||||
export const Hooks = {
|
||||
NotificationLink: {
|
||||
mounted() {
|
||||
this.markOpened = event => {
|
||||
if (event.defaultPrevented || event.button > 1) return
|
||||
|
||||
const openUrl = this.el.dataset.openUrl
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']")?.content
|
||||
if (!openUrl || !csrfToken) return
|
||||
|
||||
// Do not await this request. The link owns navigation, while keepalive
|
||||
// lets the small read-marker request finish during LiveView or full-page
|
||||
// navigation without making the destination wait for it.
|
||||
fetch(openUrl, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
keepalive: true,
|
||||
headers: {
|
||||
// This endpoint is protected by the regular browser pipeline,
|
||||
// which accepts HTML requests. The 204 response has no body.
|
||||
"accept": "text/html",
|
||||
"x-csrf-token": csrfToken,
|
||||
},
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
this.el.addEventListener("click", this.markOpened, {capture: true})
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.el.removeEventListener("click", this.markOpened, {capture: true})
|
||||
},
|
||||
},
|
||||
|
||||
SearchableCategoryPicker: {
|
||||
mounted() {
|
||||
this.refresh = () => {
|
||||
|
|
|
|||
|
|
@ -189,7 +189,9 @@ test("activity approval, privacy controls, reporting, and moderation work end to
|
|||
await gotoLiveView(participant.page, activityURL);
|
||||
await waitForMapReady(participant.page);
|
||||
await participant.page.getByRole("button", { name: "Block organizer" }).click();
|
||||
await expect(participant.page).toHaveURL(/\/activities$/);
|
||||
await expect
|
||||
.poll(() => new URL(participant.page.url()).pathname)
|
||||
.toBe("/activities");
|
||||
await waitForMapReady(participant.page);
|
||||
await gotoLiveView(participant.page, "/profile");
|
||||
const blockedUsers = participant.page.getByRole("heading", { name: "Blocked users" }).locator("..");
|
||||
|
|
@ -237,36 +239,40 @@ test("activity approval, privacy controls, reporting, and moderation work end to
|
|||
await expect(admin.page.getByRole("heading", { name: "Trust and safety" })).toBeVisible();
|
||||
|
||||
const reportCard = admin.page
|
||||
.locator("article")
|
||||
.locator("details")
|
||||
.filter({ hasText: reportDescription });
|
||||
await reportCard.locator("summary").click();
|
||||
await reportCard.getByRole("button", { name: "View scoped evidence" }).click();
|
||||
const evidence = admin.page
|
||||
.getByRole("heading", { name: "Reported evidence" })
|
||||
.locator("xpath=ancestor::section");
|
||||
await expect(evidence.getByText(organizerMessageText)).toBeVisible();
|
||||
await evidence.getByRole("button", { name: "Close" }).click();
|
||||
await reportCard.getByRole("combobox").selectOption("resolved");
|
||||
await reportCard.getByPlaceholder("Resolution note").fill("Reviewed in isolated browser E2E.");
|
||||
await reportCard.getByRole("button", { name: "Save" }).click();
|
||||
await reportCard.locator("summary").click();
|
||||
await reportCard.getByRole("combobox", { name: "Decision" }).selectOption("resolved");
|
||||
await reportCard.getByLabel("Resolution note").fill("Reviewed in isolated browser E2E.");
|
||||
await reportCard.getByRole("button", { name: "Save decision" }).click();
|
||||
await expect(admin.page.getByText("Report updated.")).toBeVisible();
|
||||
|
||||
await gotoLiveView(admin.page, "/moderation?queue=proposals");
|
||||
const proposalCard = admin.page
|
||||
.locator("article")
|
||||
.locator("details")
|
||||
.filter({ hasText: proposalTitle });
|
||||
await proposalCard.getByPlaceholder("Rejection note").fill("Covered by an existing category.");
|
||||
await proposalCard.locator(":scope > summary").click();
|
||||
await proposalCard.getByLabel("Rejection note").fill("Covered by an existing category.");
|
||||
await proposalCard.getByRole("button", { name: "Reject" }).click();
|
||||
await expect(admin.page.getByText("Proposal rejected.")).toBeVisible();
|
||||
|
||||
await gotoLiveView(admin.page, "/admin/users");
|
||||
await admin.page.getByLabel("Search users").fill(participantEmail);
|
||||
await expect(admin.page.locator("main article")).toHaveCount(1);
|
||||
const participantRow = admin.page
|
||||
.locator("article")
|
||||
.locator("main tbody tr")
|
||||
.filter({ hasText: participantEmail });
|
||||
await expect(participantRow).toHaveCount(1);
|
||||
await participantRow.getByRole("combobox").first().selectOption("restricted");
|
||||
await participantRow.getByLabel("Internal note").fill("E2E restriction boundary.");
|
||||
await participantRow.getByRole("button", { name: "Save" }).click();
|
||||
await participantRow.getByRole("link", { name: /^Manage / }).click();
|
||||
await admin.page.getByLabel("Status").selectOption("restricted");
|
||||
await admin.page.getByLabel("Internal note").fill("E2E restriction boundary.");
|
||||
await admin.page.getByRole("button", { name: "Save account status" }).click();
|
||||
await expect(admin.page.getByText("Account status updated.")).toBeVisible();
|
||||
|
||||
await gotoLiveView(participant.page, "/categories/proposals");
|
||||
|
|
|
|||
|
|
@ -70,8 +70,10 @@ test("two users complete medicine tracking, handover, realtime chat, and blind r
|
|||
await expect(requester.page.getByRole("heading", { name: requestTitle })).toBeVisible();
|
||||
|
||||
await gotoLiveView(helper.page, requestURL);
|
||||
await helper.page.getByRole("button", { name: "I can help" }).click();
|
||||
await expect(helper.page.getByRole("heading", { name: "Private match chat" })).toBeVisible();
|
||||
await clickUntilVisible(
|
||||
helper.page.getByRole("button", { name: "I can help" }),
|
||||
helper.page.getByRole("heading", { name: "Private match chat" }),
|
||||
);
|
||||
await expect(requester.page.getByRole("heading", { name: "Private match chat" })).toBeVisible();
|
||||
|
||||
await helper.page
|
||||
|
|
@ -207,10 +209,10 @@ test("two users complete medicine tracking, handover, realtime chat, and blind r
|
|||
).toBeVisible();
|
||||
|
||||
await gotoLiveView(replacement.page, roadsideURL);
|
||||
await replacement.page.getByRole("button", { name: "I can help" }).click();
|
||||
await expect(
|
||||
await clickUntilVisible(
|
||||
replacement.page.getByRole("button", { name: "I can help" }),
|
||||
replacement.page.getByRole("heading", { name: "Private match chat" }),
|
||||
).toBeVisible();
|
||||
);
|
||||
const replacementName =
|
||||
runID && fixturePassword
|
||||
? "Staging E2E Replacement Helper"
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ test("nearby alerts, private notification inbox, preferences, and data export wo
|
|||
const inbox = subscriber.page
|
||||
.getByRole("heading", { name: "Inbox" })
|
||||
.locator("xpath=ancestor::section");
|
||||
const notification = inbox.getByRole("button", { name: /New help request nearby/ });
|
||||
const notification = inbox.getByRole("link", { name: /New help request nearby/ });
|
||||
await expect(notification).toBeVisible({ timeout: 20_000 });
|
||||
await expect(inbox.getByText(privateArea)).toHaveCount(0);
|
||||
await notification.click();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ defmodule WhoNeedHelpWeb.NotificationOpenController do
|
|||
|
||||
def create(conn, %{"id" => id}) do
|
||||
case Notifications.notification_opened(conn.assigns.current_scope, id) do
|
||||
{:ok, _notification} -> send_resp(conn, :no_content, "")
|
||||
{:ok, notification} -> redirect(conn, to: notification.path)
|
||||
{:error, :not_found} -> send_resp(conn, :not_found, "")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -282,11 +282,9 @@ defmodule WhoNeedHelpWeb.NotificationLive do
|
|||
<div :if={@notifications != []} class="mt-4 divide-y divide-base-300">
|
||||
<.link
|
||||
:for={notification <- @notifications}
|
||||
navigate={notification.path}
|
||||
href={~p"/notifications/#{notification.id}/opened"}
|
||||
method="post"
|
||||
id={"notification-#{notification.id}"}
|
||||
phx-hook="NotificationLink"
|
||||
data-open-url={~p"/notifications/#{notification.id}/opened"}
|
||||
data-notification-id={notification.id}
|
||||
class={[
|
||||
"flex w-full items-start gap-3 px-2 py-4 text-left transition hover:bg-base-200",
|
||||
is_nil(notification.read_at) && "font-semibold"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule WhoNeedHelpWeb.NotificationOpenControllerTest do
|
|||
|
||||
import WhoNeedHelp.AccountsFixtures
|
||||
|
||||
test "marks an owned notification as read without redirecting", %{conn: conn} do
|
||||
test "marks an owned notification as read and redirects to its destination", %{conn: conn} do
|
||||
user = user_fixture()
|
||||
|
||||
{:ok, notification} =
|
||||
|
|
@ -21,7 +21,7 @@ defmodule WhoNeedHelpWeb.NotificationOpenControllerTest do
|
|||
|
||||
conn = conn |> log_in_user(user) |> post(~p"/notifications/#{notification.id}/opened")
|
||||
|
||||
assert response(conn, 204) == ""
|
||||
assert redirected_to(conn) == "/support"
|
||||
assert Repo.get!(Notification, notification.id).read_at
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
|||
|
||||
assert has_element?(
|
||||
view,
|
||||
"a[href='/support'][phx-hook='NotificationLink'][data-open-url='/notifications/#{notification.id}/opened'][data-notification-id='#{notification.id}']"
|
||||
"a[href='/notifications/#{notification.id}/opened'][data-method='post']"
|
||||
)
|
||||
|
||||
view
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user