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 }) {