Skip to content
Closed
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
79 changes: 71 additions & 8 deletions __tests__/components/drops/view/part/DropPartMarkdown.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
import { render, screen } from "@testing-library/react";
import DropPartMarkdown from "../../../../../components/drops/view/part/DropPartMarkdown";
import type { ReactElement } from "react";

jest.mock("../../../../../hooks/isMobileScreen", () => () => false);
jest.mock("../../../../../contexts/EmojiContext", () => ({
useEmoji: () => ({ emojiMap: [] }),
}));
jest.mock("react-tweet", () => ({
Tweet: ({ id }: any) => <div>tweet:{id}</div>,
}));
type DropPartMarkdownType = typeof import("../../../../../components/drops/view/part/DropPartMarkdown").default;

describe("DropPartMarkdown", () => {
let DropPartMarkdown: DropPartMarkdownType;
let dynamicSpy: jest.Mock;

const loadComponent = async (): Promise<void> => {
jest.resetModules();
dynamicSpy = jest.fn();

jest.doMock("../../../../../hooks/isMobileScreen", () => () => false);
jest.doMock("../../../../../contexts/EmojiContext", () => ({
useEmoji: () => ({
emojiMap: [],
findNativeEmoji: () => undefined,
}),
}));
jest.doMock("react-tweet", () => ({
Tweet: ({ id }: any) => <div>tweet:{id}</div>,
}));
jest.doMock("next/dynamic", () => ({
__esModule: true,
default: (loader: any, options: any) => {
dynamicSpy(loader, options);
const LoadingComponent = options?.loading;
const DynamicComponent = (props: any): ReactElement | null =>
LoadingComponent ? <LoadingComponent {...props} /> : null;

(DynamicComponent as any).__loader = loader;
(DynamicComponent as any).__options = options;

return DynamicComponent;
},
}));

DropPartMarkdown = (
await import("../../../../../components/drops/view/part/DropPartMarkdown")
).default;
};

beforeEach(async () => {
await loadComponent();
});

afterEach(() => {
jest.resetModules();
delete process.env.BASE_ENDPOINT;
});

it("renders gif embeds", () => {
const content = "Check this ![gif](https://media.tenor.com/test.gif)";
render(
Expand Down Expand Up @@ -57,4 +97,27 @@ describe("DropPartMarkdown", () => {
expect(a).not.toHaveAttribute("target");
expect(a).toHaveAttribute("href", "/page");
});

it("lazy loads tweet embeds with a loading skeleton", async () => {
const content = "Check this [tweet](https://twitter.com/user/status/1234567890)";

render(
<DropPartMarkdown
mentionedUsers={[]}
referencedNfts={[]}
partContent={content}
onQuoteClick={jest.fn()}
/>
);

expect(screen.getByTestId("tweet-embed-loading")).toBeInTheDocument();

expect(dynamicSpy).toHaveBeenCalledTimes(1);
const [loader, options] = dynamicSpy.mock.calls[0];
expect(options?.ssr).toBe(false);

const TweetComponent = await loader();
const { getByText } = render(<TweetComponent id="abc123" />);
expect(getByText("tweet:abc123")).toBeInTheDocument();
});
});
27 changes: 22 additions & 5 deletions components/drops/view/part/DropPartMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,15 @@ import Markdown, { ExtraProps } from "react-markdown";
import rehypeExternalLinks from "rehype-external-links";
import rehypeSanitize from "rehype-sanitize";
import remarkGfm from "remark-gfm";
import dynamic from "next/dynamic";

import { getRandomObjectId } from "../../../../helpers/AllowlistToolHelpers";

// Component definitions moved outside for better performance
const BreakComponent = () => <br />;
import DropListItemContentPart, {
DropListItemContentPartProps,
} from "../item/content/DropListItemContentPart";
import { ApiDropMentionedUser } from "../../../../generated/models/ApiDropMentionedUser";
import { ApiDropReferencedNFT } from "../../../../generated/models/ApiDropReferencedNFT";
import { Tweet } from "react-tweet";
import type { TweetProps } from "react-tweet";

import DropPartMarkdownImage from "./DropPartMarkdownImage";
import WaveDropQuoteWithDropId from "../../../waves/drops/WaveDropQuoteWithDropId";
Expand All @@ -42,6 +40,25 @@ import WaveItemChat from "../../../waves/list/WaveItemChat";
import DropItemChat from "../../../waves/drops/DropItemChat";
import ChatItemHrefButtons from "../../../waves/ChatItemHrefButtons";

const BreakComponent = () => <br />;

const TweetEmbedSkeleton = () => (
<div
data-testid="tweet-embed-loading"
className="tw-flex tw-items-center tw-justify-center tw-w-full tw-min-h-[10rem] tw-rounded-lg tw-bg-iron-900/60 tw-animate-pulse tw-text-iron-300"
>
Loading tweet…
</div>
);

const TweetEmbed = dynamic<TweetProps>(
() => import("react-tweet").then((mod) => mod.Tweet),
{
ssr: false,
loading: TweetEmbedSkeleton,
}
);

export interface DropPartMarkdownProps {
readonly mentionedUsers: Array<ApiDropMentionedUser>;
readonly referencedNfts: Array<ApiDropReferencedNFT>;
Expand Down Expand Up @@ -322,7 +339,7 @@ function DropPartMarkdown({
const renderTweetEmbed = (result: { href: string; tweetId: string }) => (
<div className="tw-flex tw-items-stretch tw-w-full tw-gap-x-1">
<div className="tw-flex-1 tw-min-w-0" data-theme="dark">
<Tweet id={result.tweetId} />
<TweetEmbed id={result.tweetId} />
</div>
<ChatItemHrefButtons href={result.href} />
</div>
Expand Down