Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,61 @@ jobs:
if: steps.check.outputs.has_module == 'true'
run: go vet ./internal/...

e2e-tests:
name: E2E Tests (Playwright)
runs-on: ubuntu-latest
needs: [dapp-frontend, dapp-backend]
defaults:
run:
working-directory: tests/e2e
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm install

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Build dapp frontend
working-directory: apps/dapp/frontend
run: npm install && npm run build

- name: Start dapp frontend
working-directory: apps/dapp/frontend
run: npm run start &
env:
PORT: 3001

- name: Wait for frontend to be ready
run: npx wait-on http://localhost:3001 --timeout 60000

- name: Run Playwright E2E tests
run: npx playwright test --project=chromium
env:
CI: true
E2E_BASE_URL: http://localhost:3001

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: tests/e2e/playwright-report/
retention-days: 7

- name: Upload test traces
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: tests/e2e/test-results/
retention-days: 7

contracts:
name: Contracts (Rust)
runs-on: ubuntu-latest
Expand Down
8 changes: 4 additions & 4 deletions apps/dapp/frontend/app/dashboard/history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const STATUS_COLORS = {
const PAGE_SIZE = 10;

export default function HistoryPage() {
const { isConnected } = useWallet();
const { isConnected, isInitializing } = useWallet();
const { transactions } = usePortfolio();
const router = useRouter();

Expand All @@ -57,10 +57,10 @@ export default function HistoryPage() {
const [currentPage, setCurrentPage] = useState(1);

useEffect(() => {
if (!isConnected) {
if (!isInitializing && !isConnected) {
router.push("/");
}
}, [isConnected, router]);
}, [isConnected, isInitializing, router]);

const filteredTransactions = useMemo(() => {
return transactions.filter(tx => {
Expand Down Expand Up @@ -110,7 +110,7 @@ export default function HistoryPage() {
URL.revokeObjectURL(url);
};

if (!isConnected) return null;
if (isInitializing || !isConnected) return null;

const isInitiallyEmpty = transactions.length === 0;

Expand Down
8 changes: 4 additions & 4 deletions apps/dapp/frontend/app/dashboard/notifications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ function formatDate(timestamp: string) {
}

export default function NotificationsPage() {
const { isConnected } = useWallet();
const { isConnected, isInitializing } = useWallet();
const router = useRouter();
const { notifications, unreadCount, markAsRead, markAllAsRead } =
useNotifications();

useEffect(() => {
if (!isConnected) {
if (!isInitializing && !isConnected) {
router.push("/");
}
}, [isConnected, router]);
}, [isConnected, isInitializing, router]);

if (!isConnected) return null;
if (isInitializing || !isConnected) return null;

return (
<div className="min-h-screen bg-background">
Expand Down
8 changes: 4 additions & 4 deletions apps/dapp/frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ import { PrometheusPanel } from "@/components/ai/prometheusPanel";
import { GuidedTour } from "@/components/onboarding/GuidedTour";

export default function Dashboard() {
const { isConnected, address } = useWallet();
const { isConnected, isInitializing, address } = useWallet();
const { positions, transactions, balances } = usePortfolio();
const router = useRouter();
const [selectedPosition, setSelectedPosition] =
useState<PortfolioPosition | null>(null);

useEffect(() => {
if (!isConnected) {
if (!isInitializing && !isConnected) {
router.push("/");
}
}, [isConnected, router]);
}, [isConnected, isInitializing, router]);

const stats = useMemo(() => {
const totalBalance = positions.reduce(
Expand Down Expand Up @@ -85,7 +85,7 @@ export default function Dashboard() {

const recentTransactions = transactions.slice(0, 5);

if (!isConnected) return null;
if (isInitializing || !isConnected) return null;

return (
<div className="min-h-screen bg-background">
Expand Down
8 changes: 4 additions & 4 deletions apps/dapp/frontend/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type NotificationSettings = {
import { useSettings } from "@/context/settings-context";

export default function SettingsPage() {
const { isConnected, address, disconnect } = useWallet();
const { isConnected, isInitializing, address, disconnect } = useWallet();
const { currency, setCurrency } = useSettings();
const router = useRouter();
const [copied, setCopied] = useState(false);
Expand All @@ -45,10 +45,10 @@ export default function SettingsPage() {
const [autoDisconnect, setAutoDisconnect] = useState("30");

useEffect(() => {
if (!isConnected) {
if (!isInitializing && !isConnected) {
router.push("/");
}
}, [isConnected, router]);
}, [isConnected, isInitializing, router]);

// Load from LocalStorage
useEffect(() => {
Expand Down Expand Up @@ -85,7 +85,7 @@ export default function SettingsPage() {
}
};

if (!isConnected) return null;
if (isInitializing || !isConnected) return null;

return (
<div className="min-h-screen bg-background text-foreground selection:bg-emerald-100 selection:text-emerald-900">
Expand Down
8 changes: 4 additions & 4 deletions apps/dapp/frontend/app/dashboard/settlements/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function buildQuotes(
}

export default function SettlementsPage() {
const { isConnected } = useWallet();
const { isConnected, isInitializing } = useWallet();
const { addNotification } = useNotifications();
const router = useRouter();

Expand All @@ -109,10 +109,10 @@ export default function SettlementsPage() {
const refreshRef = useRef<ReturnType<typeof setInterval> | null>(null);

useEffect(() => {
if (!isConnected) {
if (!isInitializing && !isConnected) {
router.push("/");
}
}, [isConnected, router]);
}, [isConnected, isInitializing, router]);

const numericAmount = parseFloat(sendAmount) || 0;
const allFieldsFilled =
Expand Down Expand Up @@ -188,7 +188,7 @@ export default function SettlementsPage() {
};
}, [allFieldsFilled, numericAmount, selectedBank, receiveCurrency, runQuoteScan, silentRefresh]);

if (!isConnected) return null;
if (isInitializing || !isConnected) return null;

const displayReceive = selectedQuote
? selectedQuote.receiveAmount
Expand Down
10 changes: 5 additions & 5 deletions apps/dapp/frontend/app/dashboard/vaults/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,16 +223,16 @@ function VaultsPageContent({ onSelect }: { onSelect: (v: VaultType) => void }) {
// -------------------- PAGE --------------------

export default function VaultsPage() {
const { isConnected } = useWallet();
const { isConnected, isInitializing } = useWallet();
const router = useRouter();

const [selectedVault, setSelectedVault] = useState<VaultType | null>(null);

useEffect(() => {
if (!isConnected) router.push("/");
}, [isConnected, router]);
if (!isInitializing && !isConnected) router.push("/");
}, [isConnected, isInitializing, router]);

if (!isConnected) return null;
if (isInitializing || !isConnected) return null;

return (
<div className="min-h-screen bg-background">
Expand All @@ -253,4 +253,4 @@ export default function VaultsPage() {
/>
</div>
);
}
}
8 changes: 4 additions & 4 deletions apps/dapp/frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import { WelcomeModal } from "@/components/onboarding/WelcomeModal";
import { useOnboarding } from "@/hooks/useOnboarding";

export default function Home() {
const { isConnected } = useWallet();
const { isConnected, isInitializing } = useWallet();
const { hasConnectedWallet } = useOnboarding();
const router = useRouter();

useEffect(() => {
if (isConnected && hasConnectedWallet) {
if (!isInitializing && isConnected && hasConnectedWallet) {
router.push("/dashboard");
}
}, [isConnected, hasConnectedWallet, router]);
}, [isConnected, isInitializing, hasConnectedWallet, router]);

if (isConnected && hasConnectedWallet) return null;
if (isInitializing || (isConnected && hasConnectedWallet)) return null;

return (
<>
Expand Down
35 changes: 35 additions & 0 deletions apps/dapp/frontend/components/wallet-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface WalletState {
address: string | null;
isConnected: boolean;
isConnecting: boolean;
isInitializing: boolean;
wallets: WalletInfo[];
walletsLoaded: boolean;
selectedWalletId: string | null;
Expand All @@ -36,6 +37,7 @@ const WalletContext = createContext<WalletState>({
address: null,
isConnected: false,
isConnecting: false,
isInitializing: true,
wallets: [],
walletsLoaded: false,
selectedWalletId: null,
Expand Down Expand Up @@ -65,6 +67,7 @@ export function WalletProvider({ children }: { children: ReactNode }) {
const { currentNetwork } = useNetwork();
const [address, setAddress] = useState<string | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const [isInitializing, setIsInitializing] = useState(true);
const [wallets, setWallets] = useState<WalletInfo[]>([]);
const [walletsLoaded, setWalletsLoaded] = useState(false);
const [selectedWalletId, setSelectedWalletId] = useState<string | null>(
Expand All @@ -76,6 +79,35 @@ export function WalletProvider({ children }: { children: ReactNode }) {
if (typeof window === "undefined") return;

const initKit = async () => {
// E2E test bypass: if the page has injected a mock wallet session
// via window.__e2e_wallet__, restore it directly without calling
// the real wallet kit (which requires a browser extension).
// Only active in test environments — never in production.
const e2eWallet = process.env.NODE_ENV !== "production"
? (window as unknown as Record<string, unknown>)
.__e2e_wallet__ as
| { address: string; walletId: string }
| undefined
: undefined;
if (e2eWallet?.address && e2eWallet?.walletId) {
setAddress(e2eWallet.address);
setSelectedWalletId(e2eWallet.walletId);
setWallets([
{
id: e2eWallet.walletId,
name: "Freighter",
icon: "",
url: "https://freighter.app",
installUrl: "https://freighter.app",
isAvailable: true,
},
]);
setWalletsLoaded(true);
setKitReady(true);
setIsInitializing(false);
return;
}

try {
const { StellarWalletsKit } = await import(
"@creit.tech/stellar-wallets-kit"
Expand Down Expand Up @@ -146,6 +178,8 @@ export function WalletProvider({ children }: { children: ReactNode }) {
} catch (err) {
console.error("Failed to initialize wallet kit:", err);
setWalletsLoaded(true);
} finally {
setIsInitializing(false);
}
};

Expand Down Expand Up @@ -225,6 +259,7 @@ export function WalletProvider({ children }: { children: ReactNode }) {
address,
isConnected: !!address,
isConnecting,
isInitializing,
wallets,
walletsLoaded,
selectedWalletId,
Expand Down
45 changes: 45 additions & 0 deletions scripts/wait-for-services.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Wait for all Nester services to become healthy before running E2E tests.
# Used in CI when starting services via Docker Compose.
#
# Usage: ./scripts/wait-for-services.sh [--timeout 120]

set -euo pipefail

TIMEOUT=120
INTERVAL=3

while [[ $# -gt 0 ]]; do
case $1 in
--timeout) TIMEOUT="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done

wait_for_url() {
local name="$1"
local url="$2"
local elapsed=0

echo "Waiting for $name at $url ..."
until curl -sf "$url" > /dev/null 2>&1; do
if [[ $elapsed -ge $TIMEOUT ]]; then
echo "ERROR: $name did not become ready within ${TIMEOUT}s"
exit 1
fi
sleep "$INTERVAL"
elapsed=$((elapsed + INTERVAL))
done
echo " $name is ready (${elapsed}s)"
}

# Dapp frontend (Next.js)
FRONTEND_URL="${E2E_BASE_URL:-http://localhost:3001}"
wait_for_url "Dapp Frontend" "$FRONTEND_URL"

# Dapp backend (Express)
BACKEND_URL="${DAPP_BACKEND_URL:-http://localhost:8080}/health-check"
wait_for_url "Dapp Backend" "$BACKEND_URL"

echo ""
echo "All services are ready. Proceeding with E2E tests."
3 changes: 3 additions & 0 deletions tests/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
playwright-report/
test-results/
Loading
Loading