From 316698bc8f96a0a57b48bb99a1bcc719edd03a29 Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Sat, 12 Nov 2022 15:29:51 -0500 Subject: [PATCH] add incident web share (#12) --- src/lanco-incidents-app/package.json | 2 +- src/lanco-incidents-app/rome.json | 12 ++ .../src/components/page-title.tsx | 34 +++- .../src/containers/layout.tsx | 2 +- .../src/hooks/use-web-share.test.tsx | 148 ++++++++++++++++++ .../src/hooks/use-web-share.ts | 36 +++++ .../pages/incident-detail/incident-detail.tsx | 20 ++- 7 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 src/lanco-incidents-app/rome.json create mode 100644 src/lanco-incidents-app/src/hooks/use-web-share.test.tsx create mode 100644 src/lanco-incidents-app/src/hooks/use-web-share.ts diff --git a/src/lanco-incidents-app/package.json b/src/lanco-incidents-app/package.json index 7c9b809..5996011 100644 --- a/src/lanco-incidents-app/package.json +++ b/src/lanco-incidents-app/package.json @@ -6,7 +6,7 @@ "serve": "vite preview", "coverage": "vitest run --coverage", "coverage-report": "vitest run --coverage && codecov --disable=gcov", - "lint": "tsc --noEmit && eslint . --ext .js,.jsx,.ts,.tsx --fix && rome check --apply", + "lint": "tsc --noEmit && eslint . --ext .js,.jsx,.ts,.tsx --fix && rome check --apply ./src", "test": "vitest" }, "dependencies": { diff --git a/src/lanco-incidents-app/rome.json b/src/lanco-incidents-app/rome.json new file mode 100644 index 0000000..73cdd6f --- /dev/null +++ b/src/lanco-incidents-app/rome.json @@ -0,0 +1,12 @@ +{ + "formatter": { + "indentStyle": "space", + "indentSize": 4 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} \ No newline at end of file diff --git a/src/lanco-incidents-app/src/components/page-title.tsx b/src/lanco-incidents-app/src/components/page-title.tsx index 1f73dd0..fa10633 100644 --- a/src/lanco-incidents-app/src/components/page-title.tsx +++ b/src/lanco-incidents-app/src/components/page-title.tsx @@ -2,13 +2,17 @@ import React, { PropsWithChildren } from "react"; interface PageTitleProps { onBack?: () => void; + onShare?: () => void; showBackButton?: boolean; + showShareButton?: boolean; } const PageTitle: React.FC> = ({ children, onBack, + onShare, showBackButton = true, + showShareButton = false, }) => { return (
@@ -16,12 +20,14 @@ const PageTitle: React.FC> = ({ )} -
{children}
+
{children}
+ {showShareButton && ( + + )}
); }; diff --git a/src/lanco-incidents-app/src/containers/layout.tsx b/src/lanco-incidents-app/src/containers/layout.tsx index d1c982a..6af4ce7 100644 --- a/src/lanco-incidents-app/src/containers/layout.tsx +++ b/src/lanco-incidents-app/src/containers/layout.tsx @@ -37,7 +37,7 @@ export default function Layout({ className={`z-40 bg-blue-900 text-gray-50 ${headerShadowClass}`} >
-
+
{headerLeft}
{isUpdating ? null : headerRight} diff --git a/src/lanco-incidents-app/src/hooks/use-web-share.test.tsx b/src/lanco-incidents-app/src/hooks/use-web-share.test.tsx new file mode 100644 index 0000000..011d253 --- /dev/null +++ b/src/lanco-incidents-app/src/hooks/use-web-share.test.tsx @@ -0,0 +1,148 @@ +/** + * @vitest-environment jsdom + */ + +import { act, renderHook, waitFor } from "@testing-library/react"; +import { describe, it, expect, afterEach, vi, beforeEach, Mock } from "vitest"; + +import { useWebShare } from "hooks/use-web-share"; + +describe("useWebShare", () => { + it("initializes", async () => { + // Arrange, Act + const { result } = renderHook(() => { + return useWebShare(); + }); + + // Assert + expect(result.current).toBeDefined(); + expect(result.current).toBeTypeOf("object"); + expect(result.current.isSharing).toBe(false); + expect(result.current.enabled).toBeDefined(); + expect(result.current.error).toBeUndefined(); + expect(result.current.share).toBeTypeOf("function"); + }); + + describe("when no browser sharing capability", () => { + it("is not enabled", async () => { + // Arrange, Act + const { result } = renderHook(() => { + return useWebShare(); + }); + + // Act + const { enabled } = result.current; + + // Assert + expect(enabled).toBe(false); + }); + }); + + describe("when browser has sharing capability", () => { + beforeEach(() => { + vi.stubGlobal("navigator", { share: vi.fn() }); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it("is enabled", async () => { + // Arrange, Act + const { result } = renderHook(() => { + return useWebShare(); + }); + + // Act + const { enabled } = result.current; + + // Assert + expect(enabled).toBe(true); + }); + }); + + describe("share()", () => { + describe("when browser has sharing capability", () => { + afterEach(() => { + vi.resetAllMocks(); + }); + + it("calls navigator.share", async () => { + // Arrange + const mockShare: Mock> = vi.fn(() => + Promise.resolve() + ); + vi.stubGlobal("navigator", { share: mockShare }); + const { result } = renderHook(() => { + return useWebShare(); + }); + + // Act + act(() => { + result.current.share({}); + }); + + // Assert + expect(mockShare).toBeCalled(); + }); + + describe("when successful", () => { + it("is updates isSharing", async () => { + // Arrange + vi.stubGlobal("navigator", { + share: () => Promise.resolve(), + }); + const all: ReturnType[] = []; + const { result, rerender } = renderHook(() => { + const value = useWebShare(); + all.push(value); + return value; + }); + + // Act + act(() => { + result.current.share({}); + }); + + // Assert + expect(result.current.isSharing).toBe(true); + + await waitFor(() => !result.current.isSharing); + rerender(); + + expect(result.current.isSharing).toBe(false); + }); + }); + + describe("when errored", () => { + it("is updates error", async () => { + // Arrange + const errorMessage = "This is an error"; + vi.stubGlobal("navigator", { + share: () => Promise.reject(errorMessage), + }); + const all: ReturnType[] = []; + const { result, rerender } = renderHook(() => { + const value = useWebShare(); + all.push(value); + return value; + }); + + // Act + act(() => { + result.current.share({}); + }); + + // Assert + expect(result.current.isSharing).toBe(true); + + await waitFor(() => !result.current.isSharing); + rerender(); + + expect(result.current.error).toBe(errorMessage); + expect(result.current.isSharing).toBe(false); + }); + }); + }); + }); +}); diff --git a/src/lanco-incidents-app/src/hooks/use-web-share.ts b/src/lanco-incidents-app/src/hooks/use-web-share.ts new file mode 100644 index 0000000..5467e0d --- /dev/null +++ b/src/lanco-incidents-app/src/hooks/use-web-share.ts @@ -0,0 +1,36 @@ +import { useState } from "react"; + +export function useWebShare() { + const [error, setError] = useState(); + const [isSharing, setIsSharing] = useState(false); + const enabled = window.navigator.share != null; + + const share = ({ title, text, url }: Omit) => { + if (!navigator.share) { + return; + } + + setIsSharing(true); + + navigator + .share({ title, text, url }) + .catch((error) => { + const errorMessage: string = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : error?.toString(); + + setError(errorMessage); + }) + .finally(() => setIsSharing(false)); + }; + + return { + isSharing, + enabled, + error, + share, + }; +} diff --git a/src/lanco-incidents-app/src/pages/incident-detail/incident-detail.tsx b/src/lanco-incidents-app/src/pages/incident-detail/incident-detail.tsx index e5076e7..13f6ab1 100644 --- a/src/lanco-incidents-app/src/pages/incident-detail/incident-detail.tsx +++ b/src/lanco-incidents-app/src/pages/incident-detail/incident-detail.tsx @@ -5,24 +5,34 @@ import { IncidentDetailContent } from "./incident-detail-content"; import PageTitle from "components/page-title"; import { IncidentRecord } from "models/view-models/incident-record"; import useIncident from "hooks/use-incident"; +import { useWebShare } from "hooks/use-web-share"; -interface IncidentDetailProps {} - -export default function IncidentDetail(props: IncidentDetailProps) { +export default function IncidentDetail() { const { id } = useParams<"id">(); const navigate = useNavigate(); const { incident } = useIncident({ id }); + const webShare = useWebShare(); const handleGoBack = () => navigate(-1); + const handleShare = () => + webShare.share({ + url: window.location.href, + }); + return ( + {getTitle(incident)} - }> + } + > );