Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿

refactor: remove test cases

yoginth.com f0bf38d7 fd46b657

verified
+7 -3524
-1
AGENTS.md
··· 48 48 - `pnpm biome:check` 49 49 - `pnpm typecheck` 50 50 - `pnpm build` 51 - - `pnpm test` 52 51 53 52 ## PR Instructions 54 53
+1 -13
README.md
··· 87 87 pnpm codegen 88 88 ``` 89 89 90 - ## Build and Test 90 + ## Build 91 91 92 92 ### Build the application 93 93 ··· 104 104 ```bash 105 105 pnpm typecheck 106 106 ``` 107 - 108 - ### Run tests 109 - 110 - Execute unit tests across all workspaces: 111 - 112 - ```bash 113 - pnpm test 114 - ``` 115 - 116 - Each workspace provides its own `vitest.config.ts`, so test behavior is scoped 117 - to that package. Vitest prints a summary for each workspace showing how many 118 - files and tests passed along with the execution time. 119 107 120 108 ### Lint and Format Code 121 109
-1
apps/api/README.md
··· 6 6 7 7 - `pnpm dev` – start the server in development mode. 8 8 - `pnpm build` – compile all workspaces. 9 - - `pnpm test` – run vitest tests.
+1 -3
apps/api/package.json
··· 12 12 "prisma:format": "prisma format --schema ./src/prisma/schema.prisma", 13 13 "prisma:migrate": "prisma migrate dev --schema ./src/prisma/schema.prisma", 14 14 "start": "tsx watch src/index.ts", 15 - "test": "vitest run", 16 15 "typecheck": "tsc --pretty" 17 16 }, 18 17 "dependencies": { ··· 42 41 "@hey/types": "workspace:*", 43 42 "@types/node": "^24.2.1", 44 43 "prisma": "^6.14.0", 45 - "typescript": "^5.9.2", 46 - "vitest": "^3.2.4" 44 + "typescript": "^5.9.2" 47 45 } 48 46 }
-186
apps/api/src/routes/oembed/getOembed.test.ts
··· 1 - import { Hono } from "hono"; 2 - import { beforeEach, describe, expect, it, vi } from "vitest"; 3 - import getOembed from "./getOembed"; 4 - 5 - // Mock Redis utilities 6 - vi.mock("@/utils/redis", () => ({ 7 - generateExtraLongExpiry: vi.fn(() => 86400), 8 - getRedis: vi.fn(), 9 - setRedis: vi.fn() 10 - })); 11 - 12 - // Mock getMetadata 13 - vi.mock("./helpers/getMetadata", () => ({ 14 - default: vi.fn() 15 - })); 16 - 17 - // Mock SHA256 utility 18 - vi.mock("../../utils/sha256", () => ({ 19 - default: vi.fn(() => "mocked-hash") 20 - })); 21 - 22 - // Mock handleApiError 23 - vi.mock("../../utils/handleApiError", () => ({ 24 - default: vi.fn((ctx, error) => ctx.json({ error: error.message }, 500)) 25 - })); 26 - 27 - import { getRedis, setRedis } from "../../utils/redis"; 28 - import getMetadata from "./helpers/getMetadata"; 29 - 30 - describe("getOembed", () => { 31 - let app: Hono; 32 - 33 - beforeEach(() => { 34 - vi.clearAllMocks(); 35 - app = new Hono(); 36 - app.get("/", getOembed); 37 - }); 38 - 39 - it("should return cached result when available", async () => { 40 - const cachedData = { 41 - description: "Cached Description", 42 - title: "Cached Title", 43 - url: "https://example.com" 44 - }; 45 - 46 - (getRedis as any).mockResolvedValue(JSON.stringify(cachedData)); 47 - 48 - const response = await app.request("/?url=https://example.com"); 49 - const result = await response.json(); 50 - 51 - expect(response.status).toBe(200); 52 - expect(result).toEqual({ 53 - cached: true, 54 - data: cachedData, 55 - status: "success" 56 - }); 57 - expect(getRedis).toHaveBeenCalledWith("oembed:mocked-hash"); 58 - expect(getMetadata).not.toHaveBeenCalled(); 59 - }); 60 - 61 - it("should fetch and cache new metadata when not cached", async () => { 62 - const metadata = { 63 - description: "Fresh Description", 64 - title: "Fresh Title", 65 - url: "https://example.com" 66 - }; 67 - 68 - (getRedis as any).mockResolvedValue(null); 69 - (getMetadata as any).mockResolvedValue(metadata); 70 - 71 - const response = await app.request("/?url=https://example.com"); 72 - const result = await response.json(); 73 - 74 - expect(response.status).toBe(200); 75 - expect(result).toEqual({ 76 - data: metadata, 77 - status: "success" 78 - }); 79 - 80 - expect(getRedis).toHaveBeenCalledWith("oembed:mocked-hash"); 81 - expect(getMetadata).toHaveBeenCalledWith("https://example.com"); 82 - expect(setRedis).toHaveBeenCalledWith( 83 - "oembed:mocked-hash", 84 - metadata, 85 - 86400 86 - ); 87 - }); 88 - 89 - it("should handle Redis errors gracefully", async () => { 90 - (getRedis as any).mockRejectedValue(new Error("Redis error")); 91 - 92 - const response = await app.request("/?url=https://example.com"); 93 - 94 - expect(response.status).toBe(500); 95 - const result = await response.json(); 96 - expect(result).toHaveProperty("error"); 97 - }); 98 - 99 - it("should handle metadata fetch errors", async () => { 100 - (getRedis as any).mockResolvedValue(null); 101 - (getMetadata as any).mockRejectedValue(new Error("Fetch error")); 102 - 103 - const response = await app.request("/?url=https://example.com"); 104 - 105 - expect(response.status).toBe(500); 106 - const result = await response.json(); 107 - expect(result).toHaveProperty("error"); 108 - }); 109 - 110 - it("should handle null metadata response", async () => { 111 - const metadata = { 112 - description: null, 113 - title: null, 114 - url: "https://example.com" 115 - }; 116 - 117 - (getRedis as any).mockResolvedValue(null); 118 - (getMetadata as any).mockResolvedValue(metadata); 119 - 120 - const response = await app.request("/?url=https://example.com"); 121 - const result = await response.json(); 122 - 123 - expect(response.status).toBe(200); 124 - expect(result).toEqual({ 125 - data: metadata, 126 - status: "success" 127 - }); 128 - }); 129 - 130 - it("should handle malformed cached data", async () => { 131 - const metadata = { 132 - description: "Fresh Description", 133 - title: "Fresh Title", 134 - url: "https://example.com" 135 - }; 136 - 137 - (getRedis as any).mockResolvedValue("invalid json"); 138 - (getMetadata as any).mockResolvedValue(metadata); 139 - 140 - const response = await app.request("/?url=https://example.com"); 141 - 142 - expect(response.status).toBe(500); 143 - const result = await response.json(); 144 - expect(result).toHaveProperty("error"); 145 - }); 146 - 147 - it("should handle URLs with special characters", async () => { 148 - const specialUrl = "https://example.com/page?q=test&foo=bar#section"; 149 - const metadata = { 150 - description: "Special URL Description", 151 - title: "Special URL Title", 152 - url: specialUrl 153 - }; 154 - 155 - (getRedis as any).mockResolvedValue(null); 156 - (getMetadata as any).mockResolvedValue(metadata); 157 - 158 - const response = await app.request( 159 - `/?url=${encodeURIComponent(specialUrl)}` 160 - ); 161 - const result = await response.json(); 162 - 163 - expect(response.status).toBe(200); 164 - expect(result).toEqual({ 165 - data: metadata, 166 - status: "success" 167 - }); 168 - expect(getMetadata).toHaveBeenCalledWith(specialUrl); 169 - }); 170 - 171 - it("should handle empty metadata gracefully", async () => { 172 - const metadata = {}; 173 - 174 - (getRedis as any).mockResolvedValue(null); 175 - (getMetadata as any).mockResolvedValue(metadata); 176 - 177 - const response = await app.request("/?url=https://example.com"); 178 - const result = await response.json(); 179 - 180 - expect(response.status).toBe(200); 181 - expect(result).toEqual({ 182 - data: metadata, 183 - status: "success" 184 - }); 185 - }); 186 - });
-258
apps/api/src/routes/oembed/helpers/getMetadata.test.ts
··· 1 - import { beforeEach, describe, expect, it, vi } from "vitest"; 2 - import getMetadata from "./getMetadata"; 3 - 4 - // Mock fetch 5 - global.fetch = vi.fn(); 6 - 7 - describe("getMetadata", () => { 8 - beforeEach(() => { 9 - vi.clearAllMocks(); 10 - }); 11 - 12 - it("should extract title and description from shared Web3 blog post", async () => { 13 - const mockHtml = ` 14 - <html> 15 - <head> 16 - <meta property="og:title" content="How to Build Your First NFT Marketplace on Ethereum"> 17 - <meta property="og:description" content="A complete guide to creating decentralized marketplaces with React, Solidity, and IPFS. Learn about smart contracts, Web3 integration, and user experience design."> 18 - </head> 19 - </html> 20 - `; 21 - 22 - (global.fetch as any).mockResolvedValue({ 23 - ok: true, 24 - text: () => Promise.resolve(mockHtml) 25 - }); 26 - 27 - const result = await getMetadata( 28 - "https://web3builder.dev/nft-marketplace-guide" 29 - ); 30 - 31 - expect(result).toEqual({ 32 - description: 33 - "A complete guide to creating decentralized marketplaces with React, Solidity, and IPFS. Learn about smart contracts, Web3 integration, and user experience design.", 34 - title: "How to Build Your First NFT Marketplace on Ethereum", 35 - url: "https://web3builder.dev/nft-marketplace-guide" 36 - }); 37 - }); 38 - 39 - it("should handle missing title and description from basic user profile", async () => { 40 - const mockHtml = ` 41 - <html> 42 - <head> 43 - <meta name="keywords" content="blockchain,crypto,nft"> 44 - </head> 45 - </html> 46 - `; 47 - 48 - (global.fetch as any).mockResolvedValue({ 49 - ok: true, 50 - text: () => Promise.resolve(mockHtml) 51 - }); 52 - 53 - const result = await getMetadata("https://lens.xyz/u/vitalik"); 54 - 55 - expect(result).toEqual({ 56 - description: null, 57 - title: null, 58 - url: "https://lens.xyz/u/vitalik" 59 - }); 60 - }); 61 - 62 - it("should handle network errors when fetching shared social media links", async () => { 63 - (global.fetch as any).mockRejectedValue(new Error("Network error")); 64 - 65 - const result = await getMetadata( 66 - "https://opensea.io/collection/cryptopunks" 67 - ); 68 - 69 - expect(result).toEqual({ 70 - description: null, 71 - title: null, 72 - url: "https://opensea.io/collection/cryptopunks" 73 - }); 74 - }); 75 - 76 - it("should handle HTTP errors when user shares dead links", async () => { 77 - (global.fetch as any).mockResolvedValue({ 78 - ok: false, 79 - status: 404 80 - }); 81 - 82 - const result = await getMetadata("https://deadnftproject.com/mint"); 83 - 84 - expect(result).toEqual({ 85 - description: null, 86 - title: null, 87 - url: "https://deadnftproject.com/mint" 88 - }); 89 - }); 90 - 91 - it("should handle malformed HTML from poorly built DeFi sites", async () => { 92 - const mockHtml = ` 93 - <html> 94 - <head> 95 - <meta property="og:title" content="🚀 New DeFi Protocol - Earn 1000% APY!" 96 - <meta property="og:description" 97 - </head> 98 - </html> 99 - `; 100 - 101 - (global.fetch as any).mockResolvedValue({ 102 - ok: true, 103 - text: () => Promise.resolve(mockHtml) 104 - }); 105 - 106 - const result = await getMetadata("https://definitely-not-a-scam-defi.com"); 107 - 108 - expect(result).toEqual({ 109 - description: null, 110 - title: "🚀 New DeFi Protocol - Earn 1000% APY!", 111 - url: "https://definitely-not-a-scam-defi.com" 112 - }); 113 - }); 114 - 115 - it("should handle empty HTML from minimal dApp landing pages", async () => { 116 - (global.fetch as any).mockResolvedValue({ 117 - ok: true, 118 - text: () => Promise.resolve("") 119 - }); 120 - 121 - const result = await getMetadata("https://minimal-dapp.vercel.app"); 122 - 123 - expect(result).toEqual({ 124 - description: null, 125 - title: null, 126 - url: "https://minimal-dapp.vercel.app" 127 - }); 128 - }); 129 - 130 - it("should use correct User-Agent header when fetching NFT marketplace links", async () => { 131 - const mockHtml = ` 132 - <html> 133 - <head> 134 - <meta property="og:title" content="Rare Pepe NFT Collection #1337"> 135 - </head> 136 - </html> 137 - `; 138 - 139 - (global.fetch as any).mockResolvedValue({ 140 - ok: true, 141 - text: () => Promise.resolve(mockHtml) 142 - }); 143 - 144 - await getMetadata("https://rarible.com/token/0x123...456"); 145 - 146 - expect(global.fetch).toHaveBeenCalledWith( 147 - "https://rarible.com/token/0x123...456", 148 - { 149 - headers: { 150 - "User-Agent": "HeyBot (like TwitterBot)" 151 - } 152 - } 153 - ); 154 - }); 155 - 156 - it("should handle Twitter meta tags when Open Graph is not available from crypto news sites", async () => { 157 - const mockHtml = ` 158 - <html> 159 - <head> 160 - <meta name="twitter:title" content="Bitcoin Hits All-Time High as Institutions Adopt Crypto"> 161 - <meta name="twitter:description" content="Major corporations and financial institutions are embracing cryptocurrency as Bitcoin reaches new record prices. Here's what this means for the future of finance."> 162 - </head> 163 - </html> 164 - `; 165 - 166 - (global.fetch as any).mockResolvedValue({ 167 - ok: true, 168 - text: () => Promise.resolve(mockHtml) 169 - }); 170 - 171 - const result = await getMetadata("https://cryptonews.com/bitcoin-ath-2024"); 172 - 173 - expect(result).toEqual({ 174 - description: 175 - "Major corporations and financial institutions are embracing cryptocurrency as Bitcoin reaches new record prices. Here's what this means for the future of finance.", 176 - title: "Bitcoin Hits All-Time High as Institutions Adopt Crypto", 177 - url: "https://cryptonews.com/bitcoin-ath-2024" 178 - }); 179 - }); 180 - 181 - it("should handle mixed meta tags from DAO governance proposals", async () => { 182 - const mockHtml = ` 183 - <html> 184 - <head> 185 - <meta property="og:title" content="Proposal #42: Upgrade Smart Contract to v2.0"> 186 - <meta name="twitter:description" content="Vote on upgrading our core smart contract to add new features and improve gas efficiency. Voting ends in 72 hours."> 187 - </head> 188 - </html> 189 - `; 190 - 191 - (global.fetch as any).mockResolvedValue({ 192 - ok: true, 193 - text: () => Promise.resolve(mockHtml) 194 - }); 195 - 196 - const result = await getMetadata( 197 - "https://snapshot.org/#/awesome-dao.eth/proposal/0x123" 198 - ); 199 - 200 - expect(result).toEqual({ 201 - description: 202 - "Vote on upgrading our core smart contract to add new features and improve gas efficiency. Voting ends in 72 hours.", 203 - title: "Proposal #42: Upgrade Smart Contract to v2.0", 204 - url: "https://snapshot.org/#/awesome-dao.eth/proposal/0x123" 205 - }); 206 - }); 207 - 208 - it("should handle fetch timeout from slow IPFS gateways", async () => { 209 - (global.fetch as any).mockImplementation( 210 - () => 211 - new Promise((_, reject) => 212 - setTimeout(() => reject(new Error("Timeout")), 100) 213 - ) 214 - ); 215 - 216 - const result = await getMetadata("https://ipfs.io/ipfs/QmSlowHash123"); 217 - 218 - expect(result).toEqual({ 219 - description: null, 220 - title: null, 221 - url: "https://ipfs.io/ipfs/QmSlowHash123" 222 - }); 223 - }); 224 - 225 - it("should handle special characters in URL from DeFi trading links", async () => { 226 - const mockHtml = ` 227 - <html> 228 - <head> 229 - <meta property="og:title" content="Swap ETH to USDC - Best Rates on DeFi"> 230 - </head> 231 - </html> 232 - `; 233 - 234 - (global.fetch as any).mockResolvedValue({ 235 - ok: true, 236 - text: () => Promise.resolve(mockHtml) 237 - }); 238 - 239 - const result = await getMetadata( 240 - "https://uniswap.org/swap?inputCurrency=ETH&outputCurrency=0xA0b86a33E6414506fb26f3e9aE8c6B8F6B3A8E7" 241 - ); 242 - 243 - expect(result).toEqual({ 244 - description: null, 245 - title: "Swap ETH to USDC - Best Rates on DeFi", 246 - url: "https://uniswap.org/swap?inputCurrency=ETH&outputCurrency=0xA0b86a33E6414506fb26f3e9aE8c6B8F6B3A8E7" 247 - }); 248 - 249 - expect(global.fetch).toHaveBeenCalledWith( 250 - "https://uniswap.org/swap?inputCurrency=ETH&outputCurrency=0xA0b86a33E6414506fb26f3e9aE8c6B8F6B3A8E7", 251 - { 252 - headers: { 253 - "User-Agent": "HeyBot (like TwitterBot)" 254 - } 255 - } 256 - ); 257 - }); 258 - });
-110
apps/api/src/routes/oembed/helpers/meta/getDescription.test.ts
··· 1 - import { parseHTML } from "linkedom"; 2 - import { describe, expect, it } from "vitest"; 3 - import getDescription from "./getDescription"; 4 - 5 - describe("getDescription", () => { 6 - it("should extract Open Graph description from a shared NFT collection", () => { 7 - const html = 8 - '<meta property="og:description" content="Check out this amazing Web3 art collection featuring 10,000 unique avatars living on the blockchain! 🎨✨">'; 9 - const { document } = parseHTML(html); 10 - const result = getDescription(document); 11 - expect(result).toBe( 12 - "Check out this amazing Web3 art collection featuring 10,000 unique avatars living on the blockchain! 🎨✨" 13 - ); 14 - }); 15 - 16 - it("should extract Twitter description from a community announcement", () => { 17 - const html = 18 - '<meta name="twitter:description" content="🚀 Big news! Our decentralized social platform just launched Spaces - join live conversations with your favorite creators and frens!">'; 19 - const { document } = parseHTML(html); 20 - const result = getDescription(document); 21 - expect(result).toBe( 22 - "🚀 Big news! Our decentralized social platform just launched Spaces - join live conversations with your favorite creators and frens!" 23 - ); 24 - }); 25 - 26 - it("should prioritize Open Graph description over Twitter description for blog post", () => { 27 - const html = ` 28 - <meta property="og:description" content="A comprehensive guide to understanding DeFi protocols, yield farming, and how to maximize your crypto earnings in 2024."> 29 - <meta name="twitter:description" content="Learn about DeFi and yield farming"> 30 - `; 31 - const { document } = parseHTML(html); 32 - const result = getDescription(document); 33 - expect(result).toBe( 34 - "A comprehensive guide to understanding DeFi protocols, yield farming, and how to maximize your crypto earnings in 2024." 35 - ); 36 - }); 37 - 38 - it("should return null when shared link has no social media descriptions", () => { 39 - const html = '<meta name="title" content="Random website">'; 40 - const { document } = parseHTML(html); 41 - const result = getDescription(document); 42 - expect(result).toBeNull(); 43 - }); 44 - 45 - it("should return null when shared post has empty descriptions", () => { 46 - const html = ` 47 - <meta property="og:description" content=""> 48 - <meta name="twitter:description" content=""> 49 - `; 50 - const { document } = parseHTML(html); 51 - const result = getDescription(document); 52 - expect(result).toBeNull(); 53 - }); 54 - 55 - it("should handle post descriptions with only whitespace", () => { 56 - const html = '<meta property="og:description" content=" ">'; 57 - const { document } = parseHTML(html); 58 - const result = getDescription(document); 59 - expect(result).toBe(" "); 60 - }); 61 - 62 - it("should handle post descriptions with mentions and special characters", () => { 63 - const html = 64 - '<meta property="og:description" content="Just minted my first NFT! 🎉 Shoutout to @opensea &amp; @ethereum for making this possible. &quot;The future is here&quot; 🚀">'; 65 - const { document } = parseHTML(html); 66 - const result = getDescription(document); 67 - expect(result).toBe( 68 - 'Just minted my first NFT! 🎉 Shoutout to @opensea & @ethereum for making this possible. "The future is here" 🚀' 69 - ); 70 - }); 71 - 72 - it("should handle very long thread descriptions", () => { 73 - const longDescription = 74 - "🧵 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."; 75 - const html = `<meta property="og:description" content="${longDescription}">`; 76 - const { document } = parseHTML(html); 77 - const result = getDescription(document); 78 - expect(result).toBe(longDescription); 79 - }); 80 - 81 - it("should handle multi-line post descriptions with formatting", () => { 82 - const html = 83 - '<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! 🛠️">'; 84 - const { document } = parseHTML(html); 85 - const result = getDescription(document); 86 - expect(result).toBe( 87 - "GM builders! 🌅\n\nJust shipped a new feature for our social dApp.\n\nCan't wait to see what you all build with it! 🛠️" 88 - ); 89 - }); 90 - 91 - it("should handle international post descriptions with emojis", () => { 92 - const html = 93 - '<meta property="og:description" content="🌏 Building the future of social media together! 一起构建社交媒体的未来!Web3コミュニティへようこそ 🚀">'; 94 - const { document } = parseHTML(html); 95 - const result = getDescription(document); 96 - expect(result).toBe( 97 - "🌏 Building the future of social media together! 一起构建社交媒体的未来!Web3コミュニティへようこそ 🚀" 98 - ); 99 - }); 100 - 101 - it("should handle descriptions with HTML entities from user-generated content", () => { 102 - const html = 103 - '<meta property="og:description" content="Posted some code on my profile: &lt;script&gt; console.log(&quot;Hello Web3!&quot;) &lt;/script&gt; Check it out!">'; 104 - const { document } = parseHTML(html); 105 - const result = getDescription(document); 106 - expect(result).toBe( 107 - 'Posted some code on my profile: <script> console.log("Hello Web3!") </script> Check it out!' 108 - ); 109 - }); 110 - });
-93
apps/api/src/routes/oembed/helpers/meta/getMetaContent.test.ts
··· 1 - import { parseHTML } from "linkedom"; 2 - import { describe, expect, it } from "vitest"; 3 - import getMetaContent from "./getMetaContent"; 4 - 5 - describe("getMetaContent", () => { 6 - it("should extract profile description meta content", () => { 7 - const html = 8 - '<meta name="description" content="Crypto enthusiast, NFT collector, and Web3 builder sharing daily insights about the decentralized future 🌐">'; 9 - const { document } = parseHTML(html); 10 - const result = getMetaContent(document, "description"); 11 - expect(result).toBe( 12 - "Crypto enthusiast, NFT collector, and Web3 builder sharing daily insights about the decentralized future 🌐" 13 - ); 14 - }); 15 - 16 - it("should extract social media post title from property attribute", () => { 17 - const html = 18 - '<meta property="og:title" content="Just launched my first dApp! 🚀 Check out this decentralized social network">'; 19 - const { document } = parseHTML(html); 20 - const result = getMetaContent(document, "og:title"); 21 - expect(result).toBe( 22 - "Just launched my first dApp! 🚀 Check out this decentralized social network" 23 - ); 24 - }); 25 - 26 - it("should return null when user profile has no meta description", () => { 27 - const html = '<meta name="keywords" content="blockchain,web3,defi">'; 28 - const { document } = parseHTML(html); 29 - const result = getMetaContent(document, "description"); 30 - expect(result).toBeNull(); 31 - }); 32 - 33 - it("should return null when shared link has incomplete meta tags", () => { 34 - const html = '<meta name="description">'; 35 - const { document } = parseHTML(html); 36 - const result = getMetaContent(document, "description"); 37 - expect(result).toBeNull(); 38 - }); 39 - 40 - it("should return empty string when user has empty bio", () => { 41 - const html = '<meta name="description" content="">'; 42 - const { document } = parseHTML(html); 43 - const result = getMetaContent(document, "description"); 44 - expect(result).toBe(""); 45 - }); 46 - 47 - it("should handle case-sensitive attribute matching for social media cards", () => { 48 - const html = 49 - '<meta NAME="description" content="Building the future of social media">'; 50 - const { document } = parseHTML(html); 51 - const result = getMetaContent(document, "description"); 52 - expect(result).toBeNull(); 53 - }); 54 - 55 - it("should prefer name attribute over property for profile descriptions", () => { 56 - const html = ` 57 - <meta name="description" content="DeFi researcher | Building on Ethereum | Follow for alpha 📈"> 58 - <meta property="description" content="Generic description"> 59 - `; 60 - const { document } = parseHTML(html); 61 - const result = getMetaContent(document, "description"); 62 - expect(result).toBe( 63 - "DeFi researcher | Building on Ethereum | Follow for alpha 📈" 64 - ); 65 - }); 66 - 67 - it("should handle multiple meta tags and return first social media description", () => { 68 - const html = ` 69 - <meta name="description" content="GM! Just dropped my new NFT collection 🎨"> 70 - <meta name="description" content="Second description"> 71 - `; 72 - const { document } = parseHTML(html); 73 - const result = getMetaContent(document, "description"); 74 - expect(result).toBe("GM! Just dropped my new NFT collection 🎨"); 75 - }); 76 - 77 - it("should handle special characters in social media content", () => { 78 - const html = 79 - '<meta name="description" content="Talking about @ethereum &amp; @uniswap - the future of &quot;DeFi&quot; is here!">'; 80 - const { document } = parseHTML(html); 81 - const result = getMetaContent(document, "description"); 82 - expect(result).toBe( 83 - 'Talking about @ethereum & @uniswap - the future of "DeFi" is here!' 84 - ); 85 - }); 86 - 87 - it("should return whitespace-only content from minimal social posts", () => { 88 - const html = '<meta name="description" content=" ">'; 89 - const { document } = parseHTML(html); 90 - const result = getMetaContent(document, "description"); 91 - expect(result).toBe(" "); 92 - }); 93 - });
-94
apps/api/src/routes/oembed/helpers/meta/getTitle.test.ts
··· 1 - import { parseHTML } from "linkedom"; 2 - import { describe, expect, it } from "vitest"; 3 - import getTitle from "./getTitle"; 4 - 5 - describe("getTitle", () => { 6 - it("should extract Open Graph title from a viral meme post", () => { 7 - const html = 8 - '<meta property="og:title" content="When you finally understand Web3 social media 😂">'; 9 - const { document } = parseHTML(html); 10 - const result = getTitle(document); 11 - expect(result).toBe("When you finally understand Web3 social media 😂"); 12 - }); 13 - 14 - it("should extract Twitter title from a trending hashtag post", () => { 15 - const html = 16 - '<meta name="twitter:title" content="Breaking: #LensSocial hits 1M users! 🚀">'; 17 - const { document } = parseHTML(html); 18 - const result = getTitle(document); 19 - expect(result).toBe("Breaking: #LensSocial hits 1M users! 🚀"); 20 - }); 21 - 22 - it("should prioritize Open Graph title over Twitter title for shared blog post", () => { 23 - const html = ` 24 - <meta property="og:title" content="How I Built a Decentralized Social App in 2024"> 25 - <meta name="twitter:title" content="My Journey Building on Lens Protocol"> 26 - `; 27 - const { document } = parseHTML(html); 28 - const result = getTitle(document); 29 - expect(result).toBe("How I Built a Decentralized Social App in 2024"); 30 - }); 31 - 32 - it("should return null when user shares a page without social media metadata", () => { 33 - const html = '<meta name="description" content="Random page content">'; 34 - const { document } = parseHTML(html); 35 - const result = getTitle(document); 36 - expect(result).toBeNull(); 37 - }); 38 - 39 - it("should return null when shared link has empty social media titles", () => { 40 - const html = ` 41 - <meta property="og:title" content=""> 42 - <meta name="twitter:title" content=""> 43 - `; 44 - const { document } = parseHTML(html); 45 - const result = getTitle(document); 46 - expect(result).toBeNull(); 47 - }); 48 - 49 - it("should handle post titles with only whitespace", () => { 50 - const html = '<meta property="og:title" content=" ">'; 51 - const { document } = parseHTML(html); 52 - const result = getTitle(document); 53 - expect(result).toBe(" "); 54 - }); 55 - 56 - it("should handle post titles with special characters and mentions", () => { 57 - const html = 58 - '<meta property="og:title" content="Check out @vitalik.eth&apos;s latest post about &quot;The Future of DeFi&quot;">'; 59 - const { document } = parseHTML(html); 60 - const result = getTitle(document); 61 - expect(result).toBe( 62 - 'Check out @vitalik.eth\'s latest post about "The Future of DeFi"' 63 - ); 64 - }); 65 - 66 - it("should handle very long thread titles", () => { 67 - const longTitle = 68 - "🧵 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."; 69 - const html = `<meta property="og:title" content="${longTitle}">`; 70 - const { document } = parseHTML(html); 71 - const result = getTitle(document); 72 - expect(result).toBe(longTitle); 73 - }); 74 - 75 - it("should handle multi-line post titles with formatting", () => { 76 - const html = 77 - '<meta property="og:title" content="GM fam!\n\nJust dropped my latest NFT collection 🎨">'; 78 - const { document } = parseHTML(html); 79 - const result = getTitle(document); 80 - expect(result).toBe( 81 - "GM fam!\n\nJust dropped my latest NFT collection 🎨" 82 - ); 83 - }); 84 - 85 - it("should handle international post titles with emojis", () => { 86 - const html = 87 - '<meta property="og:title" content="🌍 Global Web3 Community Meetup - 我们一起建设未来 🚀">'; 88 - const { document } = parseHTML(html); 89 - const result = getTitle(document); 90 - expect(result).toBe( 91 - "🌍 Global Web3 Community Meetup - 我们一起建设未来 🚀" 92 - ); 93 - }); 94 - });
-27
apps/api/src/routes/og/getAccount.test.ts
··· 1 - import { Hono } from "hono"; 2 - import { beforeEach, describe, expect, it, vi } from "vitest"; 3 - 4 - const mockHtml = `<html><head><meta property="og:title" content="Account" /></head></html>`; 5 - 6 - vi.mock("./ogUtils", () => ({ 7 - default: vi.fn(async ({ ctx }) => ctx.html(mockHtml, 200)) 8 - })); 9 - 10 - import getAccount from "./getAccount"; 11 - 12 - describe("getAccount", () => { 13 - let app: Hono; 14 - 15 - beforeEach(() => { 16 - app = new Hono(); 17 - app.get("/u/:username", getAccount); 18 - }); 19 - 20 - it("returns og html", async () => { 21 - const res = await app.request("/u/test"); 22 - const html = await res.text(); 23 - 24 - expect(res.status).toBe(200); 25 - expect(html).toContain("og:title"); 26 - }); 27 - });
-27
apps/api/src/routes/og/getGroup.test.ts
··· 1 - import { Hono } from "hono"; 2 - import { beforeEach, describe, expect, it, vi } from "vitest"; 3 - 4 - const mockHtml = `<html><head><meta property="og:title" content="Group" /></head></html>`; 5 - 6 - vi.mock("./ogUtils", () => ({ 7 - default: vi.fn(async ({ ctx }) => ctx.html(mockHtml, 200)) 8 - })); 9 - 10 - import getGroup from "./getGroup"; 11 - 12 - describe("getGroup", () => { 13 - let app: Hono; 14 - 15 - beforeEach(() => { 16 - app = new Hono(); 17 - app.get("/g/:address", getGroup); 18 - }); 19 - 20 - it("returns og html", async () => { 21 - const res = await app.request("/g/0x1234"); 22 - const html = await res.text(); 23 - 24 - expect(res.status).toBe(200); 25 - expect(html).toContain("og:title"); 26 - }); 27 - });
-27
apps/api/src/routes/og/getPost.test.ts
··· 1 - import { Hono } from "hono"; 2 - import { beforeEach, describe, expect, it, vi } from "vitest"; 3 - 4 - const mockHtml = `<html><head><meta property="og:title" content="Post" /></head></html>`; 5 - 6 - vi.mock("./ogUtils", () => ({ 7 - default: vi.fn(async ({ ctx }) => ctx.html(mockHtml, 200)) 8 - })); 9 - 10 - import getPost from "./getPost"; 11 - 12 - describe("getPost", () => { 13 - let app: Hono; 14 - 15 - beforeEach(() => { 16 - app = new Hono(); 17 - app.get("/posts/:slug", getPost); 18 - }); 19 - 20 - it("returns og html", async () => { 21 - const res = await app.request("/posts/example"); 22 - const html = await res.text(); 23 - 24 - expect(res.status).toBe(200); 25 - expect(html).toContain("og:title"); 26 - }); 27 - });
-26
apps/api/src/utils/getDbPostId.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import getDbPostId from "./getDbPostId"; 3 - 4 - describe("getDbPostId", () => { 5 - it("converts decimal string to hex with prefix", () => { 6 - const result = getDbPostId("1234567890"); 7 - expect(result).toBe("\\x499602d2"); 8 - }); 9 - 10 - it("returns empty string when input is empty", () => { 11 - expect(getDbPostId("")).toBe(""); 12 - }); 13 - 14 - it("throws error for non digit strings", () => { 15 - expect(() => getDbPostId("abc123")).toThrow("Invalid decimal value"); 16 - }); 17 - 18 - it("throws error for negative numbers", () => { 19 - expect(() => getDbPostId("-1")).toThrow("Invalid decimal value"); 20 - }); 21 - 22 - it("handles large numbers", () => { 23 - const large = "18446744073709551615"; 24 - expect(getDbPostId(large)).toBe("\\xffffffffffffffff"); 25 - }); 26 - });
-10
apps/api/src/utils/sha256.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import sha256 from "./sha256"; 3 - 4 - describe("sha256", () => { 5 - it("hashes text correctly", () => { 6 - expect(sha256("hello")).toBe( 7 - "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" 8 - ); 9 - }); 10 - });
-13
apps/api/vitest.config.ts
··· 1 - import path from "node:path"; 2 - import { defineConfig } from "vitest/config"; 3 - 4 - export default defineConfig({ 5 - resolve: { 6 - alias: { 7 - "@": path.resolve(__dirname, "./src") 8 - } 9 - }, 10 - test: { 11 - environment: "node" 12 - } 13 - });
-1
apps/web/README.md
··· 6 6 7 7 - `pnpm dev` – run the development server. 8 8 - `pnpm build` – create a production build. 9 - - `pnpm test` – execute vitest tests.
+1 -6
apps/web/package.json
··· 7 7 "build": "vite build", 8 8 "dev": "vite dev --port 4783", 9 9 "start": "vite preview --port 4783", 10 - "test": "vitest run", 11 10 "typecheck": "tsc --pretty" 12 11 }, 13 12 "dependencies": { ··· 75 74 "@hey/types": "workspace:*", 76 75 "@tailwindcss/aspect-ratio": "^0.4.2", 77 76 "@tailwindcss/forms": "^0.5.10", 78 - "@testing-library/jest-dom": "^6.7.0", 79 - "@testing-library/react": "^16.3.0", 80 - "@testing-library/user-event": "^14.6.1", 81 77 "@types/hast": "^3.0.4", 82 78 "@types/node": "^24.2.1", 83 79 "@types/react": "^19.1.10", ··· 86 82 "jsdom": "^26.1.0", 87 83 "typescript": "^5.9.2", 88 84 "vite-plugin-environment": "^1.1.3", 89 - "vite-tsconfig-paths": "^5.1.4", 90 - "vitest": "^3.2.4" 85 + "vite-tsconfig-paths": "^5.1.4" 91 86 } 92 87 }
-32
apps/web/src/components/Home/Home.test.tsx
··· 1 - import { render } from "@testing-library/react"; 2 - import type React from "react"; 3 - import { describe, expect, it, vi } from "vitest"; 4 - import Home from "./index"; 5 - 6 - vi.mock("../Composer/NewPost", () => ({ default: () => <div>NewPost</div> })); 7 - vi.mock("../Explore/ExploreFeed", () => ({ 8 - default: () => <div>ExploreFeed</div> 9 - })); 10 - vi.mock("../Shared/PageLayout", () => ({ 11 - default: ({ children }: { children: React.ReactNode }) => ( 12 - <div>{children}</div> 13 - ) 14 - })); 15 - vi.mock("../../store/persisted/useAccountStore", () => ({ 16 - useAccountStore: () => ({ currentAccount: undefined }) 17 - })); 18 - vi.mock("../../store/persisted/useHomeTabStore", () => ({ 19 - useHomeTabStore: () => ({ feedType: "FORYOU" }) 20 - })); 21 - vi.mock("./FeedType", () => ({ default: () => <div>FeedType</div> })); 22 - vi.mock("./ForYou", () => ({ default: () => <div>ForYou</div> })); 23 - vi.mock("./Hero", () => ({ default: () => <div>Hero</div> })); 24 - vi.mock("./Highlights", () => ({ default: () => <div>Highlights</div> })); 25 - vi.mock("./Timeline", () => ({ default: () => <div>Timeline</div> })); 26 - 27 - describe("Home", () => { 28 - it("renders correctly", () => { 29 - const { container } = render(<Home />); 30 - expect(container).toMatchSnapshot(); 31 - }); 32 - });
-81
apps/web/src/helpers/accountPictureUtils.test.ts
··· 1 - import { beforeEach, describe, expect, it, vi } from "vitest"; 2 - 3 - vi.mock("./compressImage", () => ({ 4 - default: vi.fn() 5 - })); 6 - 7 - vi.mock("./uploadToIPFS", () => ({ 8 - uploadFileToIPFS: vi.fn() 9 - })); 10 - 11 - import uploadCroppedImage, { readFile } from "./accountPictureUtils"; 12 - import compressImage from "./compressImage"; 13 - import { uploadFileToIPFS } from "./uploadToIPFS"; 14 - 15 - const mockedCompressImage = compressImage as unknown as ReturnType< 16 - typeof vi.fn 17 - >; 18 - const mockedUploadFileToIPFS = uploadFileToIPFS as unknown as ReturnType< 19 - typeof vi.fn 20 - >; 21 - 22 - beforeEach(() => { 23 - vi.clearAllMocks(); 24 - }); 25 - 26 - describe("readFile", () => { 27 - it("returns a data URL for the provided blob", async () => { 28 - const blob = new Blob(["hello"], { type: "image/png" }); 29 - const url = await readFile(blob); 30 - const expected = `data:image/png;base64,${Buffer.from("hello").toString("base64")}`; 31 - expect(url).toBe(expected); 32 - }); 33 - }); 34 - 35 - describe("uploadCroppedImage", () => { 36 - it("uploads the cropped image and returns the uri", async () => { 37 - const canvas = document.createElement("canvas"); 38 - const blob = new Blob(["img"], { type: "image/png" }); 39 - Object.assign(canvas, { 40 - toBlob: (cb: (b: Blob | null) => void) => cb(blob) 41 - }); 42 - 43 - const compressed = new File([blob], "compressed.png", { 44 - type: "image/png" 45 - }); 46 - mockedCompressImage.mockResolvedValue(compressed); 47 - mockedUploadFileToIPFS.mockResolvedValue({ 48 - mimeType: "image/png", 49 - uri: "ipfs://image" 50 - }); 51 - 52 - const url = await uploadCroppedImage(canvas); 53 - expect(url).toBe("ipfs://image"); 54 - expect(mockedCompressImage).toHaveBeenCalledWith(expect.any(File), { 55 - maxSizeMB: 6, 56 - maxWidthOrHeight: 3000 57 - }); 58 - expect(mockedUploadFileToIPFS).toHaveBeenCalledWith(compressed); 59 - }); 60 - 61 - it("throws when uploadFileToIPFS returns an empty uri", async () => { 62 - const canvas = document.createElement("canvas"); 63 - const blob = new Blob(["img"], { type: "image/png" }); 64 - Object.assign(canvas, { 65 - toBlob: (cb: (b: Blob | null) => void) => cb(blob) 66 - }); 67 - 68 - const compressed = new File([blob], "compressed.png", { 69 - type: "image/png" 70 - }); 71 - mockedCompressImage.mockResolvedValue(compressed); 72 - mockedUploadFileToIPFS.mockResolvedValue({ 73 - mimeType: "image/png", 74 - uri: "" 75 - }); 76 - 77 - await expect(uploadCroppedImage(canvas)).rejects.toThrow( 78 - "uploadFileToIPFS failed" 79 - ); 80 - }); 81 - });
-111
apps/web/src/helpers/attachmentUtils.test.ts
··· 1 - import { beforeEach, describe, expect, it, vi } from "vitest"; 2 - 3 - vi.mock("@hey/helpers/generateUUID", () => ({ default: vi.fn() })); 4 - vi.mock("./compressImage", () => ({ 5 - default: vi.fn(async (file: File) => file) 6 - })); 7 - vi.mock("sonner", () => ({ toast: { error: vi.fn() } })); 8 - 9 - import generateUUID from "@hey/helpers/generateUUID"; 10 - import { toast } from "sonner"; 11 - import { 12 - compressFiles, 13 - createPreviewAttachments, 14 - validateFileSize 15 - } from "./attachmentUtils"; 16 - import compressImage from "./compressImage"; 17 - 18 - Object.defineProperty(global.URL, "createObjectURL", { 19 - value: vi.fn(), 20 - writable: true 21 - }); 22 - 23 - describe("attachmentUtils", () => { 24 - beforeEach(() => { 25 - vi.clearAllMocks(); 26 - }); 27 - 28 - describe("validateFileSize", () => { 29 - it("returns false and shows toast when image exceeds limit", () => { 30 - const file = new File(["a"], "img.png", { type: "image/png" }); 31 - Object.defineProperty(file, "size", { value: 50000001 }); 32 - 33 - const result = validateFileSize(file); 34 - 35 - expect(result).toBe(false); 36 - expect(toast.error).toHaveBeenCalledWith( 37 - "Image size should be less than 50MB" 38 - ); 39 - }); 40 - 41 - it("returns true when file size is within limit", () => { 42 - const file = new File(["a"], "img.png", { type: "image/png" }); 43 - Object.defineProperty(file, "size", { value: 10 }); 44 - 45 - const result = validateFileSize(file); 46 - 47 - expect(result).toBe(true); 48 - expect(toast.error).not.toHaveBeenCalled(); 49 - }); 50 - }); 51 - 52 - describe("compressFiles", () => { 53 - it("compresses image files", async () => { 54 - const file = new File(["a"], "img.png", { type: "image/png" }); 55 - const compressed = new File(["b"], "img.png", { type: "image/png" }); 56 - vi.mocked(compressImage).mockResolvedValueOnce(compressed); 57 - 58 - const [result] = await compressFiles([file]); 59 - 60 - expect(compressImage).toHaveBeenCalledWith(file, { 61 - maxSizeMB: 9, 62 - maxWidthOrHeight: 6000 63 - }); 64 - expect(result).toBe(compressed); 65 - }); 66 - 67 - it("skips compression for gifs", async () => { 68 - const file = new File(["a"], "anim.gif", { type: "image/gif" }); 69 - 70 - const [result] = await compressFiles([file]); 71 - 72 - expect(compressImage).not.toHaveBeenCalled(); 73 - expect(result).toBe(file); 74 - }); 75 - }); 76 - 77 - describe("createPreviewAttachments", () => { 78 - it("creates preview attachments with ids and urls", () => { 79 - vi.mocked(generateUUID) 80 - .mockReturnValueOnce("id1") 81 - .mockReturnValueOnce("id2"); 82 - const urlSpy = vi 83 - .spyOn(URL, "createObjectURL") 84 - .mockReturnValueOnce("url1") 85 - .mockReturnValueOnce("url2"); 86 - const image = new File(["a"], "img.png", { type: "image/png" }); 87 - const video = new File(["a"], "vid.mp4", { type: "video/mp4" }); 88 - 89 - const result = createPreviewAttachments([image, video]); 90 - 91 - expect(result).toEqual([ 92 - { 93 - file: image, 94 - id: "id1", 95 - mimeType: "image/png", 96 - previewUri: "url1", 97 - type: "Image" 98 - }, 99 - { 100 - file: video, 101 - id: "id2", 102 - mimeType: "video/mp4", 103 - previewUri: "url2", 104 - type: "Video" 105 - } 106 - ]); 107 - 108 - urlSpy.mockRestore(); 109 - }); 110 - }); 111 - });
-19
apps/web/src/helpers/cn.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import cn from "./cn"; 3 - 4 - describe("cn", () => { 5 - it("combines post classes", () => { 6 - expect(cn("post", "rounded", "shadow")).toBe("post rounded shadow"); 7 - }); 8 - 9 - it("overrides conflicting styles", () => { 10 - expect(cn("p-4", "p-2")).toBe("p-2"); 11 - }); 12 - 13 - it("handles like button state", () => { 14 - const liked = true; 15 - expect(cn("like-button", "text-gray-500", liked && "text-red-500")).toBe( 16 - "like-button text-red-500" 17 - ); 18 - }); 19 - });
-38
apps/web/src/helpers/collectActionParams.test.ts
··· 1 - import type { CollectActionType } from "@hey/types/hey"; 2 - import { describe, expect, it } from "vitest"; 3 - import collectActionParams from "./collectActionParams"; 4 - 5 - describe("collectActionParams", () => { 6 - it("maps collect action fields to simple collect config", () => { 7 - const collectAction: CollectActionType = { 8 - collectLimit: 10, 9 - endsAt: "2024-01-01T00:00:00.000Z", 10 - followerOnly: true, 11 - payToCollect: { recipients: [] } as any 12 - }; 13 - 14 - const result = collectActionParams(collectAction); 15 - 16 - expect(result).toEqual({ 17 - simpleCollect: { 18 - collectLimit: 10, 19 - endsAt: "2024-01-01T00:00:00.000Z", 20 - payToCollect: { recipients: [] } 21 - } 22 - }); 23 - }); 24 - 25 - it("handles missing optional fields", () => { 26 - const collectAction: CollectActionType = {}; 27 - 28 - const result = collectActionParams(collectAction); 29 - 30 - expect(result).toEqual({ 31 - simpleCollect: { 32 - collectLimit: undefined, 33 - endsAt: undefined, 34 - payToCollect: undefined 35 - } 36 - }); 37 - }); 38 - });
-37
apps/web/src/helpers/compressImage.test.ts
··· 1 - import imageCompression from "browser-image-compression"; 2 - import { describe, expect, it, vi } from "vitest"; 3 - import compressImage from "./compressImage"; 4 - 5 - vi.mock("browser-image-compression"); 6 - 7 - describe("compressImage", () => { 8 - it("merges provided options with defaults", async () => { 9 - const file = new File(["data"], "photo.jpg", { type: "image/jpeg" }); 10 - const options = { maxSizeMB: 1 }; 11 - const compressed = new File(["compressed"], "photo.jpg", { 12 - type: "image/jpeg" 13 - }); 14 - 15 - vi.mocked(imageCompression).mockResolvedValueOnce(compressed); 16 - 17 - const result = await compressImage(file, options); 18 - 19 - expect(imageCompression).toHaveBeenCalledWith(file, { 20 - exifOrientation: 1, 21 - useWebWorker: true, 22 - ...options 23 - }); 24 - expect(result).toBe(compressed); 25 - }); 26 - 27 - it("returns the compressed file from imageCompression", async () => { 28 - const file = new File(["f"], "avatar.png", { type: "image/png" }); 29 - const compressed = new File(["c"], "avatar.png", { type: "image/png" }); 30 - 31 - vi.mocked(imageCompression).mockResolvedValueOnce(compressed); 32 - 33 - const result = await compressImage(file, {}); 34 - 35 - expect(result).toBe(compressed); 36 - }); 37 - });
-18
apps/web/src/helpers/convertToTitleCase.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import convertToTitleCase from "./convertToTitleCase"; 3 - 4 - describe("convertToTitleCase", () => { 5 - it("converts snake_case to title case", () => { 6 - expect(convertToTitleCase("follower_count")).toBe("Follower Count"); 7 - expect(convertToTitleCase("USER_NAME")).toBe("User Name"); 8 - }); 9 - 10 - it("converts camelCase and PascalCase to title case", () => { 11 - expect(convertToTitleCase("postLikes")).toBe("Post Likes"); 12 - expect(convertToTitleCase("ShareCount")).toBe("Share Count"); 13 - }); 14 - 15 - it("handles single word strings", () => { 16 - expect(convertToTitleCase("trend")).toBe("Trend"); 17 - }); 18 - });
-78
apps/web/src/helpers/cropUtils.test.ts
··· 1 - import type { Area } from "react-easy-crop"; 2 - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 3 - import getCroppedImg from "./cropUtils"; 4 - 5 - class TestImage { 6 - onload?: () => void; 7 - onerror?: (err: unknown) => void; 8 - width = 40; 9 - height = 30; 10 - private listeners: Record<string, Array<() => void>> = {}; 11 - addEventListener(event: string, cb: () => void) { 12 - if (!this.listeners[event]) { 13 - this.listeners[event] = []; 14 - } 15 - this.listeners[event].push(cb); 16 - } 17 - set src(_url: string) { 18 - const loadHandlers = this.listeners["load"]; 19 - if (loadHandlers) { 20 - for (const cb of loadHandlers) { 21 - cb(); 22 - } 23 - } 24 - } 25 - } 26 - 27 - describe("getCroppedImg", () => { 28 - const originalImage = global.Image; 29 - const originalGetContext = HTMLCanvasElement.prototype.getContext; 30 - 31 - beforeEach(() => { 32 - global.Image = TestImage as unknown as typeof Image; 33 - }); 34 - 35 - afterEach(() => { 36 - global.Image = originalImage; 37 - HTMLCanvasElement.prototype.getContext = originalGetContext; 38 - vi.restoreAllMocks(); 39 - }); 40 - 41 - it("returns null when pixelCrop is null", async () => { 42 - const result = await getCroppedImg("test", null); 43 - expect(result).toBeNull(); 44 - }); 45 - 46 - it("returns null when context is unavailable", async () => { 47 - HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue(null); 48 - const area: Area = { height: 10, width: 10, x: 0, y: 0 }; 49 - const result = await getCroppedImg("test", area); 50 - expect(result).toBeNull(); 51 - }); 52 - 53 - it("crops and resizes canvas to the provided area", async () => { 54 - const drawImage = vi.fn(); 55 - const getImageData = vi.fn(() => ({})); 56 - const putImageData = vi.fn(); 57 - HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue({ 58 - drawImage, 59 - getImageData, 60 - putImageData 61 - } as unknown as CanvasRenderingContext2D); 62 - 63 - const area: Area = { height: 15, width: 20, x: 5, y: 5 }; 64 - const canvas = await getCroppedImg("test", area); 65 - 66 - expect(canvas).not.toBeNull(); 67 - expect(drawImage).toHaveBeenCalled(); 68 - expect(getImageData).toHaveBeenCalledWith( 69 - area.x, 70 - area.y, 71 - area.width, 72 - area.height 73 - ); 74 - expect(putImageData).toHaveBeenCalled(); 75 - expect(canvas?.width).toBe(area.width); 76 - expect(canvas?.height).toBe(area.height); 77 - }); 78 - });
-45
apps/web/src/helpers/datetime/formatRelativeOrAbsolute.test.ts
··· 1 - import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; 2 - import formatRelativeOrAbsolute from "./formatRelativeOrAbsolute"; 3 - 4 - describe("formatRelativeOrAbsolute", () => { 5 - const now = new Date("2024-05-15T12:00:00Z"); 6 - 7 - beforeAll(() => { 8 - vi.useFakeTimers(); 9 - vi.setSystemTime(now); 10 - }); 11 - 12 - afterAll(() => { 13 - vi.useRealTimers(); 14 - }); 15 - 16 - it("formats seconds ago", () => { 17 - const date = new Date(now.getTime() - 30 * 1000); 18 - expect(formatRelativeOrAbsolute(date)).toBe("30s"); 19 - }); 20 - 21 - it("formats minutes ago", () => { 22 - const date = new Date(now.getTime() - 3 * 60 * 1000); 23 - expect(formatRelativeOrAbsolute(date)).toBe("3m"); 24 - }); 25 - 26 - it("formats hours ago", () => { 27 - const date = new Date(now.getTime() - 2 * 60 * 60 * 1000); 28 - expect(formatRelativeOrAbsolute(date)).toBe("2h"); 29 - }); 30 - 31 - it("formats days ago within a week", () => { 32 - const date = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000); 33 - expect(formatRelativeOrAbsolute(date)).toBe("5d"); 34 - }); 35 - 36 - it("formats more than a week in same year", () => { 37 - const date = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); 38 - expect(formatRelativeOrAbsolute(date)).toBe("May 1"); 39 - }); 40 - 41 - it("formats dates from previous year", () => { 42 - const date = new Date("2023-12-20T12:00:00Z"); 43 - expect(formatRelativeOrAbsolute(date)).toBe("Dec 20, 2023"); 44 - }); 45 - });
-115
apps/web/src/helpers/generateVideoThumbnails.test.ts
··· 1 - import { describe, expect, it, vi } from "vitest"; 2 - import generateVideoThumbnails from "./generateVideoThumbnails"; 3 - 4 - describe("generateVideoThumbnails", () => { 5 - it("returns empty array when file size is zero", async () => { 6 - const file = new File([], "empty.mp4", { type: "video/mp4" }); 7 - const thumbnails = await generateVideoThumbnails(file, 2); 8 - expect(thumbnails).toEqual([]); 9 - }); 10 - 11 - it("returns empty array when video fails to load", async () => { 12 - const file = new File([new Uint8Array([1])], "video.mp4", { 13 - type: "video/mp4" 14 - }); 15 - const originalCreate = (URL as any).createObjectURL; 16 - const originalRevoke = (URL as any).revokeObjectURL; 17 - const urlSpy = vi.fn(() => "blob:video"); 18 - const revokeSpy = vi.fn(); 19 - (URL as any).createObjectURL = urlSpy; 20 - (URL as any).revokeObjectURL = revokeSpy; 21 - 22 - const video: any = { 23 - addEventListener: vi.fn(), 24 - muted: false, 25 - remove: vi.fn(), 26 - src: "" 27 - }; 28 - 29 - const canvas: any = { 30 - remove: vi.fn() 31 - }; 32 - 33 - const createElementSpy = vi 34 - .spyOn(document, "createElement") 35 - .mockImplementation((tag) => (tag === "video" ? video : canvas)); 36 - 37 - const promise = generateVideoThumbnails(file, 2); 38 - video.onerror?.(); 39 - const thumbnails = await promise; 40 - 41 - expect(thumbnails).toEqual([]); 42 - expect(video.remove).toHaveBeenCalled(); 43 - expect(canvas.remove).toHaveBeenCalled(); 44 - createElementSpy.mockRestore(); 45 - (URL as any).createObjectURL = originalCreate; 46 - (URL as any).revokeObjectURL = originalRevoke; 47 - }); 48 - 49 - it("generates thumbnails when video loads", async () => { 50 - const file = new File([new Uint8Array([1])], "video.mp4", { 51 - type: "video/mp4" 52 - }); 53 - 54 - const originalCreate = (URL as any).createObjectURL; 55 - const originalRevoke = (URL as any).revokeObjectURL; 56 - 57 - const urlSpy = vi.fn(() => "blob:video"); 58 - const revokeSpy = vi.fn(); 59 - (URL as any).createObjectURL = urlSpy; 60 - (URL as any).revokeObjectURL = revokeSpy; 61 - 62 - let seekHandler: (() => void) | undefined; 63 - 64 - const video: any = { 65 - addEventListener: vi.fn((event: string, cb: () => void) => { 66 - if (event === "seeked") { 67 - seekHandler = cb; 68 - } 69 - }), 70 - duration: 2, 71 - muted: false, 72 - remove: vi.fn(), 73 - src: "", 74 - videoHeight: 50, 75 - videoWidth: 100 76 - }; 77 - 78 - Object.defineProperty(video, "currentTime", { 79 - set() { 80 - if (seekHandler) { 81 - Promise.resolve().then(() => seekHandler?.()); 82 - } 83 - } 84 - }); 85 - 86 - let drawCount = 0; 87 - const context = { drawImage: vi.fn(() => drawCount++) }; 88 - let call = 0; 89 - const canvas: any = { 90 - getContext: vi.fn(() => context), 91 - height: 0, 92 - remove: vi.fn(), 93 - toDataURL: vi.fn(() => `data:image/png;base64,${call++}`), 94 - width: 0 95 - }; 96 - 97 - const createElementSpy = vi 98 - .spyOn(document, "createElement") 99 - .mockImplementation((tag) => (tag === "video" ? video : canvas)); 100 - 101 - const promise = generateVideoThumbnails(file, 2); 102 - video.onloadeddata?.(); 103 - const thumbnails = await promise; 104 - 105 - expect(thumbnails).toHaveLength(2); 106 - expect( 107 - thumbnails.every((t) => t.startsWith("data:image/png;base64,")) 108 - ).toBe(true); 109 - expect(drawCount).toBe(2); 110 - 111 - createElementSpy.mockRestore(); 112 - (URL as any).createObjectURL = originalCreate; 113 - (URL as any).revokeObjectURL = originalRevoke; 114 - }); 115 - });
-46
apps/web/src/helpers/getAccountAttribute.test.ts
··· 1 - import { 2 - type MetadataAttributeFragment, 3 - MetadataAttributeType 4 - } from "@hey/indexer"; 5 - import { describe, expect, it } from "vitest"; 6 - import getAccountAttribute from "./getAccountAttribute"; 7 - 8 - describe("getAccountAttribute", () => { 9 - it("returns the attribute value when present", () => { 10 - const attributes: MetadataAttributeFragment[] = [ 11 - { key: "location", type: MetadataAttributeType.String, value: "Berlin" }, 12 - { 13 - key: "website", 14 - type: MetadataAttributeType.String, 15 - value: "https://example.com" 16 - } 17 - ]; 18 - const result = getAccountAttribute("website", attributes); 19 - expect(result).toBe("https://example.com"); 20 - }); 21 - 22 - it("returns an empty string when the attribute is missing", () => { 23 - const attributes: MetadataAttributeFragment[] = [ 24 - { key: "location", type: MetadataAttributeType.String, value: "Berlin" } 25 - ]; 26 - const result = getAccountAttribute("x", attributes); 27 - expect(result).toBe(""); 28 - }); 29 - 30 - it("returns an empty string when attributes are undefined", () => { 31 - const result = getAccountAttribute("website", undefined); 32 - expect(result).toBe(""); 33 - }); 34 - 35 - it("handles the 'x' attribute", () => { 36 - const attributes: MetadataAttributeFragment[] = [ 37 - { 38 - key: "x", 39 - type: MetadataAttributeType.String, 40 - value: "https://x.com/hey" 41 - } 42 - ]; 43 - const result = getAccountAttribute("x", attributes); 44 - expect(result).toBe("https://x.com/hey"); 45 - }); 46 - });
-53
apps/web/src/helpers/getAnyKeyValue.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import getAnyKeyValue from "./getAnyKeyValue"; 3 - 4 - const addressKeyValue = { 5 - __typename: "AddressKeyValue", 6 - address: "0x1234", 7 - key: "owner" 8 - } as any; 9 - 10 - const bigDecimalKeyValue = { 11 - __typename: "BigDecimalKeyValue", 12 - bigDecimal: "1000", 13 - key: "followers" 14 - } as any; 15 - 16 - const stringKeyValue = { 17 - __typename: "StringKeyValue", 18 - key: "bio", 19 - string: "Hello there" 20 - } as any; 21 - 22 - const unknownKeyValue = { 23 - __typename: "RandomKeyValue", 24 - key: "location", 25 - value: "Earth" 26 - } as any; 27 - 28 - describe("getAnyKeyValue", () => { 29 - it("returns address value when key matches", () => { 30 - const result = getAnyKeyValue([addressKeyValue], "owner"); 31 - expect(result).toEqual({ address: "0x1234" }); 32 - }); 33 - 34 - it("returns bigDecimal value when key matches", () => { 35 - const result = getAnyKeyValue([bigDecimalKeyValue], "followers"); 36 - expect(result).toEqual({ bigDecimal: "1000" }); 37 - }); 38 - 39 - it("returns string value when key matches", () => { 40 - const result = getAnyKeyValue([stringKeyValue], "bio"); 41 - expect(result).toEqual({ string: "Hello there" }); 42 - }); 43 - 44 - it("returns null when key is missing", () => { 45 - const result = getAnyKeyValue([addressKeyValue], "missing"); 46 - expect(result).toBeNull(); 47 - }); 48 - 49 - it("returns null for unsupported key value type", () => { 50 - const result = getAnyKeyValue([unknownKeyValue], "location"); 51 - expect(result).toBeNull(); 52 - }); 53 - });
-40
apps/web/src/helpers/getAssetLicense.test.ts
··· 1 - import { MetadataLicenseType } from "@hey/indexer"; 2 - import { describe, expect, it } from "vitest"; 3 - import getAssetLicense from "./getAssetLicense"; 4 - 5 - describe("getAssetLicense", () => { 6 - it("returns null when license id is missing", () => { 7 - expect(getAssetLicense(undefined)).toBeNull(); 8 - }); 9 - 10 - it("handles CC0 license", () => { 11 - const result = getAssetLicense(MetadataLicenseType.Cco); 12 - expect(result).toEqual({ 13 - helper: 14 - "Anyone can use, modify and distribute the work without any restrictions or need for attribution. CC0", 15 - label: "CC0 - no restrictions" 16 - }); 17 - }); 18 - 19 - it("handles commercial rights license", () => { 20 - const result = getAssetLicense(MetadataLicenseType.TbnlCdNplLegal); 21 - expect(result).toEqual({ 22 - helper: 23 - "You allow the collector to use the content for any purpose, except creating or sharing any derivative works, such as remixes.", 24 - label: "Commercial rights for the collector" 25 - }); 26 - }); 27 - 28 - it("handles personal rights license", () => { 29 - const result = getAssetLicense(MetadataLicenseType.TbnlNcDNplLegal); 30 - expect(result).toEqual({ 31 - helper: 32 - "You allow the collector to use the content for any personal, non-commercial purpose, except creating or sharing any derivative works, such as remixes.", 33 - label: "Personal rights for the collector" 34 - }); 35 - }); 36 - 37 - it("returns null for unsupported license", () => { 38 - expect(getAssetLicense(MetadataLicenseType.CcBy)).toBeNull(); 39 - }); 40 - });
-48
apps/web/src/helpers/getBlockedMessage.test.ts
··· 1 - import { LENS_NAMESPACE } from "@hey/data/constants"; 2 - import type { AccountFragment } from "@hey/indexer"; 3 - import { describe, expect, it } from "vitest"; 4 - import { 5 - getBlockedByMeMessage, 6 - getBlockedMeMessage 7 - } from "./getBlockedMessage"; 8 - 9 - describe("getBlockedMessage", () => { 10 - it("returns message when current user blocked another account", () => { 11 - const account = { 12 - address: "0x1234567890abcdef1234567890abcdef12345678", 13 - owner: "0x01", 14 - username: { 15 - localName: "janedoe", 16 - value: `${LENS_NAMESPACE}janedoe` 17 - } 18 - } as unknown as AccountFragment; 19 - 20 - const result = getBlockedByMeMessage(account); 21 - expect(result).toBe("You have blocked @janedoe"); 22 - }); 23 - 24 - it("returns message when another account blocked the user", () => { 25 - const account = { 26 - address: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", 27 - owner: "0x01", 28 - username: { 29 - localName: "alice", 30 - value: `${LENS_NAMESPACE}alice` 31 - } 32 - } as unknown as AccountFragment; 33 - 34 - const result = getBlockedMeMessage(account); 35 - expect(result).toBe("@alice has blocked you"); 36 - }); 37 - 38 - it("uses address when username is missing", () => { 39 - const address = "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"; 40 - const account = { 41 - address, 42 - owner: "0x01" 43 - } as unknown as AccountFragment; 44 - 45 - const result = getBlockedMeMessage(account); 46 - expect(result).toBe("#0xab…abcd has blocked you"); 47 - }); 48 - });
-73
apps/web/src/helpers/getCollectActionData.test.ts
··· 1 - import type { SimpleCollectActionFragment } from "@hey/indexer"; 2 - import { describe, expect, it } from "vitest"; 3 - import getCollectActionData from "./getCollectActionData"; 4 - 5 - describe("getCollectActionData", () => { 6 - it("returns null for non-simple collect actions", () => { 7 - const action = { 8 - __typename: "Unknown" 9 - } as unknown as SimpleCollectActionFragment; 10 - const result = getCollectActionData(action); 11 - expect(result).toBeNull(); 12 - }); 13 - 14 - it("extracts data from a collect action", () => { 15 - const action: SimpleCollectActionFragment = { 16 - __typename: "SimpleCollectAction", 17 - address: "0x1", 18 - collectLimit: 10, 19 - endsAt: "2025-01-01", 20 - payToCollect: { 21 - __typename: "PayToCollectConfig", 22 - price: { 23 - __typename: "Erc20Amount", 24 - asset: { 25 - __typename: "Erc20", 26 - contract: { __typename: "NetworkAddress", address: "0xasset" }, 27 - decimals: 18, 28 - name: "Token", 29 - symbol: "TKN" 30 - }, 31 - value: "1.5" 32 - }, 33 - recipients: [ 34 - { __typename: "RecipientPercent", address: "0xabc", percent: 80 }, 35 - { __typename: "RecipientPercent", address: "0xdef", percent: 20 } 36 - ], 37 - referralShare: null 38 - } 39 - }; 40 - 41 - const result = getCollectActionData(action); 42 - expect(result).toEqual({ 43 - assetAddress: "0xasset", 44 - assetSymbol: "TKN", 45 - collectLimit: 10, 46 - endsAt: "2025-01-01", 47 - price: 1.5, 48 - recipients: [ 49 - { __typename: "RecipientPercent", address: "0xabc", percent: 80 }, 50 - { __typename: "RecipientPercent", address: "0xdef", percent: 20 } 51 - ] 52 - }); 53 - }); 54 - 55 - it("handles actions without payment configuration", () => { 56 - const action: SimpleCollectActionFragment = { 57 - __typename: "SimpleCollectAction", 58 - address: "0x2", 59 - collectLimit: undefined, 60 - endsAt: undefined, 61 - payToCollect: null 62 - }; 63 - const result = getCollectActionData(action); 64 - expect(result).toEqual({ 65 - assetAddress: undefined, 66 - assetSymbol: undefined, 67 - collectLimit: Number.NaN, 68 - endsAt: undefined, 69 - price: 0, 70 - recipients: [] 71 - }); 72 - }); 73 - });
-25
apps/web/src/helpers/getFavicon.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import getFavicon from "./getFavicon"; 3 - 4 - describe("getFavicon", () => { 5 - it("generates favicon URL for social media domain", () => { 6 - const result = getFavicon("https://twitter.com/lens"); 7 - expect(result).toBe( 8 - "https://external-content.duckduckgo.com/ip3/twitter.com.ico" 9 - ); 10 - }); 11 - 12 - it("handles subdomains correctly", () => { 13 - const result = getFavicon("https://m.facebook.com/user"); 14 - expect(result).toBe( 15 - "https://external-content.duckduckgo.com/ip3/m.facebook.com.ico" 16 - ); 17 - }); 18 - 19 - it("falls back to unknown domain on invalid URL", () => { 20 - const result = getFavicon("not a url"); 21 - expect(result).toBe( 22 - "https://external-content.duckduckgo.com/ip3/unknowndomain.ico" 23 - ); 24 - }); 25 - });
-74
apps/web/src/helpers/getFileFromDataURL.test.ts
··· 1 - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 - import getFileFromDataURL from "./getFileFromDataURL"; 3 - 4 - class TestImage { 5 - width = 1; 6 - height = 1; 7 - crossOrigin = ""; 8 - onload?: () => void; 9 - onerror?: () => void; 10 - set src(_url: string) { 11 - queueMicrotask(() => this.onload?.()); 12 - } 13 - } 14 - 15 - describe("getFileFromDataURL", () => { 16 - const originalImage = global.Image; 17 - 18 - beforeEach(() => { 19 - global.Image = TestImage as unknown as typeof Image; 20 - }); 21 - 22 - afterEach(() => { 23 - global.Image = originalImage; 24 - vi.restoreAllMocks(); 25 - }); 26 - 27 - it("creates a file from a data URL", async () => { 28 - const canvas: any = { 29 - getContext: vi.fn(), 30 - toBlob: (cb: (blob: Blob) => void) => { 31 - cb(new Blob(["data"], { type: "image/png" })); 32 - } 33 - }; 34 - 35 - const createElementSpy = vi 36 - .spyOn(document, "createElement") 37 - .mockReturnValue(canvas); 38 - 39 - const callback = vi.fn(); 40 - 41 - getFileFromDataURL( 42 - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/w8AAwMB/6evsdoAAAAASUVORK5CYII=", 43 - "photo.png", 44 - callback 45 - ); 46 - 47 - await Promise.resolve(); 48 - 49 - expect(callback).toHaveBeenCalledTimes(1); 50 - const file = callback.mock.calls[0][0] as File; 51 - expect(file).toBeInstanceOf(File); 52 - expect(file.name).toBe("photo.png"); 53 - expect(file.type).toBe("image/png"); 54 - 55 - createElementSpy.mockRestore(); 56 - }); 57 - 58 - it("does nothing when image fails to load", async () => { 59 - class ErrorImage extends TestImage { 60 - set src(_url: string) { 61 - queueMicrotask(() => this.onerror?.()); 62 - } 63 - } 64 - 65 - global.Image = ErrorImage as unknown as typeof Image; 66 - 67 - const callback = vi.fn(); 68 - getFileFromDataURL("data:,", "bad.png", callback); 69 - 70 - await Promise.resolve(); 71 - 72 - expect(callback).not.toHaveBeenCalled(); 73 - }); 74 - });
-41
apps/web/src/helpers/getMentions.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import getMentions from "./getMentions"; 3 - 4 - describe("getMentions", () => { 5 - it("returns empty array when post has no mentions", () => { 6 - expect(getMentions("")).toEqual([]); 7 - }); 8 - 9 - it("extracts a single mention from a GM post", () => { 10 - const result = getMentions( 11 - "GM @lens/vitalik! Love your latest article about Ethereum 2.0 🚀" 12 - ); 13 - expect(result).toEqual([ 14 - { 15 - account: "", 16 - namespace: "", 17 - replace: { from: "vitalik", to: "vitalik" } 18 - } 19 - ]); 20 - }); 21 - 22 - it("extracts multiple mentions from a community shoutout", () => { 23 - const result = getMentions( 24 - "Huge props to @lens/aave and @lens/Uniswap for building the DeFi infrastructure we all depend on!" 25 - ); 26 - expect(result).toEqual([ 27 - { account: "", namespace: "", replace: { from: "aave", to: "aave" } }, 28 - { 29 - account: "", 30 - namespace: "", 31 - replace: { from: "uniswap", to: "uniswap" } 32 - } 33 - ]); 34 - }); 35 - 36 - it("ignores @ symbols in email addresses within posts", () => { 37 - expect( 38 - getMentions("DM me at hello@lens/web3builder for collaborations") 39 - ).toEqual([]); 40 - }); 41 - });
-21
apps/web/src/helpers/getTokenImage.test.ts
··· 1 - import { STATIC_IMAGES_URL } from "@hey/data/constants"; 2 - import { describe, expect, it } from "vitest"; 3 - import getTokenImage from "./getTokenImage"; 4 - 5 - describe("getTokenImage", () => { 6 - it("returns default image when symbol is missing", () => { 7 - expect(getTokenImage()).toBe(`${STATIC_IMAGES_URL}/tokens/gho.svg`); 8 - expect(getTokenImage(undefined)).toBe( 9 - `${STATIC_IMAGES_URL}/tokens/gho.svg` 10 - ); 11 - expect(getTokenImage("")).toBe(`${STATIC_IMAGES_URL}/tokens/gho.svg`); 12 - }); 13 - 14 - it("lowercases the symbol and returns the correct path", () => { 15 - expect(getTokenImage("USDC")).toBe(`${STATIC_IMAGES_URL}/tokens/usdc.svg`); 16 - expect(getTokenImage("eth")).toBe(`${STATIC_IMAGES_URL}/tokens/eth.svg`); 17 - expect(getTokenImage("WmAtIc")).toBe( 18 - `${STATIC_IMAGES_URL}/tokens/wmatic.svg` 19 - ); 20 - }); 21 - });
-32
apps/web/src/helpers/getURLs.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import getURLs from "./getURLs"; 3 - 4 - describe("getURLs", () => { 5 - it("extracts all URLs from a social media post", () => { 6 - const post = 7 - "Check out https://example.com/blog and http://social.com/profile/hey #fun"; 8 - const result = getURLs(post); 9 - expect(result).toEqual([ 10 - "https://example.com/blog", 11 - "http://social.com/profile/hey" 12 - ]); 13 - }); 14 - 15 - it("returns an empty array when no URLs are present", () => { 16 - const post = "Enjoying the sunshine #weekend @friend"; 17 - const result = getURLs(post); 18 - expect(result).toEqual([]); 19 - }); 20 - 21 - it("ignores invalid URL formats", () => { 22 - const post = "Visit htps://broken.com and http://good.com"; 23 - const result = getURLs(post); 24 - expect(result).toEqual(["http://good.com"]); 25 - }); 26 - 27 - it("handles punctuation after URLs", () => { 28 - const post = "Latest news: https://news.com/update! Stay tuned."; 29 - const result = getURLs(post); 30 - expect(result).toEqual(["https://news.com/update"]); 31 - }); 32 - });
-29
apps/web/src/helpers/getWalletDetails.test.ts
··· 1 - import { STATIC_IMAGES_URL } from "@hey/data/constants"; 2 - import { describe, expect, it } from "vitest"; 3 - import getWalletDetails from "./getWalletDetails"; 4 - 5 - describe("getWalletDetails", () => { 6 - it("returns family account provider details", () => { 7 - const result = getWalletDetails("familyAccountsProvider"); 8 - expect(result).toEqual({ 9 - logo: `${STATIC_IMAGES_URL}/wallets/family.png`, 10 - name: "Login with Family" 11 - }); 12 - }); 13 - 14 - it("returns injected wallet details", () => { 15 - const result = getWalletDetails("injected"); 16 - expect(result).toEqual({ 17 - logo: `${STATIC_IMAGES_URL}/wallets/wallet.svg`, 18 - name: "Browser Wallet" 19 - }); 20 - }); 21 - 22 - it("returns wallet connect details", () => { 23 - const result = getWalletDetails("walletConnect"); 24 - expect(result).toEqual({ 25 - logo: `${STATIC_IMAGES_URL}/wallets/walletconnect.svg`, 26 - name: "Wallet Connect" 27 - }); 28 - }); 29 - });
-19
apps/web/src/helpers/humanize.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import humanize from "./humanize"; 3 - 4 - describe("humanize", () => { 5 - it("formats large follower counts with commas", () => { 6 - expect(humanize(1234567)).toBe("1,234,567"); 7 - expect(humanize(987654321)).toBe("987,654,321"); 8 - }); 9 - 10 - it("handles zero followers and negative reputation scores", () => { 11 - expect(humanize(0)).toBe("0"); 12 - expect(humanize(-9876)).toBe("-9,876"); 13 - }); 14 - 15 - it("returns empty string for invalid social media metrics", () => { 16 - expect(humanize(Number.NaN)).toBe(""); 17 - expect(humanize(Number.POSITIVE_INFINITY)).toBe(""); 18 - }); 19 - });
-35
apps/web/src/helpers/injectReferrerToUrl.test.ts
··· 1 - import { HEY_TREASURY } from "@hey/data/constants"; 2 - import { describe, expect, it } from "vitest"; 3 - import injectReferrerToUrl from "./injectReferrerToUrl"; 4 - 5 - describe("injectReferrerToUrl", () => { 6 - it("returns the input when url is invalid", () => { 7 - const input = "not a url"; 8 - expect(injectReferrerToUrl(input)).toBe(input); 9 - }); 10 - 11 - it("adds referrer param for highlight links", () => { 12 - const url = injectReferrerToUrl("https://highlight.xyz/mint/1"); 13 - const parsed = new URL(url); 14 - expect(parsed.hostname).toBe("highlight.xyz"); 15 - expect(parsed.searchParams.get("referrer")).toBe(HEY_TREASURY); 16 - }); 17 - 18 - it("supports subdomains", () => { 19 - const url = injectReferrerToUrl("https://app.highlight.xyz/auction"); 20 - const parsed = new URL(url); 21 - expect(parsed.searchParams.get("referrer")).toBe(HEY_TREASURY); 22 - }); 23 - 24 - it("appends the param when query exists", () => { 25 - const url = injectReferrerToUrl("https://zora.co/collect?foo=bar"); 26 - const parsed = new URL(url); 27 - expect(parsed.searchParams.get("foo")).toBe("bar"); 28 - expect(parsed.searchParams.get("referrer")).toBe(HEY_TREASURY); 29 - }); 30 - 31 - it("returns the same url for other domains", () => { 32 - const original = "https://example.com/post?id=1"; 33 - expect(injectReferrerToUrl(original)).toBe(original); 34 - }); 35 - });
-23
apps/web/src/helpers/nFormatter.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import nFormatter from "./nFormatter"; 3 - 4 - // Tests for nFormatter used in social media contexts 5 - 6 - describe("nFormatter", () => { 7 - it("formats small follower counts", () => { 8 - expect(nFormatter(847)).toBe("847"); 9 - }); 10 - 11 - it("abbreviates thousands with a k", () => { 12 - expect(nFormatter(1520)).toBe("1.5k"); 13 - }); 14 - 15 - it("abbreviates millions with an M", () => { 16 - expect(nFormatter(2300000)).toBe("2.3M"); 17 - }); 18 - 19 - it("returns empty string for invalid counts", () => { 20 - expect(nFormatter(Number.POSITIVE_INFINITY)).toBe(""); 21 - expect(nFormatter(Number.NaN)).toBe(""); 22 - }); 23 - });
-15
apps/web/src/helpers/prosekit/extension.test.ts
··· 1 - import { createEditor } from "prosekit/core"; 2 - import { describe, expect, it } from "vitest"; 3 - import { defineEditorExtension } from "./extension"; 4 - 5 - describe("defineEditorExtension", () => { 6 - it("registers mention node and link mark", () => { 7 - const extension = defineEditorExtension(); 8 - const mount = document.createElement("div"); 9 - const editor = createEditor({ extension }); 10 - editor.mount(mount); 11 - const { schema } = editor.view.state; 12 - expect(schema.nodes.mention).toBeDefined(); 13 - expect(schema.marks.link).toBeDefined(); 14 - }); 15 - });
-22
apps/web/src/helpers/prosekit/markdown.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import { htmlFromMarkdown, markdownFromHTML } from "./markdown"; 3 - 4 - const joinHtml = "<p>Hello</p><p>World</p>"; 5 - 6 - describe("markdown and html conversion", () => { 7 - it("converts html to markdown without escaping underscores", () => { 8 - const result = markdownFromHTML("<p>hey_world</p>"); 9 - expect(result).toBe("hey_world\n"); 10 - }); 11 - 12 - it("joins consecutive paragraphs when converting to markdown", () => { 13 - const result = markdownFromHTML(joinHtml); 14 - expect(result).toBe("Hello\nWorld\n"); 15 - }); 16 - 17 - it("round trips markdown to html", () => { 18 - const markdown = "Hello\nWorld"; 19 - const html = htmlFromMarkdown(markdown); 20 - expect(html).toBe("<p>Hello\nWorld</p>\n"); 21 - }); 22 - });
-54
apps/web/src/helpers/rules.test.ts
··· 1 - import type { AccountFollowRules, GroupRules } from "@hey/indexer"; 2 - import { describe, expect, it } from "vitest"; 3 - import { getSimplePaymentDetails } from "./rules"; 4 - 5 - const paymentConfig = () => [ 6 - { __typename: "BigDecimalKeyValue", bigDecimal: "2", key: "amount" } as any, 7 - { 8 - __typename: "AddressKeyValue", 9 - address: "0xToken", 10 - key: "assetContract" 11 - } as any, 12 - { __typename: "StringKeyValue", key: "assetSymbol", string: "SOC" } as any 13 - ]; 14 - 15 - describe("getSimplePaymentDetails", () => { 16 - it("extracts membership fee from group rules", () => { 17 - const rules = { 18 - anyOf: [], 19 - required: [{ config: paymentConfig(), type: "SIMPLE_PAYMENT" }] 20 - } as unknown as GroupRules; 21 - 22 - const result = getSimplePaymentDetails(rules); 23 - expect(result).toEqual({ 24 - amount: 2, 25 - assetAddress: "0xToken", 26 - assetSymbol: "SOC" 27 - }); 28 - }); 29 - 30 - it("reads follow fee from optional rules when required missing", () => { 31 - const rules = { 32 - anyOf: [{ config: paymentConfig(), type: "SIMPLE_PAYMENT" }], 33 - required: [{ config: [], type: "TOKEN_GATED" }] 34 - } as unknown as AccountFollowRules; 35 - 36 - const result = getSimplePaymentDetails(rules); 37 - expect(result).toEqual({ 38 - amount: null, 39 - assetAddress: null, 40 - assetSymbol: null 41 - }); 42 - }); 43 - 44 - it("returns null values when simple payment is absent", () => { 45 - const rules = { anyOf: [], required: [] } as unknown as GroupRules; 46 - 47 - const result = getSimplePaymentDetails(rules); 48 - expect(result).toEqual({ 49 - amount: null, 50 - assetAddress: null, 51 - assetSymbol: null 52 - }); 53 - }); 54 - });
-8
apps/web/src/helpers/splitNumber.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import splitNumber from "./splitNumber"; 3 - 4 - describe("splitNumber", () => { 5 - it("evenly distributes engagement metrics across multiple feeds", () => { 6 - expect(splitNumber(5, 2)).toEqual([3, 2]); 7 - }); 8 - });
-24
apps/web/src/helpers/trimify.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import trimify from "./trimify"; 3 - 4 - describe("trimify", () => { 5 - it("trims spaces around a post", () => { 6 - const post = " Hello world "; 7 - expect(trimify(post)).toBe("Hello world"); 8 - }); 9 - 10 - it("collapses multiple blank lines within a post", () => { 11 - const post = "First line\n\n \nSecond line"; 12 - expect(trimify(post)).toBe("First line\n\nSecond line"); 13 - }); 14 - 15 - it("leaves already clean text unchanged", () => { 16 - const post = "Just a single line"; 17 - expect(trimify(post)).toBe("Just a single line"); 18 - }); 19 - 20 - it("handles trailing newlines from comments", () => { 21 - const comment = "Nice post!\n\n"; 22 - expect(trimify(comment)).toBe("Nice post!"); 23 - }); 24 - });
-19
apps/web/src/helpers/truncateByWords.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import truncateByWords from "./truncateByWords"; 3 - 4 - describe("truncateByWords", () => { 5 - it("truncates lengthy captions to the specified number of words", () => { 6 - const caption = "This is a sample caption for social media post"; 7 - expect(truncateByWords(caption, 5)).toBe("This is a sample caption…"); 8 - }); 9 - 10 - it("returns the full comment when it is within the limit", () => { 11 - const comment = "Love this app"; 12 - expect(truncateByWords(comment, 5)).toBe(comment); 13 - }); 14 - 15 - it("handles extra whitespace gracefully", () => { 16 - const bio = " Welcome to the new social network "; 17 - expect(truncateByWords(bio, 4)).toBe("Welcome to the new…"); 18 - }); 19 - });
-27
apps/web/src/helpers/truncateUrl.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import truncateUrl from "./truncateUrl"; 3 - 4 - describe("truncateUrl", () => { 5 - it("formats external urls", () => { 6 - const url = "https://www.social.com/post/123?utm=abc#top"; 7 - const result = truncateUrl(url, 50); 8 - expect(result).toBe("social.com/post/123?utm=abc#top"); 9 - }); 10 - 11 - it("does not truncate hey.xyz links", () => { 12 - const url = "https://blog.hey.xyz/discover/new"; 13 - const result = truncateUrl(url, 10); 14 - expect(result).toBe("blog.hey.xyz/discover/new"); 15 - }); 16 - 17 - it("truncates long urls", () => { 18 - const url = "https://example.com/some/very/long/path"; 19 - const result = truncateUrl(url, 15); 20 - expect(result).toBe("example.com/so…"); 21 - }); 22 - 23 - it("handles invalid urls", () => { 24 - const result = truncateUrl("invalid url", 10); 25 - expect(result).toBe("invalid u…"); 26 - }); 27 - });
-45
apps/web/src/helpers/uploadMetadata.test.ts
··· 1 - import { beforeEach, describe, expect, it, vi } from "vitest"; 2 - 3 - vi.mock("./storageClient", () => ({ 4 - storageClient: { uploadAsJson: vi.fn() } 5 - })); 6 - vi.mock("@lens-chain/storage-client", () => ({ 7 - immutable: vi.fn(() => "acl") 8 - })); 9 - 10 - import { CHAIN } from "@hey/data/constants"; 11 - import { ERRORS } from "@hey/data/errors"; 12 - import { type FileUploadResponse, immutable } from "@lens-chain/storage-client"; 13 - import { storageClient } from "./storageClient"; 14 - import uploadMetadata from "./uploadMetadata"; 15 - 16 - beforeEach(() => { 17 - vi.clearAllMocks(); 18 - }); 19 - 20 - describe("uploadMetadata", () => { 21 - it("uploads metadata and returns the uri", async () => { 22 - vi.mocked(storageClient.uploadAsJson).mockResolvedValueOnce({ 23 - uri: "ipfs://meta" 24 - } as unknown as FileUploadResponse); 25 - 26 - const result = await uploadMetadata({ foo: "bar" }); 27 - 28 - expect(immutable).toHaveBeenCalledWith(CHAIN.id); 29 - expect(storageClient.uploadAsJson).toHaveBeenCalledWith( 30 - { foo: "bar" }, 31 - { acl: "acl" } 32 - ); 33 - expect(result).toBe("ipfs://meta"); 34 - }); 35 - 36 - it("throws SomethingWentWrong error when upload fails", async () => { 37 - vi.mocked(storageClient.uploadAsJson).mockRejectedValueOnce( 38 - new Error("fail") 39 - ); 40 - 41 - await expect(uploadMetadata(null)).rejects.toThrow( 42 - ERRORS.SomethingWentWrong 43 - ); 44 - }); 45 - });
-113
apps/web/src/helpers/uploadToIPFS.test.ts
··· 1 - import { beforeEach, describe, expect, it, vi } from "vitest"; 2 - 3 - var headObject: ReturnType<typeof vi.fn>; 4 - var done: ReturnType<typeof vi.fn>; 5 - var sts: ReturnType<typeof vi.fn>; 6 - var uploadFile: ReturnType<typeof vi.fn>; 7 - var uuid: ReturnType<typeof vi.fn>; 8 - var immutableMock: ReturnType<typeof vi.fn>; 9 - 10 - vi.mock("@aws-sdk/client-s3", () => { 11 - headObject = vi.fn(); 12 - return { S3: vi.fn(() => ({ headObject })) }; 13 - }); 14 - vi.mock("@aws-sdk/lib-storage", () => { 15 - done = vi.fn(); 16 - return { Upload: vi.fn(() => ({ done })) }; 17 - }); 18 - vi.mock("./fetcher", () => { 19 - sts = vi.fn(); 20 - return { hono: { metadata: { sts } } }; 21 - }); 22 - vi.mock("./storageClient", () => { 23 - uploadFile = vi.fn(); 24 - return { storageClient: { uploadFile } }; 25 - }); 26 - vi.mock("@hey/helpers/generateUUID", () => { 27 - uuid = vi.fn(); 28 - return { default: uuid }; 29 - }); 30 - vi.mock("@lens-chain/storage-client", () => { 31 - immutableMock = vi.fn(); 32 - return { immutable: immutableMock }; 33 - }); 34 - 35 - import uploadToIPFS, * as uploadModule from "./uploadToIPFS"; 36 - 37 - const { uploadFileToIPFS } = uploadModule; 38 - 39 - beforeEach(() => { 40 - vi.clearAllMocks(); 41 - }); 42 - 43 - describe("uploadToIPFS", () => { 44 - it("uploads small files using the storage client", async () => { 45 - uploadFile.mockResolvedValueOnce({ uri: "ipfs://small" }); 46 - immutableMock.mockReturnValue("acl"); 47 - 48 - const file = new File(["x"], "img.png", { type: "image/png" }); 49 - 50 - const [result] = await uploadToIPFS([file]); 51 - 52 - expect(uploadFile).toHaveBeenCalledWith(file, { acl: "acl" }); 53 - expect(result).toEqual({ mimeType: "image/png", uri: "ipfs://small" }); 54 - expect(done).not.toHaveBeenCalled(); 55 - }); 56 - 57 - it("uploads large files using S3", async () => { 58 - sts.mockResolvedValueOnce({ 59 - accessKeyId: "a", 60 - secretAccessKey: "b", 61 - sessionToken: "c" 62 - }); 63 - headObject.mockResolvedValueOnce({ Metadata: { "ipfs-hash": "cid123" } }); 64 - uuid.mockReturnValueOnce("uuid"); 65 - immutableMock.mockReturnValue("acl"); 66 - 67 - const file = new File(["y"], "video.mp4", { type: "video/mp4" }); 68 - Object.defineProperty(file, "size", { value: 10 * 1024 * 1024 }); 69 - 70 - const [result] = await uploadToIPFS([file]); 71 - 72 - expect(sts).toHaveBeenCalled(); 73 - expect(done).toHaveBeenCalled(); 74 - expect(headObject).toHaveBeenCalled(); 75 - expect(result).toEqual({ mimeType: "video/mp4", uri: "ipfs://cid123" }); 76 - }); 77 - 78 - it("returns empty array on failure", async () => { 79 - uploadFile.mockRejectedValueOnce(new Error("err")); 80 - immutableMock.mockReturnValue("acl"); 81 - 82 - const file = new File(["z"], "img.jpg", { type: "image/jpeg" }); 83 - 84 - const result = await uploadToIPFS([file]); 85 - 86 - expect(result).toEqual([]); 87 - }); 88 - }); 89 - 90 - describe("uploadFileToIPFS", () => { 91 - it("returns file metadata from uploadToIPFS", async () => { 92 - uploadFile.mockResolvedValueOnce({ uri: "ipfs://single" }); 93 - immutableMock.mockReturnValue("acl"); 94 - 95 - const file = new File(["a"], "photo.png", { type: "image/png" }); 96 - 97 - const result = await uploadFileToIPFS(file); 98 - 99 - expect(result).toEqual({ mimeType: "image/png", uri: "ipfs://single" }); 100 - }); 101 - 102 - it("returns fallback when uploadToIPFS throws", async () => { 103 - const spy = vi 104 - .spyOn(uploadModule, "default") 105 - .mockRejectedValueOnce(new Error("err")); 106 - const file = new File(["a"], "photo.png", { type: "image/png" }); 107 - 108 - const result = await uploadFileToIPFS(file); 109 - 110 - expect(result).toEqual({ mimeType: "image/png", uri: "" }); 111 - spy.mockRestore(); 112 - }); 113 - });
+1 -1
apps/web/tsconfig.json
··· 5 5 "paths": { "@/*": ["./src/*"] } 6 6 }, 7 7 "extends": "@hey/config/react.tsconfig.json", 8 - "include": ["**/*.ts", "**/*.tsx", "vite.config.mjs", "vitest.config.mjs"] 8 + "include": ["**/*.ts", "**/*.tsx", "vite.config.mjs"] 9 9 }
-16
apps/web/vitest.config.mjs
··· 1 - import path from "node:path"; 2 - import react from "@vitejs/plugin-react"; 3 - import { defineConfig } from "vitest/config"; 4 - 5 - export default defineConfig({ 6 - plugins: [react()], 7 - resolve: { 8 - alias: { 9 - "@": path.resolve(__dirname, "./src") 10 - } 11 - }, 12 - test: { 13 - environment: "jsdom", 14 - setupFiles: "./vitest.setup.ts" 15 - } 16 - });
-1
apps/web/vitest.setup.ts
··· 1 - import "@testing-library/jest-dom/vitest";
-1
package.json
··· 18 18 "prepare": "husky install", 19 19 "start": "pnpm --recursive --parallel run start", 20 20 "sync": "node ./script/sync-public.mjs", 21 - "test": "pnpm --recursive --parallel run test", 22 21 "typecheck": "pnpm --recursive --parallel run typecheck" 23 22 }, 24 23 "devDependencies": {
-1
packages/helpers/README.md
··· 6 6 7 7 - `pnpm dev` – start development mode across workspaces. 8 8 - `pnpm build` – compile the monorepo packages. 9 - - `pnpm test` – run vitest tests.
-19
packages/helpers/escapeHtml.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import escapeHtml from "./escapeHtml"; 3 - 4 - describe("escapeHtml", () => { 5 - it("escapes HTML characters in user-generated social media content", () => { 6 - const result = escapeHtml(`<div>&'"</div>`); 7 - expect(result).toBe("&lt;div&gt;&amp;&#39;&quot;&lt;/div&gt;"); 8 - }); 9 - 10 - it("returns empty string when post content is missing", () => { 11 - expect(escapeHtml()).toBe(""); 12 - expect(escapeHtml(null)).toBe(""); 13 - }); 14 - 15 - it("leaves normal social media posts unchanged", () => { 16 - const text = "GM frens! Hope everyone is having a great day in Web3 🚀"; 17 - expect(escapeHtml(text)).toBe(text); 18 - }); 19 - });
-26
packages/helpers/formatAddress.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import formatAddress from "./formatAddress"; 3 - 4 - const sampleAddress = "0x1234567890ABCDEF1234567890abcdef12345678"; 5 - 6 - describe("formatAddress", () => { 7 - it("formats wallet address for user profile display", () => { 8 - const result = formatAddress(sampleAddress); 9 - expect(result).toBe("0x12…5678"); 10 - }); 11 - 12 - it("formats ENS address with custom length for NFT creator attribution", () => { 13 - const result = formatAddress(sampleAddress, 6); 14 - expect(result).toBe("0x1234…345678"); 15 - }); 16 - 17 - it("returns empty string when user has no connected wallet", () => { 18 - const result = formatAddress(null); 19 - expect(result).toBe(""); 20 - }); 21 - 22 - it("returns lowercase string for invalid wallet addresses in posts", () => { 23 - const result = formatAddress("NotAnAddress"); 24 - expect(result).toBe("notanaddress"); 25 - }); 26 - });
-37
packages/helpers/generateUUID.test.ts
··· 1 - import { beforeEach, describe, expect, it } from "vitest"; 2 - import generateUUID from "./generateUUID"; 3 - 4 - let callCount = 0; 5 - 6 - const asyncGenerate = async () => { 7 - callCount += 1; 8 - return generateUUID(); 9 - }; 10 - 11 - describe("generateUUID", () => { 12 - beforeEach(() => { 13 - callCount = 0; 14 - }); 15 - it("returns a valid UUID for post identifiers", () => { 16 - const id = generateUUID(); 17 - const uuidRegex = 18 - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; 19 - expect(id).toMatch(uuidRegex); 20 - }); 21 - 22 - it("generates unique identifiers for different social media posts", () => { 23 - const id1 = generateUUID(); 24 - const id2 = generateUUID(); 25 - expect(id1).not.toBe(id2); 26 - }); 27 - 28 - it("generates unique IDs in concurrent async calls", async () => { 29 - const ids = await Promise.all([ 30 - asyncGenerate(), 31 - asyncGenerate(), 32 - asyncGenerate() 33 - ]); 34 - expect(new Set(ids).size).toBe(ids.length); 35 - expect(callCount).toBe(3); 36 - }); 37 - });
-90
packages/helpers/getAccount.test.ts
··· 1 - import { LENS_NAMESPACE, NULL_ADDRESS } from "@hey/data/constants"; 2 - import type { AccountFragment } from "@hey/indexer"; 3 - import { describe, expect, it } from "vitest"; 4 - import getAccount from "./getAccount"; 5 - 6 - const aliceAddress = "0x1234567890abcdef1234567890abcdef12345678"; 7 - const bobAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; 8 - const charlieAddress = "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"; 9 - 10 - describe("getAccount", () => { 11 - it("returns default values when account is undefined", () => { 12 - const result = getAccount(undefined); 13 - expect(result).toEqual({ 14 - link: "", 15 - name: "...", 16 - username: "...", 17 - usernameWithPrefix: "..." 18 - }); 19 - }); 20 - 21 - it("returns deleted account info", () => { 22 - const account = { 23 - owner: NULL_ADDRESS 24 - } as unknown as AccountFragment; 25 - 26 - const result = getAccount(account); 27 - expect(result).toEqual({ 28 - link: "", 29 - name: "Deleted Account", 30 - username: "deleted", 31 - usernameWithPrefix: "@deleted" 32 - }); 33 - }); 34 - 35 - it("formats lens usernames and sanitizes name", () => { 36 - const account = { 37 - address: aliceAddress, 38 - metadata: { name: "Jane\u2714 Doe" }, 39 - owner: "0x01", 40 - username: { 41 - localName: "janedoe", 42 - value: `${LENS_NAMESPACE}janedoe` 43 - } 44 - } as unknown as AccountFragment; 45 - 46 - const result = getAccount(account); 47 - expect(result).toEqual({ 48 - link: "/u/janedoe", 49 - name: "Jane Doe", 50 - username: "janedoe", 51 - usernameWithPrefix: "@janedoe" 52 - }); 53 - }); 54 - 55 - it("builds link using address when username is outside lens", () => { 56 - const account = { 57 - address: bobAddress, 58 - metadata: { name: "Alice" }, 59 - owner: "0x01", 60 - username: { 61 - localName: "alice", 62 - value: "alice" 63 - } 64 - } as unknown as AccountFragment; 65 - 66 - const result = getAccount(account); 67 - expect(result).toEqual({ 68 - link: `/account/${bobAddress}`, 69 - name: "Alice", 70 - username: "alice", 71 - usernameWithPrefix: "@alice" 72 - }); 73 - }); 74 - 75 - it("uses address when username is missing", () => { 76 - const account = { 77 - address: charlieAddress, 78 - metadata: { name: "Bob" }, 79 - owner: "0x01" 80 - } as unknown as AccountFragment; 81 - 82 - const result = getAccount(account); 83 - expect(result).toEqual({ 84 - link: `/account/${charlieAddress}`, 85 - name: "Bob", 86 - username: "0xab…abcd", 87 - usernameWithPrefix: "#0xab…abcd" 88 - }); 89 - }); 90 - });
-49
packages/helpers/getAttachmentsData.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import getAttachmentsData from "./getAttachmentsData"; 3 - 4 - const IPFS_GATEWAY = "https://gw.ipfs-lens.dev/ipfs"; 5 - const STORAGE_NODE_URL = "https://api.grove.storage"; 6 - 7 - describe("getAttachmentsData", () => { 8 - it("returns empty array when attachments are undefined", () => { 9 - const result = getAttachmentsData(undefined); 10 - expect(result).toEqual([]); 11 - }); 12 - 13 - it("maps attachment fragments to sanitized data", () => { 14 - const attachments = [ 15 - { __typename: "MediaImage", item: "ipfs://photoCid" }, 16 - { 17 - __typename: "MediaVideo", 18 - cover: "ar://coverCid", 19 - item: "ipfs://clipCid" 20 - }, 21 - { 22 - __typename: "MediaAudio", 23 - artist: "DJ Alice", 24 - cover: "https://ipfs.io/ipfs/coverCid2", 25 - item: "lens://trackCid" 26 - } 27 - ]; 28 - 29 - const result = getAttachmentsData(attachments as any); 30 - 31 - expect(result).toEqual([ 32 - { 33 - type: "Image", 34 - uri: `${IPFS_GATEWAY}/photoCid` 35 - }, 36 - { 37 - coverUri: "https://gateway.arweave.net/coverCid", 38 - type: "Video", 39 - uri: `${IPFS_GATEWAY}/clipCid` 40 - }, 41 - { 42 - artist: "DJ Alice", 43 - coverUri: `${IPFS_GATEWAY}/coverCid2`, 44 - type: "Audio", 45 - uri: `${STORAGE_NODE_URL}/trackCid` 46 - } 47 - ]); 48 - }); 49 - });
-47
packages/helpers/getAvatar.test.ts
··· 1 - import { 2 - DEFAULT_AVATAR, 3 - IPFS_GATEWAY, 4 - LENS_MEDIA_SNAPSHOT_URL, 5 - TRANSFORMS 6 - } from "@hey/data/constants"; 7 - import { describe, expect, it } from "vitest"; 8 - import getAvatar from "./getAvatar"; 9 - 10 - interface ProfileMock { 11 - metadata?: { 12 - picture?: string | null; 13 - icon?: string | null; 14 - } | null; 15 - } 16 - 17 - describe("getAvatar", () => { 18 - it("returns default avatar for null entity", () => { 19 - expect(getAvatar(null)).toBe(DEFAULT_AVATAR); 20 - }); 21 - 22 - it("returns default avatar for invalid picture", () => { 23 - const entity: ProfileMock = { metadata: { picture: "" } }; 24 - expect(getAvatar(entity)).toBe(DEFAULT_AVATAR); 25 - }); 26 - 27 - it("uses icon when picture is missing", () => { 28 - const url = `${LENS_MEDIA_SNAPSHOT_URL}/icon.png`; 29 - const entity: ProfileMock = { metadata: { icon: url, picture: null } }; 30 - const expected = `${LENS_MEDIA_SNAPSHOT_URL}/${TRANSFORMS.AVATAR_SMALL}/icon.png`; 31 - expect(getAvatar(entity)).toBe(expected); 32 - }); 33 - 34 - it("sanitizes ipfs CIDs", () => { 35 - const cid = `Qm${"a".repeat(44)}`; 36 - const entity: ProfileMock = { metadata: { picture: cid } }; 37 - const expected = `${IPFS_GATEWAY}/${cid}`; 38 - expect(getAvatar(entity)).toBe(expected); 39 - }); 40 - 41 - it("applies imagekit transform for snapshot urls", () => { 42 - const url = `${LENS_MEDIA_SNAPSHOT_URL}/path/img.jpg`; 43 - const entity: ProfileMock = { metadata: { picture: url } }; 44 - const expected = `${LENS_MEDIA_SNAPSHOT_URL}/${TRANSFORMS.AVATAR_BIG}/img.jpg`; 45 - expect(getAvatar(entity, TRANSFORMS.AVATAR_BIG)).toBe(expected); 46 - }); 47 - });
-71
packages/helpers/getPostData.test.ts
··· 1 - import type { PostMetadataFragment } from "@hey/indexer"; 2 - import { describe, expect, it } from "vitest"; 3 - import getPostData from "./getPostData"; 4 - 5 - const ipfsGateway = "https://gw.ipfs-lens.dev/ipfs"; 6 - 7 - describe("getPostData", () => { 8 - it("returns content for TextOnlyMetadata", () => { 9 - const metadata = { 10 - __typename: "TextOnlyMetadata", 11 - content: "Hello world" 12 - } as unknown as PostMetadataFragment; 13 - 14 - const result = getPostData(metadata); 15 - expect(result).toEqual({ content: "Hello world" }); 16 - }); 17 - 18 - it("parses ImageMetadata and sanitizes asset uri", () => { 19 - const metadata = { 20 - __typename: "ImageMetadata", 21 - attachments: undefined, 22 - content: "A cool photo", 23 - image: { item: "ipfs://ipfs/QmImage" } 24 - } as unknown as PostMetadataFragment; 25 - 26 - const result = getPostData(metadata); 27 - expect(result).toEqual({ 28 - asset: { type: "Image", uri: `${ipfsGateway}/QmImage` }, 29 - attachments: [], 30 - content: "A cool photo" 31 - }); 32 - }); 33 - 34 - it("uses attachment fallback data for AudioMetadata", () => { 35 - const metadata = { 36 - __typename: "AudioMetadata", 37 - attachments: [ 38 - { 39 - __typename: "MediaAudio", 40 - artist: "DJ Alice", 41 - cover: "ipfs://cover1", 42 - item: "ipfs://audio1" 43 - } 44 - ], 45 - audio: { artist: undefined, cover: "", item: "" }, 46 - content: "Listen now", 47 - title: "My Song" 48 - } as unknown as PostMetadataFragment; 49 - 50 - const result = getPostData(metadata); 51 - expect(result).toEqual({ 52 - asset: { 53 - artist: "DJ Alice", 54 - cover: `${ipfsGateway}/cover1`, 55 - title: "My Song", 56 - type: "Audio", 57 - uri: `${ipfsGateway}/audio1` 58 - }, 59 - content: "Listen now" 60 - }); 61 - }); 62 - 63 - it("returns null for unknown metadata", () => { 64 - const metadata = { 65 - __typename: "UnknownPostMetadata" 66 - } as unknown as PostMetadataFragment; 67 - 68 - const result = getPostData(metadata); 69 - expect(result).toBeNull(); 70 - }); 71 - });
-82
packages/helpers/getTransactionData.test.ts
··· 1 - import type { 2 - Eip712TransactionRequest, 3 - Eip1559TransactionRequest 4 - } from "@hey/indexer"; 5 - import { describe, expect, it } from "vitest"; 6 - import getTransactionData from "./getTransactionData"; 7 - 8 - const base1559 = { 9 - data: "0x00", 10 - gasLimit: 21000, 11 - maxFeePerGas: 100n, 12 - maxPriorityFeePerGas: 2n, 13 - nonce: 1, 14 - to: "0xabc" as any, 15 - value: 0n 16 - } as any as Eip1559TransactionRequest; 17 - 18 - const base712 = { 19 - ...base1559, 20 - customData: { 21 - paymasterParams: { 22 - paymaster: "0xdef", 23 - paymasterInput: "0x123" 24 - } 25 - } 26 - } as any as Eip712TransactionRequest; 27 - 28 - describe("getTransactionData", () => { 29 - it("handles Eip1559TransactionRequest", () => { 30 - const result = getTransactionData(base1559); 31 - expect(result).toEqual({ 32 - data: "0x00", 33 - gas: 21000n, 34 - maxFeePerGas: 100n, 35 - maxPriorityFeePerGas: 2n, 36 - nonce: 1, 37 - to: "0xabc", 38 - value: 0n 39 - }); 40 - }); 41 - 42 - it("excludes paymaster fields when not sponsored", () => { 43 - const result = getTransactionData(base712); 44 - expect(result).toEqual({ 45 - data: "0x00", 46 - gas: 21000n, 47 - maxFeePerGas: 100n, 48 - maxPriorityFeePerGas: 2n, 49 - nonce: 1, 50 - to: "0xabc", 51 - value: 0n 52 - }); 53 - }); 54 - 55 - it("includes paymaster fields when sponsored", () => { 56 - const result = getTransactionData(base712, { sponsored: true }); 57 - expect(result).toEqual({ 58 - data: "0x00", 59 - gas: 21000n, 60 - maxFeePerGas: 100n, 61 - maxPriorityFeePerGas: 2n, 62 - nonce: 1, 63 - paymaster: "0xdef", 64 - paymasterInput: "0x123", 65 - to: "0xabc", 66 - value: 0n 67 - }); 68 - }); 69 - 70 - it("ignores sponsored flag for Eip1559TransactionRequest", () => { 71 - const result = getTransactionData(base1559, { sponsored: true }); 72 - expect(result).toEqual({ 73 - data: "0x00", 74 - gas: 21000n, 75 - maxFeePerGas: 100n, 76 - maxPriorityFeePerGas: 2n, 77 - nonce: 1, 78 - to: "0xabc", 79 - value: 0n 80 - }); 81 - }); 82 - });
-22
packages/helpers/imageKit.test.ts
··· 1 - import { LENS_MEDIA_SNAPSHOT_URL } from "@hey/data/constants"; 2 - import { describe, expect, it } from "vitest"; 3 - import imageKit from "./imageKit"; 4 - 5 - describe("imageKit", () => { 6 - it("returns an empty string for empty url", () => { 7 - const result = imageKit(""); 8 - expect(result).toBe(""); 9 - }); 10 - 11 - it("returns the original url when not a lens snapshot", () => { 12 - const url = "https://example.com/photo.jpg"; 13 - const result = imageKit(url, "tr:w-100"); 14 - expect(result).toBe(url); 15 - }); 16 - 17 - it("applies transform when url is a lens snapshot", () => { 18 - const original = `${LENS_MEDIA_SNAPSHOT_URL}/photo.jpg`; 19 - const result = imageKit(original, "tr:w-200"); 20 - expect(result).toBe(`${LENS_MEDIA_SNAPSHOT_URL}/tr:w-200/photo.jpg`); 21 - }); 22 - });
-16
packages/helpers/isAccountDeleted.test.ts
··· 1 - import { NULL_ADDRESS } from "@hey/data/constants"; 2 - import type { AccountFragment } from "@hey/indexer"; 3 - import { describe, expect, it } from "vitest"; 4 - import isAccountDeleted from "./isAccountDeleted"; 5 - 6 - describe("isAccountDeleted", () => { 7 - it("returns true when the account owner is the null address", () => { 8 - const account = { owner: NULL_ADDRESS } as unknown as AccountFragment; 9 - expect(isAccountDeleted(account)).toBe(true); 10 - }); 11 - 12 - it("returns false when the account owner is not the null address", () => { 13 - const account = { owner: "0x123" } as unknown as AccountFragment; 14 - expect(isAccountDeleted(account)).toBe(false); 15 - }); 16 - });
-27
packages/helpers/normalizeDescription.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import normalizeDescription from "./normalizeDescription"; 3 - 4 - describe("normalizeDescription", () => { 5 - it("uses fallback when text is too short", () => { 6 - const result = normalizeDescription( 7 - "short", 8 - "This fallback description is definitely longer than twenty five characters." 9 - ); 10 - expect(result).toBe( 11 - "This fallback description is definitely longer than twenty five characters.".slice( 12 - 0, 13 - 160 14 - ) 15 - ); 16 - }); 17 - 18 - it("truncates long text", () => { 19 - const longText = "a".repeat(200); 20 - expect(normalizeDescription(longText, "fallback").length).toBe(160); 21 - }); 22 - 23 - it("returns trimmed text when within range", () => { 24 - const text = "This is a valid description for OG meta tags."; 25 - expect(normalizeDescription(text, "fallback")).toBe(text); 26 - }); 27 - });
+1 -3
packages/helpers/package.json
··· 4 4 "private": true, 5 5 "license": "AGPL-3.0", 6 6 "scripts": { 7 - "test": "vitest run", 8 7 "typecheck": "tsc --pretty" 9 8 }, 10 9 "dependencies": { ··· 16 15 "devDependencies": { 17 16 "@hey/config": "workspace:*", 18 17 "@types/node": "^24.2.1", 19 - "typescript": "^5.9.2", 20 - "vitest": "^3.2.4" 18 + "typescript": "^5.9.2" 21 19 } 22 20 }
-38
packages/helpers/parseJwt.test.ts
··· 1 - import { describe, expect, it } from "vitest"; 2 - import parseJwt from "./parseJwt"; 3 - 4 - const createToken = (payload: object) => { 5 - const base64Payload = Buffer.from(JSON.stringify(payload)).toString("base64"); 6 - return `header.${base64Payload}.signature`; 7 - }; 8 - 9 - describe("parseJwt", () => { 10 - it("parses valid jwt payload", () => { 11 - const payload = { act: { sub: "456" }, exp: 10, sid: "abc", sub: "123" }; 12 - const token = createToken(payload); 13 - 14 - const result = parseJwt(token); 15 - 16 - expect(result).toEqual(payload); 17 - }); 18 - 19 - it("supports base64url encoded tokens", () => { 20 - const payload = { act: { sub: "789" }, exp: 20, sid: "def", sub: "456" }; 21 - const base64Url = Buffer.from(JSON.stringify(payload)).toString( 22 - "base64url" 23 - ); 24 - const token = `header.${base64Url}.signature`; 25 - 26 - const result = parseJwt(token); 27 - 28 - expect(result).toEqual(payload); 29 - }); 30 - 31 - it("returns default payload for invalid token", () => { 32 - const invalidToken = "invalid.token"; 33 - 34 - const result = parseJwt(invalidToken); 35 - 36 - expect(result).toEqual({ act: { sub: "" }, exp: 0, sid: "", sub: "" }); 37 - }); 38 - });
-19
packages/helpers/postHelpers.test.ts
··· 1 - import type { AnyPostFragment } from "@hey/indexer"; 2 - import { describe, expect, it } from "vitest"; 3 - import { isRepost } from "./postHelpers"; 4 - 5 - describe("isRepost", () => { 6 - it("returns true when user shares a viral Web3 meme", () => { 7 - const post = { __typename: "Repost" } as unknown as AnyPostFragment; 8 - expect(isRepost(post)).toBe(true); 9 - }); 10 - 11 - it("returns false when user creates original crypto content", () => { 12 - const post = { __typename: "Post" } as unknown as AnyPostFragment; 13 - expect(isRepost(post)).toBe(false); 14 - }); 15 - 16 - it("returns false when post data is unavailable", () => { 17 - expect(isRepost(null)).toBe(false); 18 - }); 19 - });
-36
packages/helpers/sanitizeDStorageUrl.test.ts
··· 1 - import { IPFS_GATEWAY, STORAGE_NODE_URL } from "@hey/data/constants"; 2 - import { describe, expect, it } from "vitest"; 3 - import sanitizeDStorageUrl from "./sanitizeDStorageUrl"; 4 - 5 - const ipfsGateway = `${IPFS_GATEWAY}/`; 6 - 7 - describe("sanitizeDStorageUrl", () => { 8 - it("returns an empty string when url is undefined", () => { 9 - expect(sanitizeDStorageUrl()).toBe(""); 10 - }); 11 - 12 - it("handles bare IPFS hashes", () => { 13 - const hash = `Qm${"a".repeat(44)}`; 14 - expect(sanitizeDStorageUrl(hash)).toBe(`${ipfsGateway}${hash}`); 15 - }); 16 - 17 - it("converts common ipfs url formats", () => { 18 - const formats = ["https://ipfs.io/ipfs/", "ipfs://ipfs/", "ipfs://"]; 19 - const hash = `Qm${"b".repeat(44)}`; 20 - for (const prefix of formats) { 21 - expect(sanitizeDStorageUrl(`${prefix}${hash}`)).toBe( 22 - `${ipfsGateway}${hash}` 23 - ); 24 - } 25 - }); 26 - 27 - it("converts lens:// links", () => { 28 - expect(sanitizeDStorageUrl("lens://abc")).toBe(`${STORAGE_NODE_URL}/abc`); 29 - }); 30 - 31 - it("converts ar:// links", () => { 32 - expect(sanitizeDStorageUrl("ar://xyz")).toBe( 33 - "https://gateway.arweave.net/xyz" 34 - ); 35 - }); 36 - });
-7
packages/helpers/vitest.config.ts
··· 1 - import { defineConfig } from "vitest/config"; 2 - 3 - export default defineConfig({ 4 - test: { 5 - environment: "node" 6 - } 7 - });
-1
packages/indexer/README.md
··· 6 6 7 7 - `pnpm dev` – start development mode across workspaces. 8 8 - `pnpm build` – compile the monorepo packages. 9 - - `pnpm test` – run vitest tests.
-46
packages/indexer/apollo/helpers/cursorBasedPagination.test.ts
··· 1 - import type { FieldMergeFunction } from "@apollo/client/core"; 2 - import { describe, expect, it } from "vitest"; 3 - import type { PaginatedResultInfoFragment } from "../../generated"; 4 - import cursorBasedPagination from "./cursorBasedPagination"; 5 - 6 - const pageInfo: PaginatedResultInfoFragment = { 7 - __typename: "PaginatedResultInfo", 8 - next: null, 9 - prev: null 10 - }; 11 - 12 - interface TestPagination { 13 - items: string[]; 14 - pageInfo: PaginatedResultInfoFragment; 15 - } 16 - 17 - describe("cursorBasedPagination", () => { 18 - it("merges existing and incoming social media posts in feed", () => { 19 - const policy = cursorBasedPagination<TestPagination>([]); 20 - const existing = { items: ["post1"], pageInfo }; 21 - const incoming = { 22 - items: ["post2"], 23 - pageInfo: { ...pageInfo, next: "post3" } 24 - }; 25 - 26 - const merged = ( 27 - policy.merge as FieldMergeFunction<TestPagination, TestPagination> 28 - )(existing, incoming, {} as any); 29 - 30 - expect(merged.items).toEqual(["post1", "post2"]); 31 - expect(merged.pageInfo).toEqual(incoming.pageInfo); 32 - }); 33 - 34 - it("returns social media feed items and pagination info unchanged", () => { 35 - const policy = cursorBasedPagination<TestPagination>([]); 36 - const existing = { 37 - items: ["post1", "post2"], 38 - pageInfo: { ...pageInfo, next: "post4" } 39 - }; 40 - 41 - const read = policy.read?.(existing, {} as any); 42 - 43 - expect(read?.items).toEqual(existing.items); 44 - expect(read?.pageInfo).toEqual(existing.pageInfo); 45 - }); 46 - });
+1 -3
packages/indexer/package.json
··· 6 6 "main": "generated.ts", 7 7 "scripts": { 8 8 "codegen": "graphql-codegen", 9 - "test": "vitest run", 10 9 "typecheck": "tsc --pretty" 11 10 }, 12 11 "dependencies": { ··· 22 21 "@graphql-codegen/typescript-react-apollo": "^4.3.3", 23 22 "@hey/config": "workspace:*", 24 23 "@types/node": "^24.2.1", 25 - "typescript": "^5.9.2", 26 - "vitest": "^3.2.4" 24 + "typescript": "^5.9.2" 27 25 } 28 26 }
-1
packages/types/README.md
··· 6 6 7 7 - `pnpm dev` – start development mode across workspaces. 8 8 - `pnpm build` – compile the monorepo packages. 9 - - `pnpm test` – run the repository test suites.
+1 -508
pnpm-lock.yaml
··· 93 93 typescript: 94 94 specifier: ^5.9.2 95 95 version: 5.9.2 96 - vitest: 97 - specifier: ^3.2.4 98 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 99 96 100 97 apps/mobile: 101 98 dependencies: 102 99 '@apollo/client': 103 100 specifier: ^3.13.9 104 - version: 3.13.9(@types/react@19.1.10)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.11.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 101 + version: 3.13.9(@types/react@19.1.10)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.11.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 105 102 '@expo/vector-icons': 106 103 specifier: ^14.1.0 107 104 version: 14.1.0(expo-font@13.3.2(expo@53.0.20(@babel/core@7.28.3)(@expo/metro-runtime@5.0.4(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(graphql@16.11.0)(react-native-webview@13.13.5(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10))(react@19.1.1))(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10))(react@19.1.1)(utf-8-validate@5.0.10))(react@19.1.1))(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10))(react@19.1.1) ··· 395 392 '@tailwindcss/forms': 396 393 specifier: ^0.5.10 397 394 version: 0.5.10(tailwindcss@4.1.12) 398 - '@testing-library/jest-dom': 399 - specifier: ^6.7.0 400 - version: 6.7.0 401 - '@testing-library/react': 402 - specifier: ^16.3.0 403 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 404 - '@testing-library/user-event': 405 - specifier: ^14.6.1 406 - version: 14.6.1(@testing-library/dom@10.4.1) 407 395 '@types/hast': 408 396 specifier: ^3.0.4 409 397 version: 3.0.4 ··· 431 419 vite-tsconfig-paths: 432 420 specifier: ^5.1.4 433 421 version: 5.1.4(typescript@5.9.2)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) 434 - vitest: 435 - specifier: ^3.2.4 436 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 437 422 438 423 packages/config: {} 439 424 ··· 477 462 typescript: 478 463 specifier: ^5.9.2 479 464 version: 5.9.2 480 - vitest: 481 - specifier: ^3.2.4 482 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 483 465 484 466 packages/indexer: 485 467 dependencies: ··· 517 499 typescript: 518 500 specifier: ^5.9.2 519 501 version: 5.9.2 520 - vitest: 521 - specifier: ^3.2.4 522 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 523 502 524 503 packages/types: 525 504 dependencies: ··· 546 525 peerDependenciesMeta: 547 526 graphql: 548 527 optional: true 549 - 550 - '@adobe/css-tools@4.4.4': 551 - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} 552 528 553 529 '@adraffy/ens-normalize@1.10.1': 554 530 resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} ··· 3579 3555 '@tanstack/virtual-core@3.13.12': 3580 3556 resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} 3581 3557 3582 - '@testing-library/dom@10.4.1': 3583 - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} 3584 - engines: {node: '>=18'} 3585 - 3586 - '@testing-library/jest-dom@6.7.0': 3587 - resolution: {integrity: sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==} 3588 - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} 3589 - 3590 - '@testing-library/react@16.3.0': 3591 - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} 3592 - engines: {node: '>=18'} 3593 - peerDependencies: 3594 - '@testing-library/dom': ^10.0.0 3595 - '@types/react': ^18.0.0 || ^19.0.0 3596 - '@types/react-dom': ^18.0.0 || ^19.0.0 3597 - react: ^18.0.0 || ^19.0.0 3598 - react-dom: ^18.0.0 || ^19.0.0 3599 - peerDependenciesMeta: 3600 - '@types/react': 3601 - optional: true 3602 - '@types/react-dom': 3603 - optional: true 3604 - 3605 - '@testing-library/user-event@14.6.1': 3606 - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} 3607 - engines: {node: '>=12', npm: '>=6'} 3608 - peerDependencies: 3609 - '@testing-library/dom': '>=7.21.4' 3610 - 3611 3558 '@theguild/federation-composition@0.19.1': 3612 3559 resolution: {integrity: sha512-E4kllHSRYh+FsY0VR+fwl0rmWhDV8xUgWawLZTXmy15nCWQwj0BDsoEpdEXjPh7xes+75cRaeJcSbZ4jkBuSdg==} 3613 3560 engines: {node: '>=18'} 3614 3561 peerDependencies: 3615 3562 graphql: ^16.0.0 3616 3563 3617 - '@types/aria-query@5.0.4': 3618 - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} 3619 - 3620 3564 '@types/babel__core@7.20.5': 3621 3565 resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 3622 3566 ··· 3629 3573 '@types/babel__traverse@7.28.0': 3630 3574 resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} 3631 3575 3632 - '@types/chai@5.2.2': 3633 - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} 3634 - 3635 3576 '@types/debug@4.1.12': 3636 3577 resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} 3637 - 3638 - '@types/deep-eql@4.0.2': 3639 - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 3640 3578 3641 3579 '@types/estree-jsx@1.0.5': 3642 3580 resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} ··· 3736 3674 peerDependencies: 3737 3675 vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 3738 3676 3739 - '@vitest/expect@3.2.4': 3740 - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} 3741 - 3742 - '@vitest/mocker@3.2.4': 3743 - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} 3744 - peerDependencies: 3745 - msw: ^2.4.9 3746 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 3747 - peerDependenciesMeta: 3748 - msw: 3749 - optional: true 3750 - vite: 3751 - optional: true 3752 - 3753 - '@vitest/pretty-format@3.2.4': 3754 - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} 3755 - 3756 - '@vitest/runner@3.2.4': 3757 - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} 3758 - 3759 - '@vitest/snapshot@3.2.4': 3760 - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} 3761 - 3762 - '@vitest/spy@3.2.4': 3763 - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} 3764 - 3765 - '@vitest/utils@3.2.4': 3766 - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} 3767 - 3768 3677 '@wagmi/connectors@5.9.3': 3769 3678 resolution: {integrity: sha512-HmSRFB3SFE1jAPs1E28I6/VOyA82i4KzC0OyG1JLEkOkyLlGsakPxtwXVdw/7kv9L4ppADWWktvwOjvhvpRmdQ==} 3770 3679 peerDependencies: ··· 4032 3941 resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} 4033 3942 engines: {node: '>=10'} 4034 3943 4035 - aria-query@5.3.0: 4036 - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} 4037 - 4038 - aria-query@5.3.2: 4039 - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} 4040 - engines: {node: '>= 0.4'} 4041 - 4042 3944 array-timsort@1.0.3: 4043 3945 resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} 4044 3946 ··· 4053 3955 resolution: {integrity: sha512-s6v4HnA+vYSGO4eZX+F+I3gvF74wPk+m6Z1Q3w1Dsg4Pnv/R24vhKAasoMVZGvDpOOfTg1Qz4ptZnEbuy95XsQ==} 4054 3956 engines: {node: '>=14.0.0'} 4055 3957 4056 - assertion-error@2.0.1: 4057 - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 4058 - engines: {node: '>=12'} 4059 - 4060 3958 astral-regex@2.0.0: 4061 3959 resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} 4062 3960 engines: {node: '>=8'} ··· 4246 4144 magicast: 4247 4145 optional: true 4248 4146 4249 - cac@6.7.14: 4250 - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 4251 - engines: {node: '>=8'} 4252 - 4253 4147 call-bind-apply-helpers@1.0.2: 4254 4148 resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 4255 4149 engines: {node: '>= 0.4'} ··· 4302 4196 ccount@2.0.1: 4303 4197 resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} 4304 4198 4305 - chai@5.2.1: 4306 - resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} 4307 - engines: {node: '>=18'} 4308 - 4309 4199 chalk@2.4.2: 4310 4200 resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 4311 4201 engines: {node: '>=4'} ··· 4334 4224 4335 4225 chardet@2.1.0: 4336 4226 resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} 4337 - 4338 - check-error@2.1.1: 4339 - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 4340 - engines: {node: '>= 16'} 4341 4227 4342 4228 chokidar@3.6.0: 4343 4229 resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} ··· 4564 4450 resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} 4565 4451 engines: {node: '>= 6'} 4566 4452 4567 - css.escape@1.5.1: 4568 - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} 4569 - 4570 4453 cssesc@3.0.0: 4571 4454 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 4572 4455 engines: {node: '>=4'} ··· 4654 4537 resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} 4655 4538 engines: {node: '>=0.10'} 4656 4539 4657 - deep-eql@5.0.2: 4658 - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 4659 - engines: {node: '>=6'} 4660 - 4661 4540 deep-extend@0.6.0: 4662 4541 resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} 4663 4542 engines: {node: '>=4.0.0'} ··· 4743 4622 dlv@1.1.3: 4744 4623 resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 4745 4624 4746 - dom-accessibility-api@0.5.16: 4747 - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} 4748 - 4749 - dom-accessibility-api@0.6.3: 4750 - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} 4751 - 4752 4625 dom-serializer@2.0.0: 4753 4626 resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 4754 4627 ··· 4869 4742 resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 4870 4743 engines: {node: '>= 0.4'} 4871 4744 4872 - es-module-lexer@1.7.0: 4873 - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 4874 - 4875 4745 es-object-atoms@1.1.1: 4876 4746 resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 4877 4747 engines: {node: '>= 0.4'} ··· 4915 4785 estree-util-is-identifier-name@3.0.0: 4916 4786 resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} 4917 4787 4918 - estree-walker@3.0.3: 4919 - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 4920 - 4921 4788 etag@1.8.1: 4922 4789 resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 4923 4790 engines: {node: '>= 0.6'} ··· 4959 4826 4960 4827 exec-async@2.2.0: 4961 4828 resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==} 4962 - 4963 - expect-type@1.2.2: 4964 - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} 4965 - engines: {node: '>=12.0.0'} 4966 4829 4967 4830 expo-asset@11.1.7: 4968 4831 resolution: {integrity: sha512-b5P8GpjUh08fRCf6m5XPVAh7ra42cQrHBIMgH2UXP+xsj4Wufl6pLy6jRF5w6U7DranUMbsXm8TOyq4EHy7ADg==} ··· 5783 5646 js-tokens@4.0.0: 5784 5647 resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 5785 5648 5786 - js-tokens@9.0.1: 5787 - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} 5788 - 5789 5649 js-yaml@3.14.1: 5790 5650 resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} 5791 5651 hasBin: true ··· 6080 5940 resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 6081 5941 hasBin: true 6082 5942 6083 - loupe@3.2.0: 6084 - resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} 6085 - 6086 5943 lower-case-first@2.0.2: 6087 5944 resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} 6088 5945 ··· 6094 5951 6095 5952 lru-cache@5.1.1: 6096 5953 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 6097 - 6098 - lz-string@1.5.0: 6099 - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} 6100 - hasBin: true 6101 5954 6102 5955 magic-string@0.30.17: 6103 5956 resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} ··· 6325 6178 resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} 6326 6179 engines: {node: '>=6'} 6327 6180 6328 - min-indent@1.0.1: 6329 - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} 6330 - engines: {node: '>=4'} 6331 - 6332 6181 mini-svg-data-uri@1.4.4: 6333 6182 resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} 6334 6183 hasBin: true ··· 6724 6573 pathe@2.0.3: 6725 6574 resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 6726 6575 6727 - pathval@2.0.1: 6728 - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} 6729 - engines: {node: '>= 14.16'} 6730 - 6731 6576 perfect-debounce@1.0.0: 6732 6577 resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} 6733 6578 ··· 6937 6782 resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} 6938 6783 engines: {node: '>=6'} 6939 6784 6940 - pretty-format@27.5.1: 6941 - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} 6942 - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} 6943 - 6944 6785 pretty-format@29.7.0: 6945 6786 resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} 6946 6787 engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} ··· 7224 7065 react-is@16.13.1: 7225 7066 resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 7226 7067 7227 - react-is@17.0.2: 7228 - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} 7229 - 7230 7068 react-is@18.3.1: 7231 7069 resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} 7232 7070 ··· 7400 7238 real-require@0.1.0: 7401 7239 resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} 7402 7240 engines: {node: '>= 12.13.0'} 7403 - 7404 - redent@3.0.0: 7405 - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} 7406 - engines: {node: '>=8'} 7407 7241 7408 7242 redis@5.8.1: 7409 7243 resolution: {integrity: sha512-RZjBKYX/qFF809x6vDcE5VA6L3MmiuT+BkbXbIyyyeU0lPD47V4z8qTzN+Z/kKFwpojwCItOfaItYuAjNs8pTQ==} ··· 7698 7532 shiki@3.9.2: 7699 7533 resolution: {integrity: sha512-t6NKl5e/zGTvw/IyftLcumolgOczhuroqwXngDeMqJ3h3EQiTY/7wmfgPlsmloD8oYfqkEDqxiaH37Pjm1zUhQ==} 7700 7534 7701 - siginfo@2.0.0: 7702 - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 7703 - 7704 7535 signal-exit@3.0.7: 7705 7536 resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 7706 7537 ··· 7796 7627 resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} 7797 7628 engines: {node: '>=10'} 7798 7629 7799 - stackback@0.0.2: 7800 - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 7801 - 7802 7630 stackframe@1.3.4: 7803 7631 resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} 7804 7632 ··· 7814 7642 resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 7815 7643 engines: {node: '>= 0.8'} 7816 7644 7817 - std-env@3.9.0: 7818 - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} 7819 - 7820 7645 stream-browserify@3.0.0: 7821 7646 resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} 7822 7647 ··· 7863 7688 resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 7864 7689 engines: {node: '>=12'} 7865 7690 7866 - strip-indent@3.0.0: 7867 - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} 7868 - engines: {node: '>=8'} 7869 - 7870 7691 strip-json-comments@2.0.1: 7871 7692 resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} 7872 7693 engines: {node: '>=0.10.0'} 7873 - 7874 - strip-literal@3.0.0: 7875 - resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} 7876 7694 7877 7695 strip-markdown@6.0.0: 7878 7696 resolution: {integrity: sha512-mSa8FtUoX3ExJYDkjPUTC14xaBAn4Ik5GPQD45G5E2egAmeV3kHgVSTfIoSDggbF6Pk9stahVgqsLCNExv6jHw==} ··· 7994 7812 resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} 7995 7813 engines: {node: '>=16'} 7996 7814 7997 - tinybench@2.9.0: 7998 - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 7999 - 8000 - tinyexec@0.3.2: 8001 - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 8002 - 8003 7815 tinyexec@1.0.1: 8004 7816 resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} 8005 7817 ··· 8007 7819 resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} 8008 7820 engines: {node: '>=12.0.0'} 8009 7821 8010 - tinypool@1.1.1: 8011 - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} 8012 - engines: {node: ^18.0.0 || >=20.0.0} 8013 - 8014 - tinyrainbow@2.0.0: 8015 - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 8016 - engines: {node: '>=14.0.0'} 8017 - 8018 - tinyspy@4.0.3: 8019 - resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} 8020 - engines: {node: '>=14.0.0'} 8021 - 8022 7822 title-case@3.0.3: 8023 7823 resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} 8024 7824 ··· 8442 8242 vue: 8443 8243 optional: true 8444 8244 8445 - vite-node@3.2.4: 8446 - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} 8447 - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 8448 - hasBin: true 8449 - 8450 8245 vite-plugin-environment@1.1.3: 8451 8246 resolution: {integrity: sha512-9LBhB0lx+2lXVBEWxFZC+WO7PKEyE/ykJ7EPWCq95NEcCpblxamTbs5Dm3DLBGzwODpJMEnzQywJU8fw6XGGGA==} 8452 8247 peerDependencies: ··· 8500 8295 yaml: 8501 8296 optional: true 8502 8297 8503 - vitest@3.2.4: 8504 - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} 8505 - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 8506 - hasBin: true 8507 - peerDependencies: 8508 - '@edge-runtime/vm': '*' 8509 - '@types/debug': ^4.1.12 8510 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 8511 - '@vitest/browser': 3.2.4 8512 - '@vitest/ui': 3.2.4 8513 - happy-dom: '*' 8514 - jsdom: '*' 8515 - peerDependenciesMeta: 8516 - '@edge-runtime/vm': 8517 - optional: true 8518 - '@types/debug': 8519 - optional: true 8520 - '@types/node': 8521 - optional: true 8522 - '@vitest/browser': 8523 - optional: true 8524 - '@vitest/ui': 8525 - optional: true 8526 - happy-dom: 8527 - optional: true 8528 - jsdom: 8529 - optional: true 8530 - 8531 8298 vlq@1.0.1: 8532 8299 resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} 8533 8300 ··· 8611 8378 which@2.0.2: 8612 8379 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 8613 8380 engines: {node: '>= 8'} 8614 - hasBin: true 8615 - 8616 - why-is-node-running@2.3.0: 8617 - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 8618 - engines: {node: '>=8'} 8619 8381 hasBin: true 8620 8382 8621 8383 wonka@6.3.5: ··· 8878 8640 optionalDependencies: 8879 8641 graphql: 16.11.0 8880 8642 8881 - '@adobe/css-tools@4.4.4': {} 8882 - 8883 8643 '@adraffy/ens-normalize@1.10.1': {} 8884 8644 8885 8645 '@adraffy/ens-normalize@1.11.0': {} ··· 8914 8674 transitivePeerDependencies: 8915 8675 - '@types/react' 8916 8676 8917 - '@apollo/client@3.13.9(@types/react@19.1.10)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.11.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': 8918 - dependencies: 8919 - '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) 8920 - '@wry/caches': 1.0.1 8921 - '@wry/equality': 0.5.7 8922 - '@wry/trie': 0.5.0 8923 - graphql: 16.11.0 8924 - graphql-tag: 2.12.6(graphql@16.11.0) 8925 - hoist-non-react-statics: 3.3.2 8926 - optimism: 0.18.1 8927 - prop-types: 15.8.1 8928 - rehackt: 0.1.0(@types/react@19.1.10)(react@19.1.1) 8929 - symbol-observable: 4.0.0 8930 - ts-invariant: 0.10.3 8931 - tslib: 2.8.1 8932 - zen-observable-ts: 1.2.5 8933 - optionalDependencies: 8934 - graphql-ws: 6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) 8935 - react: 19.1.1 8936 - react-dom: 19.1.1(react@19.1.1) 8937 - transitivePeerDependencies: 8938 - - '@types/react' 8939 - 8940 8677 '@apollo/client@3.13.9(@types/react@19.1.10)(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.11.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': 8941 8678 dependencies: 8942 8679 '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) ··· 13586 13323 13587 13324 '@tanstack/virtual-core@3.13.12': {} 13588 13325 13589 - '@testing-library/dom@10.4.1': 13590 - dependencies: 13591 - '@babel/code-frame': 7.27.1 13592 - '@babel/runtime': 7.28.3 13593 - '@types/aria-query': 5.0.4 13594 - aria-query: 5.3.0 13595 - dom-accessibility-api: 0.5.16 13596 - lz-string: 1.5.0 13597 - picocolors: 1.1.1 13598 - pretty-format: 27.5.1 13599 - 13600 - '@testing-library/jest-dom@6.7.0': 13601 - dependencies: 13602 - '@adobe/css-tools': 4.4.4 13603 - aria-query: 5.3.2 13604 - css.escape: 1.5.1 13605 - dom-accessibility-api: 0.6.3 13606 - picocolors: 1.1.1 13607 - redent: 3.0.0 13608 - 13609 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': 13610 - dependencies: 13611 - '@babel/runtime': 7.28.3 13612 - '@testing-library/dom': 10.4.1 13613 - react: 19.1.1 13614 - react-dom: 19.1.1(react@19.1.1) 13615 - optionalDependencies: 13616 - '@types/react': 19.1.10 13617 - '@types/react-dom': 19.1.7(@types/react@19.1.10) 13618 - 13619 - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': 13620 - dependencies: 13621 - '@testing-library/dom': 10.4.1 13622 - 13623 13326 '@theguild/federation-composition@0.19.1(graphql@16.11.0)': 13624 13327 dependencies: 13625 13328 constant-case: 3.0.4 ··· 13629 13332 lodash.sortby: 4.7.0 13630 13333 transitivePeerDependencies: 13631 13334 - supports-color 13632 - 13633 - '@types/aria-query@5.0.4': {} 13634 13335 13635 13336 '@types/babel__core@7.20.5': 13636 13337 dependencies: ··· 13653 13354 dependencies: 13654 13355 '@babel/types': 7.28.2 13655 13356 13656 - '@types/chai@5.2.2': 13657 - dependencies: 13658 - '@types/deep-eql': 4.0.2 13659 - 13660 13357 '@types/debug@4.1.12': 13661 13358 dependencies: 13662 13359 '@types/ms': 2.1.0 13663 - 13664 - '@types/deep-eql@4.0.2': {} 13665 13360 13666 13361 '@types/estree-jsx@1.0.5': 13667 13362 dependencies: ··· 13766 13461 transitivePeerDependencies: 13767 13462 - supports-color 13768 13463 13769 - '@vitest/expect@3.2.4': 13770 - dependencies: 13771 - '@types/chai': 5.2.2 13772 - '@vitest/spy': 3.2.4 13773 - '@vitest/utils': 3.2.4 13774 - chai: 5.2.1 13775 - tinyrainbow: 2.0.0 13776 - 13777 - '@vitest/mocker@3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))': 13778 - dependencies: 13779 - '@vitest/spy': 3.2.4 13780 - estree-walker: 3.0.3 13781 - magic-string: 0.30.17 13782 - optionalDependencies: 13783 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 13784 - 13785 - '@vitest/pretty-format@3.2.4': 13786 - dependencies: 13787 - tinyrainbow: 2.0.0 13788 - 13789 - '@vitest/runner@3.2.4': 13790 - dependencies: 13791 - '@vitest/utils': 3.2.4 13792 - pathe: 2.0.3 13793 - strip-literal: 3.0.0 13794 - 13795 - '@vitest/snapshot@3.2.4': 13796 - dependencies: 13797 - '@vitest/pretty-format': 3.2.4 13798 - magic-string: 0.30.17 13799 - pathe: 2.0.3 13800 - 13801 - '@vitest/spy@3.2.4': 13802 - dependencies: 13803 - tinyspy: 4.0.3 13804 - 13805 - '@vitest/utils@3.2.4': 13806 - dependencies: 13807 - '@vitest/pretty-format': 3.2.4 13808 - loupe: 3.2.0 13809 - tinyrainbow: 2.0.0 13810 - 13811 13464 '@wagmi/connectors@5.9.3(@types/react@19.1.10)(@wagmi/core@2.19.0(@tanstack/query-core@5.85.3)(@types/react@19.1.10)(immer@10.1.1)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.75)))(bufferutil@4.0.9)(immer@10.1.1)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.75))(zod@3.25.75)': 13812 13465 dependencies: 13813 13466 '@base-org/account': 1.1.1(@types/react@19.1.10)(bufferutil@4.0.9)(immer@10.1.1)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.75) ··· 14540 14193 dependencies: 14541 14194 tslib: 2.8.1 14542 14195 14543 - aria-query@5.3.0: 14544 - dependencies: 14545 - dequal: 2.0.3 14546 - 14547 - aria-query@5.3.2: {} 14548 - 14549 14196 array-timsort@1.0.3: {} 14550 14197 14551 14198 array-union@2.1.0: {} ··· 14553 14200 asap@2.0.6: {} 14554 14201 14555 14202 assert-options@0.8.3: {} 14556 - 14557 - assertion-error@2.0.1: {} 14558 14203 14559 14204 astral-regex@2.0.0: {} 14560 14205 ··· 14837 14482 pkg-types: 2.2.0 14838 14483 rc9: 2.1.2 14839 14484 14840 - cac@6.7.14: {} 14841 - 14842 14485 call-bind-apply-helpers@1.0.2: 14843 14486 dependencies: 14844 14487 es-errors: 1.3.0 ··· 14889 14532 14890 14533 ccount@2.0.1: {} 14891 14534 14892 - chai@5.2.1: 14893 - dependencies: 14894 - assertion-error: 2.0.1 14895 - check-error: 2.1.1 14896 - deep-eql: 5.0.2 14897 - loupe: 3.2.0 14898 - pathval: 2.0.1 14899 - 14900 14535 chalk@2.4.2: 14901 14536 dependencies: 14902 14537 ansi-styles: 3.2.1 ··· 14946 14581 14947 14582 chardet@2.1.0: {} 14948 14583 14949 - check-error@2.1.1: {} 14950 - 14951 14584 chokidar@3.6.0: 14952 14585 dependencies: 14953 14586 anymatch: 3.1.3 ··· 15194 14827 15195 14828 css-what@6.2.2: {} 15196 14829 15197 - css.escape@1.5.1: {} 15198 - 15199 14830 cssesc@3.0.0: {} 15200 14831 15201 14832 cssom@0.5.0: {} ··· 15252 14883 15253 14884 decode-uri-component@0.2.2: {} 15254 14885 15255 - deep-eql@5.0.2: {} 15256 - 15257 14886 deep-extend@0.6.0: {} 15258 14887 15259 14888 deepmerge-ts@7.1.5: {} ··· 15312 14941 15313 14942 dlv@1.1.3: {} 15314 14943 15315 - dom-accessibility-api@0.5.16: {} 15316 - 15317 - dom-accessibility-api@0.6.3: {} 15318 - 15319 14944 dom-serializer@2.0.0: 15320 14945 dependencies: 15321 14946 domelementtype: 2.3.0 ··· 15435 15060 15436 15061 es-errors@1.3.0: {} 15437 15062 15438 - es-module-lexer@1.7.0: {} 15439 - 15440 15063 es-object-atoms@1.1.1: 15441 15064 dependencies: 15442 15065 es-errors: 1.3.0 ··· 15488 15111 15489 15112 estree-util-is-identifier-name@3.0.0: {} 15490 15113 15491 - estree-walker@3.0.3: 15492 - dependencies: 15493 - '@types/estree': 1.0.8 15494 - 15495 15114 etag@1.8.1: {} 15496 15115 15497 15116 eth-block-tracker@7.1.0: ··· 15550 15169 events@3.3.0: {} 15551 15170 15552 15171 exec-async@2.2.0: {} 15553 - 15554 - expect-type@1.2.2: {} 15555 15172 15556 15173 expo-asset@11.1.7(expo@53.0.20(@babel/core@7.28.3)(@expo/metro-runtime@5.0.4(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(graphql@16.11.0)(react-native-webview@13.13.5(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10))(react@19.1.1))(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10))(react@19.1.1)(utf-8-validate@5.0.10))(react-native@0.79.5(@babel/core@7.28.3)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(utf-8-validate@5.0.10))(react@19.1.1): 15557 15174 dependencies: ··· 15975 15592 ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) 15976 15593 optional: true 15977 15594 15978 - graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): 15979 - dependencies: 15980 - graphql: 16.11.0 15981 - optionalDependencies: 15982 - crossws: 0.3.5 15983 - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) 15984 - optional: true 15985 - 15986 15595 graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): 15987 15596 dependencies: 15988 15597 graphql: 16.11.0 ··· 16535 16144 16536 16145 js-tokens@4.0.0: {} 16537 16146 16538 - js-tokens@9.0.1: {} 16539 - 16540 16147 js-yaml@3.14.1: 16541 16148 dependencies: 16542 16149 argparse: 1.0.10 ··· 16807 16414 dependencies: 16808 16415 js-tokens: 4.0.0 16809 16416 16810 - loupe@3.2.0: {} 16811 - 16812 16417 lower-case-first@2.0.2: 16813 16418 dependencies: 16814 16419 tslib: 2.8.1 ··· 16823 16428 dependencies: 16824 16429 yallist: 3.1.1 16825 16430 16826 - lz-string@1.5.0: {} 16827 - 16828 16431 magic-string@0.30.17: 16829 16432 dependencies: 16830 16433 '@jridgewell/sourcemap-codec': 1.5.5 ··· 17285 16888 17286 16889 mimic-fn@2.1.0: {} 17287 16890 17288 - min-indent@1.0.1: {} 17289 - 17290 16891 mini-svg-data-uri@1.4.4: {} 17291 16892 17292 16893 minimatch@3.1.2: ··· 17710 17311 17711 17312 pathe@2.0.3: {} 17712 17313 17713 - pathval@2.0.1: {} 17714 - 17715 17314 perfect-debounce@1.0.0: {} 17716 17315 17717 17316 pg-cloudflare@1.2.7: ··· 17905 17504 17906 17505 pretty-bytes@5.6.0: {} 17907 17506 17908 - pretty-format@27.5.1: 17909 - dependencies: 17910 - ansi-regex: 5.0.1 17911 - ansi-styles: 5.2.0 17912 - react-is: 17.0.2 17913 - 17914 17507 pretty-format@29.7.0: 17915 17508 dependencies: 17916 17509 '@jest/schemas': 29.6.3 ··· 18195 17788 18196 17789 react-is@16.13.1: {} 18197 17790 18198 - react-is@17.0.2: {} 18199 - 18200 17791 react-is@18.3.1: {} 18201 17792 18202 17793 react-is@19.1.1: {} ··· 18439 18030 18440 18031 real-require@0.1.0: {} 18441 18032 18442 - redent@3.0.0: 18443 - dependencies: 18444 - indent-string: 4.0.0 18445 - strip-indent: 3.0.0 18446 - 18447 18033 redis@5.8.1: 18448 18034 dependencies: 18449 18035 '@redis/bloom': 5.8.1(@redis/client@5.8.1) ··· 18804 18390 '@shikijs/vscode-textmate': 10.0.2 18805 18391 '@types/hast': 3.0.4 18806 18392 18807 - siginfo@2.0.0: {} 18808 - 18809 18393 signal-exit@3.0.7: {} 18810 18394 18811 18395 signal-exit@4.1.0: {} ··· 18901 18485 dependencies: 18902 18486 escape-string-regexp: 2.0.0 18903 18487 18904 - stackback@0.0.2: {} 18905 - 18906 18488 stackframe@1.3.4: {} 18907 18489 18908 18490 stacktrace-parser@0.1.11: ··· 18912 18494 statuses@1.5.0: {} 18913 18495 18914 18496 statuses@2.0.1: {} 18915 - 18916 - std-env@3.9.0: {} 18917 18497 18918 18498 stream-browserify@3.0.0: 18919 18499 dependencies: ··· 18965 18545 dependencies: 18966 18546 ansi-regex: 6.1.0 18967 18547 18968 - strip-indent@3.0.0: 18969 - dependencies: 18970 - min-indent: 1.0.1 18971 - 18972 18548 strip-json-comments@2.0.1: {} 18973 - 18974 - strip-literal@3.0.0: 18975 - dependencies: 18976 - js-tokens: 9.0.1 18977 18549 18978 18550 strip-markdown@6.0.0: 18979 18551 dependencies: ··· 19120 18692 19121 18693 timeout-signal@2.0.0: {} 19122 18694 19123 - tinybench@2.9.0: {} 19124 - 19125 - tinyexec@0.3.2: {} 19126 - 19127 18695 tinyexec@1.0.1: {} 19128 18696 19129 18697 tinyglobby@0.2.14: 19130 18698 dependencies: 19131 18699 fdir: 6.5.0(picomatch@4.0.3) 19132 18700 picomatch: 4.0.3 19133 - 19134 - tinypool@1.1.1: {} 19135 - 19136 - tinyrainbow@2.0.0: {} 19137 - 19138 - tinyspy@4.0.3: {} 19139 18701 19140 18702 title-case@3.0.3: 19141 18703 dependencies: ··· 19488 19050 react: 19.1.1 19489 19051 react-dom: 19.1.1(react@19.1.1) 19490 19052 19491 - vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1): 19492 - dependencies: 19493 - cac: 6.7.14 19494 - debug: 4.4.1 19495 - es-module-lexer: 1.7.0 19496 - pathe: 2.0.3 19497 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 19498 - transitivePeerDependencies: 19499 - - '@types/node' 19500 - - jiti 19501 - - less 19502 - - lightningcss 19503 - - sass 19504 - - sass-embedded 19505 - - stylus 19506 - - sugarss 19507 - - supports-color 19508 - - terser 19509 - - tsx 19510 - - yaml 19511 - 19512 19053 vite-plugin-environment@1.1.3(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)): 19513 19054 dependencies: 19514 19055 vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) ··· 19540 19081 terser: 5.43.1 19541 19082 tsx: 4.20.4 19542 19083 yaml: 2.8.1 19543 - 19544 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1): 19545 - dependencies: 19546 - '@types/chai': 5.2.2 19547 - '@vitest/expect': 3.2.4 19548 - '@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) 19549 - '@vitest/pretty-format': 3.2.4 19550 - '@vitest/runner': 3.2.4 19551 - '@vitest/snapshot': 3.2.4 19552 - '@vitest/spy': 3.2.4 19553 - '@vitest/utils': 3.2.4 19554 - chai: 5.2.1 19555 - debug: 4.4.1 19556 - expect-type: 1.2.2 19557 - magic-string: 0.30.17 19558 - pathe: 2.0.3 19559 - picomatch: 4.0.3 19560 - std-env: 3.9.0 19561 - tinybench: 2.9.0 19562 - tinyexec: 0.3.2 19563 - tinyglobby: 0.2.14 19564 - tinypool: 1.1.1 19565 - tinyrainbow: 2.0.0 19566 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 19567 - vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) 19568 - why-is-node-running: 2.3.0 19569 - optionalDependencies: 19570 - '@types/debug': 4.1.12 19571 - '@types/node': 24.3.0 19572 - jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) 19573 - transitivePeerDependencies: 19574 - - jiti 19575 - - less 19576 - - lightningcss 19577 - - msw 19578 - - sass 19579 - - sass-embedded 19580 - - stylus 19581 - - sugarss 19582 - - supports-color 19583 - - terser 19584 - - tsx 19585 - - yaml 19586 19084 19587 19085 vlq@1.0.1: {} 19588 19086 ··· 19691 19189 which@2.0.2: 19692 19190 dependencies: 19693 19191 isexe: 2.0.0 19694 - 19695 - why-is-node-running@2.3.0: 19696 - dependencies: 19697 - siginfo: 2.0.0 19698 - stackback: 0.0.2 19699 19192 19700 19193 wonka@6.3.5: {} 19701 19194