Make notification navigation reliable
This commit is contained in:
parent
ead8a67d0f
commit
5e5478389d
|
|
@ -1037,39 +1037,6 @@ export const mountStaticAidMaps = root => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Hooks = {
|
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: {
|
SearchableCategoryPicker: {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.refresh = () => {
|
this.refresh = () => {
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,9 @@ test("activity approval, privacy controls, reporting, and moderation work end to
|
||||||
await gotoLiveView(participant.page, activityURL);
|
await gotoLiveView(participant.page, activityURL);
|
||||||
await waitForMapReady(participant.page);
|
await waitForMapReady(participant.page);
|
||||||
await participant.page.getByRole("button", { name: "Block organizer" }).click();
|
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 waitForMapReady(participant.page);
|
||||||
await gotoLiveView(participant.page, "/profile");
|
await gotoLiveView(participant.page, "/profile");
|
||||||
const blockedUsers = participant.page.getByRole("heading", { name: "Blocked users" }).locator("..");
|
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();
|
await expect(admin.page.getByRole("heading", { name: "Trust and safety" })).toBeVisible();
|
||||||
|
|
||||||
const reportCard = admin.page
|
const reportCard = admin.page
|
||||||
.locator("article")
|
.locator("details")
|
||||||
.filter({ hasText: reportDescription });
|
.filter({ hasText: reportDescription });
|
||||||
|
await reportCard.locator("summary").click();
|
||||||
await reportCard.getByRole("button", { name: "View scoped evidence" }).click();
|
await reportCard.getByRole("button", { name: "View scoped evidence" }).click();
|
||||||
const evidence = admin.page
|
const evidence = admin.page
|
||||||
.getByRole("heading", { name: "Reported evidence" })
|
.getByRole("heading", { name: "Reported evidence" })
|
||||||
.locator("xpath=ancestor::section");
|
.locator("xpath=ancestor::section");
|
||||||
await expect(evidence.getByText(organizerMessageText)).toBeVisible();
|
await expect(evidence.getByText(organizerMessageText)).toBeVisible();
|
||||||
await evidence.getByRole("button", { name: "Close" }).click();
|
await evidence.getByRole("button", { name: "Close" }).click();
|
||||||
await reportCard.getByRole("combobox").selectOption("resolved");
|
await reportCard.locator("summary").click();
|
||||||
await reportCard.getByPlaceholder("Resolution note").fill("Reviewed in isolated browser E2E.");
|
await reportCard.getByRole("combobox", { name: "Decision" }).selectOption("resolved");
|
||||||
await reportCard.getByRole("button", { name: "Save" }).click();
|
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 expect(admin.page.getByText("Report updated.")).toBeVisible();
|
||||||
|
|
||||||
|
await gotoLiveView(admin.page, "/moderation?queue=proposals");
|
||||||
const proposalCard = admin.page
|
const proposalCard = admin.page
|
||||||
.locator("article")
|
.locator("details")
|
||||||
.filter({ hasText: proposalTitle });
|
.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 proposalCard.getByRole("button", { name: "Reject" }).click();
|
||||||
await expect(admin.page.getByText("Proposal rejected.")).toBeVisible();
|
await expect(admin.page.getByText("Proposal rejected.")).toBeVisible();
|
||||||
|
|
||||||
await gotoLiveView(admin.page, "/admin/users");
|
await gotoLiveView(admin.page, "/admin/users");
|
||||||
await admin.page.getByLabel("Search users").fill(participantEmail);
|
await admin.page.getByLabel("Search users").fill(participantEmail);
|
||||||
await expect(admin.page.locator("main article")).toHaveCount(1);
|
|
||||||
const participantRow = admin.page
|
const participantRow = admin.page
|
||||||
.locator("article")
|
.locator("main tbody tr")
|
||||||
.filter({ hasText: participantEmail });
|
.filter({ hasText: participantEmail });
|
||||||
await expect(participantRow).toHaveCount(1);
|
await expect(participantRow).toHaveCount(1);
|
||||||
await participantRow.getByRole("combobox").first().selectOption("restricted");
|
await participantRow.getByRole("link", { name: /^Manage / }).click();
|
||||||
await participantRow.getByLabel("Internal note").fill("E2E restriction boundary.");
|
await admin.page.getByLabel("Status").selectOption("restricted");
|
||||||
await participantRow.getByRole("button", { name: "Save" }).click();
|
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 expect(admin.page.getByText("Account status updated.")).toBeVisible();
|
||||||
|
|
||||||
await gotoLiveView(participant.page, "/categories/proposals");
|
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 expect(requester.page.getByRole("heading", { name: requestTitle })).toBeVisible();
|
||||||
|
|
||||||
await gotoLiveView(helper.page, requestURL);
|
await gotoLiveView(helper.page, requestURL);
|
||||||
await helper.page.getByRole("button", { name: "I can help" }).click();
|
await clickUntilVisible(
|
||||||
await expect(helper.page.getByRole("heading", { name: "Private match chat" })).toBeVisible();
|
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 expect(requester.page.getByRole("heading", { name: "Private match chat" })).toBeVisible();
|
||||||
|
|
||||||
await helper.page
|
await helper.page
|
||||||
|
|
@ -207,10 +209,10 @@ test("two users complete medicine tracking, handover, realtime chat, and blind r
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await gotoLiveView(replacement.page, roadsideURL);
|
await gotoLiveView(replacement.page, roadsideURL);
|
||||||
await replacement.page.getByRole("button", { name: "I can help" }).click();
|
await clickUntilVisible(
|
||||||
await expect(
|
replacement.page.getByRole("button", { name: "I can help" }),
|
||||||
replacement.page.getByRole("heading", { name: "Private match chat" }),
|
replacement.page.getByRole("heading", { name: "Private match chat" }),
|
||||||
).toBeVisible();
|
);
|
||||||
const replacementName =
|
const replacementName =
|
||||||
runID && fixturePassword
|
runID && fixturePassword
|
||||||
? "Staging E2E Replacement Helper"
|
? "Staging E2E Replacement Helper"
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ test("nearby alerts, private notification inbox, preferences, and data export wo
|
||||||
const inbox = subscriber.page
|
const inbox = subscriber.page
|
||||||
.getByRole("heading", { name: "Inbox" })
|
.getByRole("heading", { name: "Inbox" })
|
||||||
.locator("xpath=ancestor::section");
|
.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(notification).toBeVisible({ timeout: 20_000 });
|
||||||
await expect(inbox.getByText(privateArea)).toHaveCount(0);
|
await expect(inbox.getByText(privateArea)).toHaveCount(0);
|
||||||
await notification.click();
|
await notification.click();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ defmodule WhoNeedHelpWeb.NotificationOpenController do
|
||||||
|
|
||||||
def create(conn, %{"id" => id}) do
|
def create(conn, %{"id" => id}) do
|
||||||
case Notifications.notification_opened(conn.assigns.current_scope, 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, "")
|
{:error, :not_found} -> send_resp(conn, :not_found, "")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -282,11 +282,9 @@ defmodule WhoNeedHelpWeb.NotificationLive do
|
||||||
<div :if={@notifications != []} class="mt-4 divide-y divide-base-300">
|
<div :if={@notifications != []} class="mt-4 divide-y divide-base-300">
|
||||||
<.link
|
<.link
|
||||||
:for={notification <- @notifications}
|
:for={notification <- @notifications}
|
||||||
navigate={notification.path}
|
href={~p"/notifications/#{notification.id}/opened"}
|
||||||
|
method="post"
|
||||||
id={"notification-#{notification.id}"}
|
id={"notification-#{notification.id}"}
|
||||||
phx-hook="NotificationLink"
|
|
||||||
data-open-url={~p"/notifications/#{notification.id}/opened"}
|
|
||||||
data-notification-id={notification.id}
|
|
||||||
class={[
|
class={[
|
||||||
"flex w-full items-start gap-3 px-2 py-4 text-left transition hover:bg-base-200",
|
"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"
|
is_nil(notification.read_at) && "font-semibold"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule WhoNeedHelpWeb.NotificationOpenControllerTest do
|
||||||
|
|
||||||
import WhoNeedHelp.AccountsFixtures
|
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()
|
user = user_fixture()
|
||||||
|
|
||||||
{:ok, notification} =
|
{:ok, notification} =
|
||||||
|
|
@ -21,7 +21,7 @@ defmodule WhoNeedHelpWeb.NotificationOpenControllerTest do
|
||||||
|
|
||||||
conn = conn |> log_in_user(user) |> post(~p"/notifications/#{notification.id}/opened")
|
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
|
assert Repo.get!(Notification, notification.id).read_at
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
|
|
||||||
assert has_element?(
|
assert has_element?(
|
||||||
view,
|
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
|
view
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user