···8787pnpm codegen
8888```
89899090-## Build and Test
9090+## Build
91919292### Build the application
9393···104104```bash
105105pnpm typecheck
106106```
107107-108108-### Run tests
109109-110110-Execute unit tests across all workspaces:
111111-112112-```bash
113113-pnpm test
114114-```
115115-116116-Each workspace provides its own `vitest.config.ts`, so test behavior is scoped
117117-to that package. Vitest prints a summary for each workspace showing how many
118118-files and tests passed along with the execution time.
119107120108### Lint and Format Code
121109
-1
apps/api/README.md
···6677- `pnpm dev` – start the server in development mode.
88- `pnpm build` – compile all workspaces.
99-- `pnpm test` – run vitest tests.
···11-import { parseHTML } from "linkedom";
22-import { describe, expect, it } from "vitest";
33-import getDescription from "./getDescription";
44-55-describe("getDescription", () => {
66- it("should extract Open Graph description from a shared NFT collection", () => {
77- const html =
88- '<meta property="og:description" content="Check out this amazing Web3 art collection featuring 10,000 unique avatars living on the blockchain! 🎨✨">';
99- const { document } = parseHTML(html);
1010- const result = getDescription(document);
1111- expect(result).toBe(
1212- "Check out this amazing Web3 art collection featuring 10,000 unique avatars living on the blockchain! 🎨✨"
1313- );
1414- });
1515-1616- it("should extract Twitter description from a community announcement", () => {
1717- const html =
1818- '<meta name="twitter:description" content="🚀 Big news! Our decentralized social platform just launched Spaces - join live conversations with your favorite creators and frens!">';
1919- const { document } = parseHTML(html);
2020- const result = getDescription(document);
2121- expect(result).toBe(
2222- "🚀 Big news! Our decentralized social platform just launched Spaces - join live conversations with your favorite creators and frens!"
2323- );
2424- });
2525-2626- it("should prioritize Open Graph description over Twitter description for blog post", () => {
2727- const html = `
2828- <meta property="og:description" content="A comprehensive guide to understanding DeFi protocols, yield farming, and how to maximize your crypto earnings in 2024.">
2929- <meta name="twitter:description" content="Learn about DeFi and yield farming">
3030- `;
3131- const { document } = parseHTML(html);
3232- const result = getDescription(document);
3333- expect(result).toBe(
3434- "A comprehensive guide to understanding DeFi protocols, yield farming, and how to maximize your crypto earnings in 2024."
3535- );
3636- });
3737-3838- it("should return null when shared link has no social media descriptions", () => {
3939- const html = '<meta name="title" content="Random website">';
4040- const { document } = parseHTML(html);
4141- const result = getDescription(document);
4242- expect(result).toBeNull();
4343- });
4444-4545- it("should return null when shared post has empty descriptions", () => {
4646- const html = `
4747- <meta property="og:description" content="">
4848- <meta name="twitter:description" content="">
4949- `;
5050- const { document } = parseHTML(html);
5151- const result = getDescription(document);
5252- expect(result).toBeNull();
5353- });
5454-5555- it("should handle post descriptions with only whitespace", () => {
5656- const html = '<meta property="og:description" content=" ">';
5757- const { document } = parseHTML(html);
5858- const result = getDescription(document);
5959- expect(result).toBe(" ");
6060- });
6161-6262- it("should handle post descriptions with mentions and special characters", () => {
6363- const html =
6464- '<meta property="og:description" content="Just minted my first NFT! 🎉 Shoutout to @opensea & @ethereum for making this possible. "The future is here" 🚀">';
6565- const { document } = parseHTML(html);
6666- const result = getDescription(document);
6767- expect(result).toBe(
6868- 'Just minted my first NFT! 🎉 Shoutout to @opensea & @ethereum for making this possible. "The future is here" 🚀'
6969- );
7070- });
7171-7272- it("should handle very long thread descriptions", () => {
7373- const longDescription =
7474- "🧵 MEGA THREAD: Let me explain why decentralized social media is the future and how it's going to change everything we know about online communities. From data ownership to censorship resistance, from creator monetization to user empowerment, there are so many reasons why Web3 social platforms are revolutionary. In this thread, I'll break down the key differences between Web2 and Web3 social media, the main protocols you should know about, and why you should care about this shift. Whether you're a developer, content creator, or just someone who spends time online, this affects you. Let's dive deep into the world of decentralized social networking and explore what makes it so special. We'll cover everything from blockchain basics to advanced concepts like composability and interoperability. By the end of this thread, you'll understand why so many people are excited about the future of social media and how you can be part of this revolution.";
7575- const html = `<meta property="og:description" content="${longDescription}">`;
7676- const { document } = parseHTML(html);
7777- const result = getDescription(document);
7878- expect(result).toBe(longDescription);
7979- });
8080-8181- it("should handle multi-line post descriptions with formatting", () => {
8282- const html =
8383- '<meta property="og:description" content="GM builders! 🌅\n\nJust shipped a new feature for our social dApp.\n\nCan\'t wait to see what you all build with it! 🛠️">';
8484- const { document } = parseHTML(html);
8585- const result = getDescription(document);
8686- expect(result).toBe(
8787- "GM builders! 🌅\n\nJust shipped a new feature for our social dApp.\n\nCan't wait to see what you all build with it! 🛠️"
8888- );
8989- });
9090-9191- it("should handle international post descriptions with emojis", () => {
9292- const html =
9393- '<meta property="og:description" content="🌏 Building the future of social media together! 一起构建社交媒体的未来!Web3コミュニティへようこそ 🚀">';
9494- const { document } = parseHTML(html);
9595- const result = getDescription(document);
9696- expect(result).toBe(
9797- "🌏 Building the future of social media together! 一起构建社交媒体的未来!Web3コミュニティへようこそ 🚀"
9898- );
9999- });
100100-101101- it("should handle descriptions with HTML entities from user-generated content", () => {
102102- const html =
103103- '<meta property="og:description" content="Posted some code on my profile: <script> console.log("Hello Web3!") </script> Check it out!">';
104104- const { document } = parseHTML(html);
105105- const result = getDescription(document);
106106- expect(result).toBe(
107107- 'Posted some code on my profile: <script> console.log("Hello Web3!") </script> Check it out!'
108108- );
109109- });
110110-});
···11-import { parseHTML } from "linkedom";
22-import { describe, expect, it } from "vitest";
33-import getTitle from "./getTitle";
44-55-describe("getTitle", () => {
66- it("should extract Open Graph title from a viral meme post", () => {
77- const html =
88- '<meta property="og:title" content="When you finally understand Web3 social media 😂">';
99- const { document } = parseHTML(html);
1010- const result = getTitle(document);
1111- expect(result).toBe("When you finally understand Web3 social media 😂");
1212- });
1313-1414- it("should extract Twitter title from a trending hashtag post", () => {
1515- const html =
1616- '<meta name="twitter:title" content="Breaking: #LensSocial hits 1M users! 🚀">';
1717- const { document } = parseHTML(html);
1818- const result = getTitle(document);
1919- expect(result).toBe("Breaking: #LensSocial hits 1M users! 🚀");
2020- });
2121-2222- it("should prioritize Open Graph title over Twitter title for shared blog post", () => {
2323- const html = `
2424- <meta property="og:title" content="How I Built a Decentralized Social App in 2024">
2525- <meta name="twitter:title" content="My Journey Building on Lens Protocol">
2626- `;
2727- const { document } = parseHTML(html);
2828- const result = getTitle(document);
2929- expect(result).toBe("How I Built a Decentralized Social App in 2024");
3030- });
3131-3232- it("should return null when user shares a page without social media metadata", () => {
3333- const html = '<meta name="description" content="Random page content">';
3434- const { document } = parseHTML(html);
3535- const result = getTitle(document);
3636- expect(result).toBeNull();
3737- });
3838-3939- it("should return null when shared link has empty social media titles", () => {
4040- const html = `
4141- <meta property="og:title" content="">
4242- <meta name="twitter:title" content="">
4343- `;
4444- const { document } = parseHTML(html);
4545- const result = getTitle(document);
4646- expect(result).toBeNull();
4747- });
4848-4949- it("should handle post titles with only whitespace", () => {
5050- const html = '<meta property="og:title" content=" ">';
5151- const { document } = parseHTML(html);
5252- const result = getTitle(document);
5353- expect(result).toBe(" ");
5454- });
5555-5656- it("should handle post titles with special characters and mentions", () => {
5757- const html =
5858- '<meta property="og:title" content="Check out @vitalik.eth's latest post about "The Future of DeFi"">';
5959- const { document } = parseHTML(html);
6060- const result = getTitle(document);
6161- expect(result).toBe(
6262- 'Check out @vitalik.eth\'s latest post about "The Future of DeFi"'
6363- );
6464- });
6565-6666- it("should handle very long thread titles", () => {
6767- const longTitle =
6868- "🧵 THREAD: Everything you need to know about decentralized social media protocols and why they matter for the future of online communities and content creation. Let me break it down for you in simple terms that anyone can understand. This is going to be a long one so grab some coffee and let's dive deep into the world of Web3 social platforms and their revolutionary potential to reshape how we connect, share, and monetize our digital presence in the creator economy. From Lens Protocol to Farcaster, we'll explore the key players and what makes them special.";
6969- const html = `<meta property="og:title" content="${longTitle}">`;
7070- const { document } = parseHTML(html);
7171- const result = getTitle(document);
7272- expect(result).toBe(longTitle);
7373- });
7474-7575- it("should handle multi-line post titles with formatting", () => {
7676- const html =
7777- '<meta property="og:title" content="GM fam!\n\nJust dropped my latest NFT collection 🎨">';
7878- const { document } = parseHTML(html);
7979- const result = getTitle(document);
8080- expect(result).toBe(
8181- "GM fam!\n\nJust dropped my latest NFT collection 🎨"
8282- );
8383- });
8484-8585- it("should handle international post titles with emojis", () => {
8686- const html =
8787- '<meta property="og:title" content="🌍 Global Web3 Community Meetup - 我们一起建设未来 🚀">';
8888- const { document } = parseHTML(html);
8989- const result = getTitle(document);
9090- expect(result).toBe(
9191- "🌍 Global Web3 Community Meetup - 我们一起建设未来 🚀"
9292- );
9393- });
9494-});
-27
apps/api/src/routes/og/getAccount.test.ts
···11-import { Hono } from "hono";
22-import { beforeEach, describe, expect, it, vi } from "vitest";
33-44-const mockHtml = `<html><head><meta property="og:title" content="Account" /></head></html>`;
55-66-vi.mock("./ogUtils", () => ({
77- default: vi.fn(async ({ ctx }) => ctx.html(mockHtml, 200))
88-}));
99-1010-import getAccount from "./getAccount";
1111-1212-describe("getAccount", () => {
1313- let app: Hono;
1414-1515- beforeEach(() => {
1616- app = new Hono();
1717- app.get("/u/:username", getAccount);
1818- });
1919-2020- it("returns og html", async () => {
2121- const res = await app.request("/u/test");
2222- const html = await res.text();
2323-2424- expect(res.status).toBe(200);
2525- expect(html).toContain("og:title");
2626- });
2727-});
-27
apps/api/src/routes/og/getGroup.test.ts
···11-import { Hono } from "hono";
22-import { beforeEach, describe, expect, it, vi } from "vitest";
33-44-const mockHtml = `<html><head><meta property="og:title" content="Group" /></head></html>`;
55-66-vi.mock("./ogUtils", () => ({
77- default: vi.fn(async ({ ctx }) => ctx.html(mockHtml, 200))
88-}));
99-1010-import getGroup from "./getGroup";
1111-1212-describe("getGroup", () => {
1313- let app: Hono;
1414-1515- beforeEach(() => {
1616- app = new Hono();
1717- app.get("/g/:address", getGroup);
1818- });
1919-2020- it("returns og html", async () => {
2121- const res = await app.request("/g/0x1234");
2222- const html = await res.text();
2323-2424- expect(res.status).toBe(200);
2525- expect(html).toContain("og:title");
2626- });
2727-});
-27
apps/api/src/routes/og/getPost.test.ts
···11-import { Hono } from "hono";
22-import { beforeEach, describe, expect, it, vi } from "vitest";
33-44-const mockHtml = `<html><head><meta property="og:title" content="Post" /></head></html>`;
55-66-vi.mock("./ogUtils", () => ({
77- default: vi.fn(async ({ ctx }) => ctx.html(mockHtml, 200))
88-}));
99-1010-import getPost from "./getPost";
1111-1212-describe("getPost", () => {
1313- let app: Hono;
1414-1515- beforeEach(() => {
1616- app = new Hono();
1717- app.get("/posts/:slug", getPost);
1818- });
1919-2020- it("returns og html", async () => {
2121- const res = await app.request("/posts/example");
2222- const html = await res.text();
2323-2424- expect(res.status).toBe(200);
2525- expect(html).toContain("og:title");
2626- });
2727-});
-26
apps/api/src/utils/getDbPostId.test.ts
···11-import { describe, expect, it } from "vitest";
22-import getDbPostId from "./getDbPostId";
33-44-describe("getDbPostId", () => {
55- it("converts decimal string to hex with prefix", () => {
66- const result = getDbPostId("1234567890");
77- expect(result).toBe("\\x499602d2");
88- });
99-1010- it("returns empty string when input is empty", () => {
1111- expect(getDbPostId("")).toBe("");
1212- });
1313-1414- it("throws error for non digit strings", () => {
1515- expect(() => getDbPostId("abc123")).toThrow("Invalid decimal value");
1616- });
1717-1818- it("throws error for negative numbers", () => {
1919- expect(() => getDbPostId("-1")).toThrow("Invalid decimal value");
2020- });
2121-2222- it("handles large numbers", () => {
2323- const large = "18446744073709551615";
2424- expect(getDbPostId(large)).toBe("\\xffffffffffffffff");
2525- });
2626-});
-10
apps/api/src/utils/sha256.test.ts
···11-import { describe, expect, it } from "vitest";
22-import sha256 from "./sha256";
33-44-describe("sha256", () => {
55- it("hashes text correctly", () => {
66- expect(sha256("hello")).toBe(
77- "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
88- );
99- });
1010-});
···11-import {
22- type MetadataAttributeFragment,
33- MetadataAttributeType
44-} from "@hey/indexer";
55-import { describe, expect, it } from "vitest";
66-import getAccountAttribute from "./getAccountAttribute";
77-88-describe("getAccountAttribute", () => {
99- it("returns the attribute value when present", () => {
1010- const attributes: MetadataAttributeFragment[] = [
1111- { key: "location", type: MetadataAttributeType.String, value: "Berlin" },
1212- {
1313- key: "website",
1414- type: MetadataAttributeType.String,
1515- value: "https://example.com"
1616- }
1717- ];
1818- const result = getAccountAttribute("website", attributes);
1919- expect(result).toBe("https://example.com");
2020- });
2121-2222- it("returns an empty string when the attribute is missing", () => {
2323- const attributes: MetadataAttributeFragment[] = [
2424- { key: "location", type: MetadataAttributeType.String, value: "Berlin" }
2525- ];
2626- const result = getAccountAttribute("x", attributes);
2727- expect(result).toBe("");
2828- });
2929-3030- it("returns an empty string when attributes are undefined", () => {
3131- const result = getAccountAttribute("website", undefined);
3232- expect(result).toBe("");
3333- });
3434-3535- it("handles the 'x' attribute", () => {
3636- const attributes: MetadataAttributeFragment[] = [
3737- {
3838- key: "x",
3939- type: MetadataAttributeType.String,
4040- value: "https://x.com/hey"
4141- }
4242- ];
4343- const result = getAccountAttribute("x", attributes);
4444- expect(result).toBe("https://x.com/hey");
4545- });
4646-});
-53
apps/web/src/helpers/getAnyKeyValue.test.ts
···11-import { describe, expect, it } from "vitest";
22-import getAnyKeyValue from "./getAnyKeyValue";
33-44-const addressKeyValue = {
55- __typename: "AddressKeyValue",
66- address: "0x1234",
77- key: "owner"
88-} as any;
99-1010-const bigDecimalKeyValue = {
1111- __typename: "BigDecimalKeyValue",
1212- bigDecimal: "1000",
1313- key: "followers"
1414-} as any;
1515-1616-const stringKeyValue = {
1717- __typename: "StringKeyValue",
1818- key: "bio",
1919- string: "Hello there"
2020-} as any;
2121-2222-const unknownKeyValue = {
2323- __typename: "RandomKeyValue",
2424- key: "location",
2525- value: "Earth"
2626-} as any;
2727-2828-describe("getAnyKeyValue", () => {
2929- it("returns address value when key matches", () => {
3030- const result = getAnyKeyValue([addressKeyValue], "owner");
3131- expect(result).toEqual({ address: "0x1234" });
3232- });
3333-3434- it("returns bigDecimal value when key matches", () => {
3535- const result = getAnyKeyValue([bigDecimalKeyValue], "followers");
3636- expect(result).toEqual({ bigDecimal: "1000" });
3737- });
3838-3939- it("returns string value when key matches", () => {
4040- const result = getAnyKeyValue([stringKeyValue], "bio");
4141- expect(result).toEqual({ string: "Hello there" });
4242- });
4343-4444- it("returns null when key is missing", () => {
4545- const result = getAnyKeyValue([addressKeyValue], "missing");
4646- expect(result).toBeNull();
4747- });
4848-4949- it("returns null for unsupported key value type", () => {
5050- const result = getAnyKeyValue([unknownKeyValue], "location");
5151- expect(result).toBeNull();
5252- });
5353-});
-40
apps/web/src/helpers/getAssetLicense.test.ts
···11-import { MetadataLicenseType } from "@hey/indexer";
22-import { describe, expect, it } from "vitest";
33-import getAssetLicense from "./getAssetLicense";
44-55-describe("getAssetLicense", () => {
66- it("returns null when license id is missing", () => {
77- expect(getAssetLicense(undefined)).toBeNull();
88- });
99-1010- it("handles CC0 license", () => {
1111- const result = getAssetLicense(MetadataLicenseType.Cco);
1212- expect(result).toEqual({
1313- helper:
1414- "Anyone can use, modify and distribute the work without any restrictions or need for attribution. CC0",
1515- label: "CC0 - no restrictions"
1616- });
1717- });
1818-1919- it("handles commercial rights license", () => {
2020- const result = getAssetLicense(MetadataLicenseType.TbnlCdNplLegal);
2121- expect(result).toEqual({
2222- helper:
2323- "You allow the collector to use the content for any purpose, except creating or sharing any derivative works, such as remixes.",
2424- label: "Commercial rights for the collector"
2525- });
2626- });
2727-2828- it("handles personal rights license", () => {
2929- const result = getAssetLicense(MetadataLicenseType.TbnlNcDNplLegal);
3030- expect(result).toEqual({
3131- helper:
3232- "You allow the collector to use the content for any personal, non-commercial purpose, except creating or sharing any derivative works, such as remixes.",
3333- label: "Personal rights for the collector"
3434- });
3535- });
3636-3737- it("returns null for unsupported license", () => {
3838- expect(getAssetLicense(MetadataLicenseType.CcBy)).toBeNull();
3939- });
4040-});
-48
apps/web/src/helpers/getBlockedMessage.test.ts
···11-import { LENS_NAMESPACE } from "@hey/data/constants";
22-import type { AccountFragment } from "@hey/indexer";
33-import { describe, expect, it } from "vitest";
44-import {
55- getBlockedByMeMessage,
66- getBlockedMeMessage
77-} from "./getBlockedMessage";
88-99-describe("getBlockedMessage", () => {
1010- it("returns message when current user blocked another account", () => {
1111- const account = {
1212- address: "0x1234567890abcdef1234567890abcdef12345678",
1313- owner: "0x01",
1414- username: {
1515- localName: "janedoe",
1616- value: `${LENS_NAMESPACE}janedoe`
1717- }
1818- } as unknown as AccountFragment;
1919-2020- const result = getBlockedByMeMessage(account);
2121- expect(result).toBe("You have blocked @janedoe");
2222- });
2323-2424- it("returns message when another account blocked the user", () => {
2525- const account = {
2626- address: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
2727- owner: "0x01",
2828- username: {
2929- localName: "alice",
3030- value: `${LENS_NAMESPACE}alice`
3131- }
3232- } as unknown as AccountFragment;
3333-3434- const result = getBlockedMeMessage(account);
3535- expect(result).toBe("@alice has blocked you");
3636- });
3737-3838- it("uses address when username is missing", () => {
3939- const address = "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
4040- const account = {
4141- address,
4242- owner: "0x01"
4343- } as unknown as AccountFragment;
4444-4545- const result = getBlockedMeMessage(account);
4646- expect(result).toBe("#0xab…abcd has blocked you");
4747- });
4848-});
···11-import { describe, expect, it } from "vitest";
22-import humanize from "./humanize";
33-44-describe("humanize", () => {
55- it("formats large follower counts with commas", () => {
66- expect(humanize(1234567)).toBe("1,234,567");
77- expect(humanize(987654321)).toBe("987,654,321");
88- });
99-1010- it("handles zero followers and negative reputation scores", () => {
1111- expect(humanize(0)).toBe("0");
1212- expect(humanize(-9876)).toBe("-9,876");
1313- });
1414-1515- it("returns empty string for invalid social media metrics", () => {
1616- expect(humanize(Number.NaN)).toBe("");
1717- expect(humanize(Number.POSITIVE_INFINITY)).toBe("");
1818- });
1919-});
-35
apps/web/src/helpers/injectReferrerToUrl.test.ts
···11-import { HEY_TREASURY } from "@hey/data/constants";
22-import { describe, expect, it } from "vitest";
33-import injectReferrerToUrl from "./injectReferrerToUrl";
44-55-describe("injectReferrerToUrl", () => {
66- it("returns the input when url is invalid", () => {
77- const input = "not a url";
88- expect(injectReferrerToUrl(input)).toBe(input);
99- });
1010-1111- it("adds referrer param for highlight links", () => {
1212- const url = injectReferrerToUrl("https://highlight.xyz/mint/1");
1313- const parsed = new URL(url);
1414- expect(parsed.hostname).toBe("highlight.xyz");
1515- expect(parsed.searchParams.get("referrer")).toBe(HEY_TREASURY);
1616- });
1717-1818- it("supports subdomains", () => {
1919- const url = injectReferrerToUrl("https://app.highlight.xyz/auction");
2020- const parsed = new URL(url);
2121- expect(parsed.searchParams.get("referrer")).toBe(HEY_TREASURY);
2222- });
2323-2424- it("appends the param when query exists", () => {
2525- const url = injectReferrerToUrl("https://zora.co/collect?foo=bar");
2626- const parsed = new URL(url);
2727- expect(parsed.searchParams.get("foo")).toBe("bar");
2828- expect(parsed.searchParams.get("referrer")).toBe(HEY_TREASURY);
2929- });
3030-3131- it("returns the same url for other domains", () => {
3232- const original = "https://example.com/post?id=1";
3333- expect(injectReferrerToUrl(original)).toBe(original);
3434- });
3535-});
-23
apps/web/src/helpers/nFormatter.test.ts
···11-import { describe, expect, it } from "vitest";
22-import nFormatter from "./nFormatter";
33-44-// Tests for nFormatter used in social media contexts
55-66-describe("nFormatter", () => {
77- it("formats small follower counts", () => {
88- expect(nFormatter(847)).toBe("847");
99- });
1010-1111- it("abbreviates thousands with a k", () => {
1212- expect(nFormatter(1520)).toBe("1.5k");
1313- });
1414-1515- it("abbreviates millions with an M", () => {
1616- expect(nFormatter(2300000)).toBe("2.3M");
1717- });
1818-1919- it("returns empty string for invalid counts", () => {
2020- expect(nFormatter(Number.POSITIVE_INFINITY)).toBe("");
2121- expect(nFormatter(Number.NaN)).toBe("");
2222- });
2323-});
-15
apps/web/src/helpers/prosekit/extension.test.ts
···11-import { createEditor } from "prosekit/core";
22-import { describe, expect, it } from "vitest";
33-import { defineEditorExtension } from "./extension";
44-55-describe("defineEditorExtension", () => {
66- it("registers mention node and link mark", () => {
77- const extension = defineEditorExtension();
88- const mount = document.createElement("div");
99- const editor = createEditor({ extension });
1010- editor.mount(mount);
1111- const { schema } = editor.view.state;
1212- expect(schema.nodes.mention).toBeDefined();
1313- expect(schema.marks.link).toBeDefined();
1414- });
1515-});
-22
apps/web/src/helpers/prosekit/markdown.test.ts
···11-import { describe, expect, it } from "vitest";
22-import { htmlFromMarkdown, markdownFromHTML } from "./markdown";
33-44-const joinHtml = "<p>Hello</p><p>World</p>";
55-66-describe("markdown and html conversion", () => {
77- it("converts html to markdown without escaping underscores", () => {
88- const result = markdownFromHTML("<p>hey_world</p>");
99- expect(result).toBe("hey_world\n");
1010- });
1111-1212- it("joins consecutive paragraphs when converting to markdown", () => {
1313- const result = markdownFromHTML(joinHtml);
1414- expect(result).toBe("Hello\nWorld\n");
1515- });
1616-1717- it("round trips markdown to html", () => {
1818- const markdown = "Hello\nWorld";
1919- const html = htmlFromMarkdown(markdown);
2020- expect(html).toBe("<p>Hello\nWorld</p>\n");
2121- });
2222-});
-54
apps/web/src/helpers/rules.test.ts
···11-import type { AccountFollowRules, GroupRules } from "@hey/indexer";
22-import { describe, expect, it } from "vitest";
33-import { getSimplePaymentDetails } from "./rules";
44-55-const paymentConfig = () => [
66- { __typename: "BigDecimalKeyValue", bigDecimal: "2", key: "amount" } as any,
77- {
88- __typename: "AddressKeyValue",
99- address: "0xToken",
1010- key: "assetContract"
1111- } as any,
1212- { __typename: "StringKeyValue", key: "assetSymbol", string: "SOC" } as any
1313-];
1414-1515-describe("getSimplePaymentDetails", () => {
1616- it("extracts membership fee from group rules", () => {
1717- const rules = {
1818- anyOf: [],
1919- required: [{ config: paymentConfig(), type: "SIMPLE_PAYMENT" }]
2020- } as unknown as GroupRules;
2121-2222- const result = getSimplePaymentDetails(rules);
2323- expect(result).toEqual({
2424- amount: 2,
2525- assetAddress: "0xToken",
2626- assetSymbol: "SOC"
2727- });
2828- });
2929-3030- it("reads follow fee from optional rules when required missing", () => {
3131- const rules = {
3232- anyOf: [{ config: paymentConfig(), type: "SIMPLE_PAYMENT" }],
3333- required: [{ config: [], type: "TOKEN_GATED" }]
3434- } as unknown as AccountFollowRules;
3535-3636- const result = getSimplePaymentDetails(rules);
3737- expect(result).toEqual({
3838- amount: null,
3939- assetAddress: null,
4040- assetSymbol: null
4141- });
4242- });
4343-4444- it("returns null values when simple payment is absent", () => {
4545- const rules = { anyOf: [], required: [] } as unknown as GroupRules;
4646-4747- const result = getSimplePaymentDetails(rules);
4848- expect(result).toEqual({
4949- amount: null,
5050- assetAddress: null,
5151- assetSymbol: null
5252- });
5353- });
5454-});
-8
apps/web/src/helpers/splitNumber.test.ts
···11-import { describe, expect, it } from "vitest";
22-import splitNumber from "./splitNumber";
33-44-describe("splitNumber", () => {
55- it("evenly distributes engagement metrics across multiple feeds", () => {
66- expect(splitNumber(5, 2)).toEqual([3, 2]);
77- });
88-});
-24
apps/web/src/helpers/trimify.test.ts
···11-import { describe, expect, it } from "vitest";
22-import trimify from "./trimify";
33-44-describe("trimify", () => {
55- it("trims spaces around a post", () => {
66- const post = " Hello world ";
77- expect(trimify(post)).toBe("Hello world");
88- });
99-1010- it("collapses multiple blank lines within a post", () => {
1111- const post = "First line\n\n \nSecond line";
1212- expect(trimify(post)).toBe("First line\n\nSecond line");
1313- });
1414-1515- it("leaves already clean text unchanged", () => {
1616- const post = "Just a single line";
1717- expect(trimify(post)).toBe("Just a single line");
1818- });
1919-2020- it("handles trailing newlines from comments", () => {
2121- const comment = "Nice post!\n\n";
2222- expect(trimify(comment)).toBe("Nice post!");
2323- });
2424-});
-19
apps/web/src/helpers/truncateByWords.test.ts
···11-import { describe, expect, it } from "vitest";
22-import truncateByWords from "./truncateByWords";
33-44-describe("truncateByWords", () => {
55- it("truncates lengthy captions to the specified number of words", () => {
66- const caption = "This is a sample caption for social media post";
77- expect(truncateByWords(caption, 5)).toBe("This is a sample caption…");
88- });
99-1010- it("returns the full comment when it is within the limit", () => {
1111- const comment = "Love this app";
1212- expect(truncateByWords(comment, 5)).toBe(comment);
1313- });
1414-1515- it("handles extra whitespace gracefully", () => {
1616- const bio = " Welcome to the new social network ";
1717- expect(truncateByWords(bio, 4)).toBe("Welcome to the new…");
1818- });
1919-});
···6677- `pnpm dev` – start development mode across workspaces.
88- `pnpm build` – compile the monorepo packages.
99-- `pnpm test` – run vitest tests.
-19
packages/helpers/escapeHtml.test.ts
···11-import { describe, expect, it } from "vitest";
22-import escapeHtml from "./escapeHtml";
33-44-describe("escapeHtml", () => {
55- it("escapes HTML characters in user-generated social media content", () => {
66- const result = escapeHtml(`<div>&'"</div>`);
77- expect(result).toBe("<div>&'"</div>");
88- });
99-1010- it("returns empty string when post content is missing", () => {
1111- expect(escapeHtml()).toBe("");
1212- expect(escapeHtml(null)).toBe("");
1313- });
1414-1515- it("leaves normal social media posts unchanged", () => {
1616- const text = "GM frens! Hope everyone is having a great day in Web3 🚀";
1717- expect(escapeHtml(text)).toBe(text);
1818- });
1919-});
-26
packages/helpers/formatAddress.test.ts
···11-import { describe, expect, it } from "vitest";
22-import formatAddress from "./formatAddress";
33-44-const sampleAddress = "0x1234567890ABCDEF1234567890abcdef12345678";
55-66-describe("formatAddress", () => {
77- it("formats wallet address for user profile display", () => {
88- const result = formatAddress(sampleAddress);
99- expect(result).toBe("0x12…5678");
1010- });
1111-1212- it("formats ENS address with custom length for NFT creator attribution", () => {
1313- const result = formatAddress(sampleAddress, 6);
1414- expect(result).toBe("0x1234…345678");
1515- });
1616-1717- it("returns empty string when user has no connected wallet", () => {
1818- const result = formatAddress(null);
1919- expect(result).toBe("");
2020- });
2121-2222- it("returns lowercase string for invalid wallet addresses in posts", () => {
2323- const result = formatAddress("NotAnAddress");
2424- expect(result).toBe("notanaddress");
2525- });
2626-});
···11-import { LENS_MEDIA_SNAPSHOT_URL } from "@hey/data/constants";
22-import { describe, expect, it } from "vitest";
33-import imageKit from "./imageKit";
44-55-describe("imageKit", () => {
66- it("returns an empty string for empty url", () => {
77- const result = imageKit("");
88- expect(result).toBe("");
99- });
1010-1111- it("returns the original url when not a lens snapshot", () => {
1212- const url = "https://example.com/photo.jpg";
1313- const result = imageKit(url, "tr:w-100");
1414- expect(result).toBe(url);
1515- });
1616-1717- it("applies transform when url is a lens snapshot", () => {
1818- const original = `${LENS_MEDIA_SNAPSHOT_URL}/photo.jpg`;
1919- const result = imageKit(original, "tr:w-200");
2020- expect(result).toBe(`${LENS_MEDIA_SNAPSHOT_URL}/tr:w-200/photo.jpg`);
2121- });
2222-});
-16
packages/helpers/isAccountDeleted.test.ts
···11-import { NULL_ADDRESS } from "@hey/data/constants";
22-import type { AccountFragment } from "@hey/indexer";
33-import { describe, expect, it } from "vitest";
44-import isAccountDeleted from "./isAccountDeleted";
55-66-describe("isAccountDeleted", () => {
77- it("returns true when the account owner is the null address", () => {
88- const account = { owner: NULL_ADDRESS } as unknown as AccountFragment;
99- expect(isAccountDeleted(account)).toBe(true);
1010- });
1111-1212- it("returns false when the account owner is not the null address", () => {
1313- const account = { owner: "0x123" } as unknown as AccountFragment;
1414- expect(isAccountDeleted(account)).toBe(false);
1515- });
1616-});
-27
packages/helpers/normalizeDescription.test.ts
···11-import { describe, expect, it } from "vitest";
22-import normalizeDescription from "./normalizeDescription";
33-44-describe("normalizeDescription", () => {
55- it("uses fallback when text is too short", () => {
66- const result = normalizeDescription(
77- "short",
88- "This fallback description is definitely longer than twenty five characters."
99- );
1010- expect(result).toBe(
1111- "This fallback description is definitely longer than twenty five characters.".slice(
1212- 0,
1313- 160
1414- )
1515- );
1616- });
1717-1818- it("truncates long text", () => {
1919- const longText = "a".repeat(200);
2020- expect(normalizeDescription(longText, "fallback").length).toBe(160);
2121- });
2222-2323- it("returns trimmed text when within range", () => {
2424- const text = "This is a valid description for OG meta tags.";
2525- expect(normalizeDescription(text, "fallback")).toBe(text);
2626- });
2727-});
···11-import type { AnyPostFragment } from "@hey/indexer";
22-import { describe, expect, it } from "vitest";
33-import { isRepost } from "./postHelpers";
44-55-describe("isRepost", () => {
66- it("returns true when user shares a viral Web3 meme", () => {
77- const post = { __typename: "Repost" } as unknown as AnyPostFragment;
88- expect(isRepost(post)).toBe(true);
99- });
1010-1111- it("returns false when user creates original crypto content", () => {
1212- const post = { __typename: "Post" } as unknown as AnyPostFragment;
1313- expect(isRepost(post)).toBe(false);
1414- });
1515-1616- it("returns false when post data is unavailable", () => {
1717- expect(isRepost(null)).toBe(false);
1818- });
1919-});
···6677- `pnpm dev` – start development mode across workspaces.
88- `pnpm build` – compile the monorepo packages.
99-- `pnpm test` – run the repository test suites.