diff --git a/storybook/stories/components/CafeUnitTest/index.jsx b/storybook/stories/components/CafeUnitTest/index.jsx index e5c13a6b4..7dcc2d1ec 100644 --- a/storybook/stories/components/CafeUnitTest/index.jsx +++ b/storybook/stories/components/CafeUnitTest/index.jsx @@ -1,22 +1,26 @@ /* eslint-disable */ -import React from "react"; -import { useEffect, useState, Suspense } from "react"; -import { useAccount, useSwitchChain } from "wagmi"; -import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from "wagmi"; -import { WagmiProvider, createConfig, http } from "wagmi"; -import { mainnet, base, baseSepolia, baseGoerli } from "wagmi/chains"; +import React, { useEffect, useState } from "react"; +import { + useAccount, + useSwitchChain, + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, + WagmiProvider, + createConfig, + http, +} from "wagmi"; +import { base, baseSepolia } from "wagmi/chains"; import { coinbaseWallet, metaMask, injected } from "wagmi/connectors"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { OnchainKitProvider } from "@coinbase/onchainkit"; import { ConnectWallet, - ConnectWalletText, Wallet, WalletDropdown, WalletDropdownBasename, WalletDropdownDisconnect, - WalletDefault, } from "@coinbase/onchainkit/wallet"; import { Address, Avatar, Name, Identity, EthBalance } from "@coinbase/onchainkit/identity"; import { color } from "@coinbase/onchainkit/theme"; @@ -117,55 +121,7 @@ const directionsStyle = { fontWeight: "500", }; -const containerStyle = { - fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif", - maxWidth: "800px", - margin: "0 auto", - padding: "32px", - background: "linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)", - borderRadius: "20px", - boxShadow: "0 10px 40px rgba(0, 0, 0, 0.1)", - backdropFilter: "blur(10px)", - border: "1px solid rgba(255, 255, 255, 0.2)", - minHeight: "600px", -}; - -const cardStyle = { - background: "rgba(255, 255, 255, 0.9)", - backdropFilter: "blur(10px)", - borderRadius: "16px", - padding: "24px", - marginBottom: "24px", - boxShadow: "0 8px 25px rgba(0, 0, 0, 0.08)", - border: "1px solid rgba(255, 255, 255, 0.5)", -}; - -const titleStyle = { - fontSize: "24px", - fontWeight: "700", - color: "#2d3748", - marginBottom: "24px", - textAlign: "center", - background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", - backgroundClip: "text", - WebkitBackgroundClip: "text", - WebkitTextFillColor: "transparent", -}; - -const walletSectionStyle = { - background: "rgba(255, 255, 255, 0.95)", - borderRadius: "16px", - padding: "24px", - marginBottom: "4px", - boxShadow: "0 8px 25px rgba(0, 0, 0, 0.08)", - border: "1px solid rgba(255, 255, 255, 0.5)", - textAlign: "center", -}; - -// Create a query client for tanstack query (required by wagmi v2) -const queryClient = new QueryClient(); - -// Define the SANDBOX_CHAIN (matching App.tsx) +// Custom network export const SANDBOX_CHAIN = defineChain({ id: 8453200058, name: "Sandbox Network", @@ -181,28 +137,6 @@ export const SANDBOX_CHAIN = defineChain({ }, }); -// Define your wagmi config -const wagmiConfig = createConfig({ - chains: [base, baseSepolia, SANDBOX_CHAIN], - connectors: [ - coinbaseWallet({ - appName: "OnchainKit", - }), - metaMask({ - dappMetadata: { - name: "OnchainKit", - }, - }), - injected(), - ], - ssr: true, - transports: { - [base.id]: http(), - [baseSepolia.id]: http(), - [SANDBOX_CHAIN.id]: http(), - }, -}); - export function CafeUnitTest({ nftNum }) { const { isConnecting, isDisconnected, address, chain } = useAccount(); const { switchChain } = useSwitchChain(); @@ -211,31 +145,25 @@ export function CafeUnitTest({ nftNum }) { const [contractFormEntry, setContractFormEntry] = useState(""); const [submittedContract, setSubmittedContract] = useState(""); const [hasPin, setHasPin] = useState(false); - const [fetchNFTStatus, setFetchNFTStatus] = useState(true); const [testingState, setTestingState] = useState("idle"); // 'idle', 'testing', 'waiting', 'completed', 'error' const nftData = useNFTData(); - const nft = nftData[nftNum]; - const { data: hasNFT, error: nftError } = useReadContract({ + const { + data: hasNFT, + error: nftError, + refetch: refetchNFT, + } = useReadContract({ address: nft.deployment.address, abi: nft.deployment.abi, - functionName: "owners", + functionName: "balanceOf", args: [address], - enabled: fetchNFTStatus, - onSettled(data, error) { - if (error) { - console.error("Error checking NFT ownership:", error); - setMessages(["Error checking NFT ownership status.", "Please check your connection and try again."]); - } else { - setHasPin(!!data); - } - setFetchNFTStatus(false); + query: { + enabled: !!address, }, }); - // Test Contract Function const { writeContract: testContract, isPending: isTestLoading, @@ -257,12 +185,18 @@ export function CafeUnitTest({ nftNum }) { setContractFormEntry(event.target.value); } + // NFT ownership / errors useEffect(() => { - if (hasNFT != null) { - setHasPin(hasNFT); + if (nftError) { + console.error("Error checking NFT ownership:", nftError); + setMessages(["Error checking NFT ownership status.", "Please check your connection and try again."]); + setHasPin(false); + } else if (hasNFT !== undefined) { + setHasPin(Number(hasNFT) > 0); } - }, [hasNFT]); + }, [hasNFT, nftError, address]); + // Error during contract testing useEffect(() => { if (isTestError) { setMessages([ @@ -274,7 +208,7 @@ export function CafeUnitTest({ nftNum }) { } }, [isTestError]); - // Update manual state based on wagmi states + // wagmi state → local state useEffect(() => { if (isTestLoading) { setTestingState("testing"); @@ -287,67 +221,58 @@ export function CafeUnitTest({ nftNum }) { } }, [isTestReceiptLoading, transactionHash]); + // Receipt → completed + refetch NFT useEffect(() => { if (transactionReceipt) { setTestingState("completed"); console.log("Transaction receipt received:", transactionReceipt); + if (address) { + refetchNFT(); + } } - }, [transactionReceipt]); + }, [transactionReceipt, address, refetchNFT]); - // Reset everything when chain or address changes + // Timeout for a stuck transaction (with unmount protection) + useEffect(() => { + if (!transactionHash) return; + + let isMounted = true; + + const timeoutId = setTimeout(() => { + if (!isMounted) return; + + setTestingState((state) => { + if (state === "waiting" || state === "testing") { + setMessages((prev) => [...prev, "Transaction taking too long. Please try again."]); + return "idle"; + } + return state; + }); + }, 30000); + + return () => { + isMounted = false; + clearTimeout(timeoutId); + }; + }, [transactionHash]); + + // Reset when network / address changes useEffect(() => { console.log("Connected to chain:", chain?.id, chain?.name); - // Reset all state when chain or address changes setTestingState("idle"); if (!submittedContract) { setMessages(["Submit your contract address."]); } + setHasPin(false); }, [chain, address, submittedContract]); + // Parsing TestSuiteResult events (log aggregation + unmount protection + null-safe) useEffect(() => { - async function processEventLog(parsedLog) { - const processed = []; - if (parsedLog.eventName === "TestSuiteResult") { - const { testResults } = parsedLog.args; - // Results don't know which tests failed, so find them - for (const testResult of testResults) { - processed.push(`✅ ${testResult.message}`); - const { assertResults } = testResult; - const { elements: arList, num } = assertResults; - // Slice out unused in array - arList is a dynamic memory array implementation - // so it may have unused elements allocated - const elements = arList.slice(0, Number(num)); - let passedAllAsserts = true; - for (const element of elements) { - if (!element.passed) { - passedAllAsserts = false; - } - } - if (!passedAllAsserts) { - processed[processed.length - 1] = `❌${processed[processed.length - 1].slice(1)}`; - for (const element of elements) { - if (element.passed === false) { - try { - processed.push(`-> ${element.assertionError}`); - } catch { - // An error in the assert smart contract sometimes sends strings - // with bytes that can't be converted to utf-8 - // It can't be fixed here because the error is caused within iface.parseLog(log) - // See: https://github.com/ethers-io/ethers.js/issues/714 - - processed.push("-> Assertion failed (cannot parse message)"); - } - } - } - } - } - } - - setMessages([...processed]); - setTestingState("completed"); - } + let isMounted = true; if (transactionReceipt) { + const allProcessed: string[] = []; + for (const log of transactionReceipt.logs) { try { const parsed = decodeEventLog({ @@ -355,20 +280,71 @@ export function CafeUnitTest({ nftNum }) { data: log.data, topics: log.topics, }); - console.log("topics", parsed); - processEventLog(parsed); + + if (parsed.eventName === "TestSuiteResult") { + const args: any = parsed.args || {}; + const testResults = args.testResults; + + if (testResults && Array.isArray(testResults)) { + for (const testResult of testResults) { + if (!testResult || typeof testResult !== "object") continue; + + const message = testResult.message || "Unknown test"; + allProcessed.push(`✅ ${message}`); + + const assertResults = testResult.assertResults || {}; + const arList = Array.isArray(assertResults.elements) ? assertResults.elements : []; + const num = Number(assertResults.num || arList.length); + const elements = arList.slice(0, num); + + let passedAllAsserts = true; + for (const element of elements) { + if (element && element.passed === false) { + passedAllAsserts = false; + } + } + + if (!passedAllAsserts) { + const lastIndex = allProcessed.length - 1; + if (lastIndex >= 0) { + allProcessed[lastIndex] = `❌${allProcessed[lastIndex].slice(1)}`; + } + + for (const element of elements) { + if (element && element.passed === false) { + try { + const errMsg = element.assertionError || "Unknown error"; + allProcessed.push(`-> ${errMsg}`); + } catch { + allProcessed.push("-> Assertion failed (cannot parse message)"); + } + } + } + } + } + } else if (allProcessed.length === 0) { + allProcessed.push("⚠️ No valid test results found."); + } + } } catch (e) { - // Skip other log types (can't tell type without parsing) console.log("SKIPPED LOG", e); } } + + if (isMounted && allProcessed.length > 0) { + setMessages(allProcessed); + setTestingState("completed"); + } } - }, [transactionReceipt, contractFormEntry, nft.deployment.abi]); + + return () => { + isMounted = false; + }; + }, [transactionReceipt, nft.deployment.abi]); async function handleContractSubmit(event) { event.preventDefault(); - // Clear any previous submission state setTestingState("testing"); setSubmittedContract(contractFormEntry); setMessages(["Running tests..."]); @@ -380,17 +356,6 @@ export function CafeUnitTest({ nftNum }) { functionName: "testContract", args: [contractFormEntry], }); - - // Set a timeout in case transaction hangs - const timeoutId = setTimeout(() => { - if (testingState === "waiting" || testingState === "testing") { - console.log("Transaction taking too long, resetting state"); - setMessages([...messages, "Transaction taking too long. Please try again."]); - setTestingState("idle"); - } - }, 30000); // 30 second timeout - - return () => clearTimeout(timeoutId); } catch (error) { console.error("Error submitting contract:", error); setMessages(["Error submitting contract for testing.", "Please check your connection and try again."]); @@ -398,18 +363,15 @@ export function CafeUnitTest({ nftNum }) { } } - // Handle the manual reset button action + // Manual reset (not yet wired to UI) function handleManualReset() { console.log("Manual reset triggered"); - // Reset the testing state setTestingState("idle"); setMessages(["Submit your contract address."]); - // Also try the wagmi resets if (resetTestContract) resetTestContract(); if (resetTransactionReceipt) resetTransactionReceipt(); - // Force clear localStorage related to the current state try { Object.keys(localStorage).forEach((key) => { if (key.includes("wagmi") || key.includes("transaction")) { @@ -426,7 +388,6 @@ export function CafeUnitTest({ nftNum }) { const listItems = messages.map((message, index) => { let style = messageStyle; - // Apply appropriate style based on testing state if (testingState === "error") { style = errorMessageStyle; } else if (testingState === "testing" || testingState === "waiting") { @@ -468,57 +429,67 @@ export function CafeUnitTest({ nftNum }) { -
+
Please connect your wallet to continue
); } - if (isConnecting) { - return ( -
- Connecting... -
- ); - } + padding: "20px", + }} + > + Connecting... + + ); + } + if (chain?.id !== baseSepolia.id) { - return ( -
-
+
- ⚠️ You are not connected to Base Sepolia -
- + marginBottom: "16px", + }} + > + ⚠️ You are not connected to Base Sepolia
- ); + +
+ ); } + return ( -
+ }} + >
@@ -547,22 +519,29 @@ export function CafeUnitTest({ nftNum }) {
-
- {renderTests()} -
- -
- {renderResult()} -
- -
+
{renderTests()}
+ +
{renderResult()}
+ + { e.target.style.borderColor = "#667eea"; - e.target.style.boxShadow = "0 0 0 3px rgba(102, 126, 234, 0.1), 0 4px 12px rgba(0, 0, 0, 0.1)"; + e.target.style.boxShadow = + "0 0 0 3px rgba(102, 126, 234, 0.1), 0 4px 12px rgba(0, 0, 0, 0.1)"; }} onBlur={(e) => { e.target.style.borderColor = "#e2e8f0"; @@ -584,22 +564,21 @@ export function CafeUnitTest({ nftNum }) { ...buttonStyle, ...buttonEnabledColor, }} - type="button" - onClick={handleContractSubmit} + type="submit" onMouseEnter={(e) => { - if (!e.target.disabled) { - Object.assign(e.target.style, buttonEnabledColor); + if (!e.currentTarget.disabled) { + Object.assign(e.currentTarget.style, buttonEnabledColor); } }} onMouseLeave={(e) => { - if (!e.target.disabled) { - e.target.style.transform = "translateY(0)"; - e.target.style.boxShadow = "0 4px 15px rgba(0, 81, 255, 0.3)"; + if (!e.currentTarget.disabled) { + e.currentTarget.style.transform = "translateY(0)"; + e.currentTarget.style.boxShadow = "0 4px 15px rgba(0, 81, 255, 0.3)"; } }} - > - Submit - + > + Submit + ) : ( + disabled + > + {testingState === "testing" ? "🔍 Testing Contract..." : "⏳ Waiting for confirmation..."} + )}
@@ -620,12 +599,10 @@ export function CafeUnitTest({ nftNum }) { return
{renderTestSubmission()}
; } -// Create a wrapper component that provides the necessary context +// Wrapper with providers function CafeUnitTestWithProviders(props) { - // Use client-side only rendering to avoid hydration issues const [mounted, setMounted] = useState(false); - // Create stable instances of query client and config const queryClientRef = React.useRef(new QueryClient()); const configRef = React.useRef( createConfig({ @@ -650,19 +627,17 @@ function CafeUnitTestWithProviders(props) { }) ); - // This prevents hydration errors by only rendering client-side useEffect(() => { setMounted(true); }, []); - // Return a loading state on the server if (!mounted) { return
Loading wallet connection...
; } - // Get API keys from environment variables - const viteCdpApiKey = "15d5754b-9cab-406a-8720-42c7341f5945"; - const viteProjectId = "81eac42f-0b13-48e3-a6bb-9dd44a958a1e"; + const viteCdpApiKey = import.meta.env.VITE_CDP_API_KEY || "YOUR_CDP_API_KEY_HERE"; + const viteProjectId = import.meta.env.VITE_PROJECT_ID || "YOUR_PROJECT_ID_HERE"; + const schemaId = import.meta.env.VITE_SCHEMA_ID || "YOUR_SCHEMA_ID_HERE"; return ( <> @@ -672,29 +647,29 @@ function CafeUnitTestWithProviders(props) { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - + @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } - + .cafe-unit-test-container { animation: fadeIn 0.5s ease-out; color: #1a202c; } - + .cafe-unit-test-container input:focus { transform: translateY(-1px); } - + .cafe-unit-test-container button:hover:not(:disabled) { transform: translateY(-2px) !important; } - + .cafe-unit-test-container button:active:not(:disabled) { transform: translateY(0px) !important; } - + .cafe-unit-test-container input::placeholder { color: #a0aec0; font-weight: 400; @@ -707,14 +682,14 @@ function CafeUnitTestWithProviders(props) { apiKey={viteCdpApiKey} chain={baseSepolia} projectId={viteProjectId} - schemaId="0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9" + schemaId={schemaId} config={{ appearance: { mode: "auto", theme: "default", }, wallet: { - display: 'modal', + display: "modal", }, }} > @@ -730,18 +705,21 @@ function CafeUnitTestWithProviders(props) { ); } -// Simple error boundary component to catch and display errors -class ErrorBoundary extends React.Component { +// Error boundary +class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + { hasError: boolean; error: Error | null } +> { constructor(props) { super(props); this.state = { hasError: false, error: null }; } - static getDerivedStateFromError(error) { + static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } - componentDidCatch(error, errorInfo) { + componentDidCatch(error: Error, errorInfo: any) { console.error("Error in CafeUnitTest component:", error, errorInfo); } @@ -766,5 +744,5 @@ class ErrorBoundary extends React.Component { } } -// Export the wrapped component instead export default CafeUnitTestWithProviders; +```