Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ReactNode } from "react";
import { render, screen } from "@testing-library/react";

import { createLinkRenderer } from "@/components/drops/view/part/dropPartMarkdown/linkHandlers";
import { ensureStableSeizeLink } from "@/helpers/SeizeLinkParser";
import { publicEnv } from "@/config/env";

jest.mock("@/components/drops/view/part/dropPartMarkdown/youtubePreview", () => ({
Expand All @@ -14,7 +15,9 @@ jest.mock("@/components/drops/view/part/dropPartMarkdown/youtubePreview", () =>

jest.mock("@/components/drops/view/part/DropPartMarkdownImage", () => ({
__esModule: true,
default: ({ src }: { src: string }) => <img data-testid="markdown-image" src={src} alt="" />,
default: ({ src }: { src: string }) => (
<img data-testid="markdown-image" src={src} alt="" />
),
}));

jest.mock("@/components/drops/view/part/dropPartMarkdown/renderers", () => ({
Expand Down Expand Up @@ -100,13 +103,41 @@ jest.mock("@/components/waves/list/WaveItemChat", () => ({

jest.mock("@/components/waves/drops/DropItemChat", () => ({
__esModule: true,
default: ({ dropId }: { dropId: string }) => <div data-testid="drop-card" data-drop={dropId} />,
default: ({ dropId, href }: { dropId: string; href: string }) => (
<div data-testid="drop-card" data-drop={dropId} data-href={href}>
<div data-testid="chat-buttons" data-href={href} />
</div>
),
}));

jest.mock("@/helpers/SeizeLinkParser", () => {
const actual = jest.requireActual("@/helpers/SeizeLinkParser");
let currentHrefOverride: string | undefined;

const ensureStableSeizeLink = (href: string) =>
actual.ensureStableSeizeLink(href, currentHrefOverride);

(ensureStableSeizeLink as any).__setCurrentHref = (href?: string) => {
currentHrefOverride = href;
};

return {
...actual,
ensureStableSeizeLink,
};
});

const onQuoteClick = jest.fn();

const baseRenderer = () => createLinkRenderer({ onQuoteClick });

const setEnsureCurrentHref = (href?: string) => {
const setter = (ensureStableSeizeLink as any).__setCurrentHref;
if (typeof setter === "function") {
setter(href);
}
};

describe("createLinkRenderer", () => {
const FALLBACK_BASE_ENDPOINT = "https://6529.io";
const originalBaseEndpointEnv = publicEnv.BASE_ENDPOINT;
Expand All @@ -116,6 +147,7 @@ describe("createLinkRenderer", () => {
jest.clearAllMocks();
publicEnv.BASE_ENDPOINT = FALLBACK_BASE_ENDPOINT;
process.env.BASE_ENDPOINT = FALLBACK_BASE_ENDPOINT;
setEnsureCurrentHref();
});

afterEach(() => {
Expand All @@ -125,6 +157,7 @@ describe("createLinkRenderer", () => {
} else {
process.env.BASE_ENDPOINT = originalProcessBaseEndpoint;
}
setEnsureCurrentHref();
});

it("renders DropPartMarkdownImage for img elements", () => {
Expand Down Expand Up @@ -182,6 +215,38 @@ describe("createLinkRenderer", () => {
expect(screen.getByTestId("drop-card")).toHaveAttribute("data-drop", "def");
});

it("normalizes root drop links using current location context", () => {
setEnsureCurrentHref("https://6529.io/messages?wave=current-wave");

const { renderAnchor } = baseRenderer();
const element = renderAnchor({ href: "https://6529.io/?drop=drop-123" } as any);
render(<>{element}</>);

expect(screen.getByTestId("drop-card")).toHaveAttribute("data-drop", "drop-123");
expect(screen.getByTestId("chat-buttons")).toHaveAttribute(
"data-href",
"https://6529.io/messages?wave=current-wave&drop=drop-123"
);
setEnsureCurrentHref();
});

it("normalizes drop links shared from other paths", () => {
setEnsureCurrentHref("https://6529.io/messages?wave=current-wave");

const { renderAnchor } = baseRenderer();
const element = renderAnchor({
href: "https://6529.io/waves?wave=other-wave&drop=drop-456",
} as any);
render(<>{element}</>);

expect(screen.getByTestId("drop-card")).toHaveAttribute("data-drop", "drop-456");
expect(screen.getByTestId("chat-buttons")).toHaveAttribute(
"data-href",
"https://6529.io/messages?wave=current-wave&drop=drop-456"
);
setEnsureCurrentHref();
});

it.each([
["standard status link", "https://twitter.com/user/status/987654321"],
["mobile twitter link", "https://mobile.twitter.com/user/status/987654321"],
Expand Down
71 changes: 71 additions & 0 deletions __tests__/helpers/SeizeLinkParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
describe("SeizeLinkParser with mocked BASE_ENDPOINT", () => {
let parseSeizeQuoteLink: any;
let parseSeizeQueryLink: any;
let ensureStableSeizeLink: any;

beforeAll(() => {
jest.resetModules();
Expand All @@ -13,6 +14,7 @@ describe("SeizeLinkParser with mocked BASE_ENDPOINT", () => {
({
parseSeizeQuoteLink,
parseSeizeQueryLink,
ensureStableSeizeLink,
} = require("@/helpers/SeizeLinkParser"));
});

Expand Down Expand Up @@ -79,4 +81,73 @@ describe("SeizeLinkParser with mocked BASE_ENDPOINT", () => {
expect(res).toBeNull();
});
});

describe("ensureStableSeizeLink", () => {
it("returns original href for non-base URLs", () => {
const incoming = "https://othersite.com/?drop=drop-id";
const current = "https://site.com/messages?wave=abc";
expect(ensureStableSeizeLink(incoming, current)).toBe(incoming);
});

it("returns original href when drop param missing", () => {
const incoming = "https://site.com/";
const current = "https://site.com/messages?wave=abc";
expect(ensureStableSeizeLink(incoming, current)).toBe(incoming);
});

it("rewrites root drop link to current path with drop param", () => {
const incoming = "https://site.com/?drop=drop-id";
const current = "https://site.com/messages?wave=abc";
expect(ensureStableSeizeLink(incoming, current)).toBe(
"https://site.com/messages?wave=abc&drop=drop-id"
);
});

it("handles relative drop links", () => {
const incoming = "?drop=drop-id";
const current = "https://site.com/messages";
expect(ensureStableSeizeLink(incoming, current)).toBe(
"https://site.com/messages?drop=drop-id"
);
});

it("preserves existing query params and replaces drop", () => {
const incoming = "https://site.com/?drop=new-drop";
const current = "https://site.com/messages?wave=abc&drop=old-drop";
expect(ensureStableSeizeLink(incoming, current)).toBe(
"https://site.com/messages?wave=abc&drop=new-drop"
);
});

it("rebases drop links from other paths onto current location", () => {
const incoming = "https://site.com/waves?wave=abc&drop=def";
const current = "https://site.com/messages?wave=xyz";
expect(ensureStableSeizeLink(incoming, current)).toBe(
"https://site.com/messages?wave=xyz&drop=def"
);
});

it("refreshes cached origin when BASE_ENDPOINT changes", () => {
const { publicEnv } = require("@/config/env");

expect(
ensureStableSeizeLink(
"https://site.com/?drop=drop-1",
"https://site.com/messages"
)
).toBe("https://site.com/messages?drop=drop-1");

publicEnv.BASE_ENDPOINT = "https://other.com";
try {
expect(
ensureStableSeizeLink(
"https://other.com/?drop=drop-2",
"https://other.com/messages"
)
).toBe("https://other.com/messages?drop=drop-2");
} finally {
publicEnv.BASE_ENDPOINT = "https://site.com";
}
});
});
});
16 changes: 13 additions & 3 deletions components/drops/view/part/dropPartMarkdown/handlers/seize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApiDrop } from "@/generated/models/ApiDrop";
import {
parseSeizeQuoteLink,
parseSeizeQueryLink,
getSeizeBaseOrigin,
type SeizeQuoteLinkInfo,
} from "@/helpers/SeizeLinkParser";

Expand Down Expand Up @@ -91,12 +92,21 @@ const createSeizeWaveHandler = (): LinkHandler =>
);

const getDropId = (href: string): string | null => {
const result = parseSeizeQueryLink(href, "/waves", ["wave", "drop"], true);
if (!result || typeof result.drop !== "string") {
const baseOrigin = getSeizeBaseOrigin();
if (!baseOrigin) {
return null;
}

return result.drop;
try {
const url = new URL(href, baseOrigin);
if (url.origin !== baseOrigin) {
return null;
}
const dropId = url.searchParams.get("drop");
return dropId ?? null;
} catch {
return null;
}
};

const createSeizeDropHandler = (): LinkHandler =>
Expand Down
39 changes: 26 additions & 13 deletions components/drops/view/part/dropPartMarkdown/linkHandlers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ErrorBoundary } from "react-error-boundary";
import { ExtraProps } from "react-markdown";

import { ApiDrop } from "@/generated/models/ApiDrop";
import { ensureStableSeizeLink } from "@/helpers/SeizeLinkParser";

import LinkPreviewCard from "@/components/waves/LinkPreviewCard";
import DropPartMarkdownImage from "../DropPartMarkdownImage";
Expand Down Expand Up @@ -68,21 +69,28 @@ export const createLinkRenderer = ({

const renderAnchor: LinkRenderer["renderAnchor"] = (props) => {
const { href } = props;
if (!href || !isValidLink(href)) {
if (!href) {
return null;
}

const parsedUrl = parseUrl(href);
const renderFallbackAnchor = () => renderExternalOrInternalLink(href, props);
const matchSeize = findMatch(seizeHandlers, href);
const stableHref = ensureStableSeizeLink(href);
if (!isValidLink(stableHref)) {
return null;
}

const parsedUrl = parseUrl(stableHref);
const anchorProps = { ...props, href: stableHref };
const renderFallbackAnchor = () =>
renderExternalOrInternalLink(stableHref, anchorProps);
const matchSeize = findMatch(seizeHandlers, stableHref);
const renderOpenGraph = () => {
if (!shouldUseOpenGraphPreview(href, parsedUrl)) {
if (!shouldUseOpenGraphPreview(stableHref, parsedUrl)) {
return renderFallbackAnchor();
}

return (
<LinkPreviewCard
href={href}
href={stableHref}
renderFallback={() => renderFallbackAnchor()}
/>
);
Expand Down Expand Up @@ -117,7 +125,7 @@ export const createLinkRenderer = ({

const renderFromHandler = (handler: LinkHandler): ReactElement | null => {
try {
const rendered = handler.render(href);
const rendered = handler.render(stableHref);
if (rendered === null || rendered === undefined) {
throw new Error("Link handler returned no content");
}
Expand All @@ -138,7 +146,7 @@ export const createLinkRenderer = ({
}
}

const matchExternal = findMatch(handlers, href);
const matchExternal = findMatch(handlers, stableHref);

if (matchExternal) {
const rendered = renderFromHandler(matchExternal);
Expand All @@ -156,22 +164,27 @@ export const createLinkRenderer = ({
};

const isSmartLink = (href: string): boolean => {
if (!href || !isValidLink(href)) {
if (!href) {
return false;
}

const stableHref = ensureStableSeizeLink(href);
if (!isValidLink(stableHref)) {
return false;
}

const parsedUrl = parseUrl(href);
const seizeMatch = findMatch(seizeHandlers, href);
const parsedUrl = parseUrl(stableHref);
const seizeMatch = findMatch(seizeHandlers, stableHref);
if (seizeMatch) {
return seizeMatch.display === "block";
}

const match = findMatch(handlers, href);
const match = findMatch(handlers, stableHref);
if (match) {
return match.display === "block";
}

return shouldUseOpenGraphPreview(href, parsedUrl);
return shouldUseOpenGraphPreview(stableHref, parsedUrl);
};

return {
Expand Down
Loading