Skip to content
Merged
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
4 changes: 4 additions & 0 deletions bimex-frontend/e2e/fixtures/freighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export async function mockFreighterConnected(page: Page): Promise<void> {
// isConnected() short-circuits when window.freighter is truthy
;(window as any).freighter = true

// walletKit.getConnectedAddress() requires this to attempt auto-reconnect
localStorage.setItem('lastWallet', 'freighter')

// Intercept postMessage to fake Freighter extension responses
const _postMessage = window.postMessage.bind(window)
window.postMessage = (data: any, targetOrigin: any, transfer?: any) => {
Expand Down Expand Up @@ -79,6 +82,7 @@ export async function mockFreighterConnected(page: Page): Promise<void> {
export async function mockFreighterDisconnected(page: Page): Promise<void> {
await page.addInitScript(() => {
;(window as any).freighter = undefined
localStorage.removeItem('lastWallet')

const _postMessage = window.postMessage.bind(window)
window.postMessage = (data: any, targetOrigin: any, transfer?: any) => {
Expand Down
6 changes: 3 additions & 3 deletions bimex-frontend/e2e/golden-path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ test.describe('Golden Path – flujo completo connect wallet → invertir → yi
})
await page.goto('/')

const heroBtn = page.getByRole('button', { name: /freighter/i }).first()
await expect(heroBtn).toBeVisible({ timeout: 10_000 })
const hero = page.locator('.landing-hero-section')
await expect(hero.getByRole('button', { name: /huella|face id|passkey|conectar/i }).first()).toBeVisible({ timeout: 10_000 })

const navBtn = page.getByRole('button', { name: /^conectar$/i })
const navBtn = page.locator('nav[aria-label="Navegación principal"]').getByRole('button', { name: /conectar/i })
await expect(navBtn).toBeVisible()

await expect(page).toHaveURL('/')
Expand Down
8 changes: 4 additions & 4 deletions bimex-frontend/e2e/landing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ test.describe('Landing – sin wallet', () => {
await page.goto('/')
})

test('muestra el botón "Freighter" en el hero', async ({ page }) => {
const heroBtn = page.getByRole('button', { name: /freighter/i }).first()
await expect(heroBtn).toBeVisible()
test('muestra los botones de conexión en el hero', async ({ page }) => {
const hero = page.locator('.landing-hero-section')
await expect(hero.getByRole('button', { name: /huella|face id|passkey|conectar/i }).first()).toBeVisible()
})

test('muestra el botón "Conectar" en la navbar del landing', async ({ page }) => {
const navBtn = page.getByRole('button', { name: /^conectar$/i })
const navBtn = page.locator('nav[aria-label="Navegación principal"]').getByRole('button', { name: /conectar/i })
await expect(navBtn).toBeVisible()
})

Expand Down
6 changes: 3 additions & 3 deletions bimex-frontend/src/stellar/walletKit.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { StellarWalletsKit, FreighterModule, WalletNetwork } from "@creit.tech/stellar-wallets-kit";
import { CONFIG } from "./contrato.js";

const _isMainnet = CONFIG.NETWORK === "mainnet";

let kit = null;

function initializeKit() {
Expand All @@ -17,8 +15,10 @@ function initializeKit() {
}).catch(() => {});
}

const isMainnet = CONFIG.NETWORK === "mainnet";

kit = new StellarWalletsKit({
network: _isMainnet ? WalletNetwork.PUBLIC : WalletNetwork.TESTNET,
network: isMainnet ? WalletNetwork.PUBLIC : WalletNetwork.TESTNET,
selectedWalletId: "freighter",
modules,
});
Expand Down
53 changes: 14 additions & 39 deletions bimex-frontend/src/test/ConectarWallet.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import ConectarWallet from "../components/ConectarWallet.jsx";
import {
getAddress,
getNetwork,
isAllowed,
isConnected,
requestAccess,
} from "@stellar/freighter-api";
import { getConnectedAddress, openWalletModal } from "../stellar/walletKit.js";
import "../i18n/index.js";

vi.mock("@stellar/freighter-api", () => ({
isConnected: vi.fn(),
isAllowed: vi.fn(),
requestAccess: vi.fn(),
getAddress: vi.fn(),
getNetwork: vi.fn(),
vi.mock("../stellar/walletKit.js", () => ({
getConnectedAddress: vi.fn(),
openWalletModal: vi.fn(),
}));

vi.mock("../stellar/contrato", () => ({
Expand All @@ -37,11 +28,8 @@ import { obtenerEstadoTrustlineMXNe, crearTrustlineMXNe } from "../stellar/contr

beforeEach(() => {
vi.clearAllMocks();
isConnected.mockResolvedValue({ isConnected: false });
isAllowed.mockResolvedValue({ isAllowed: false });
requestAccess.mockResolvedValue({});
getNetwork.mockResolvedValue({ networkPassphrase: "Test SDF Network ; September 2015" });
getAddress.mockResolvedValue({ address: "" });
getConnectedAddress.mockResolvedValue(null);
openWalletModal.mockResolvedValue(null);
obtenerEstadoTrustlineMXNe.mockResolvedValue({
aplica: true,
tieneTrustline: true,
Expand All @@ -60,15 +48,13 @@ describe("ConectarWallet", () => {
it("muestra el botón de conexión cuando no hay wallet conectada", () => {
render(<ConectarWallet autoConectar={false} />);

expect(screen.getByRole("button", { name: "Freighter" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Connect" })).toBeInTheDocument();
});

it("muestra la dirección truncada cuando ya existe una sesión autorizada", async () => {
const address = "GABC1234567890WXYZ";
const onConectado = vi.fn();
isConnected.mockResolvedValueOnce({ isConnected: true });
isAllowed.mockResolvedValueOnce({ isAllowed: true });
getAddress.mockResolvedValueOnce({ address });
getConnectedAddress.mockResolvedValueOnce(address);

render(<ConectarWallet onConectado={onConectado} />);

Expand All @@ -80,14 +66,11 @@ describe("ConectarWallet", () => {
it("llama onConectado con la dirección al conectar manualmente", async () => {
const address = "GDEF1234567890QRST";
const onConectado = vi.fn();
isConnected.mockResolvedValue({ isConnected: true });
requestAccess.mockResolvedValue({});
getNetwork.mockResolvedValue({ networkPassphrase: "Test SDF Network ; September 2015" });
getAddress.mockResolvedValue({ address });
openWalletModal.mockResolvedValueOnce(address);

render(<ConectarWallet autoConectar={false} onConectado={onConectado} />);

await userEvent.click(screen.getByRole("button", { name: "Freighter" }));
await userEvent.click(screen.getByRole("button", { name: "Connect" }));

await waitFor(() => {
expect(onConectado).toHaveBeenCalledWith(address);
Expand All @@ -97,9 +80,7 @@ describe("ConectarWallet", () => {

it("muestra banner cuando falta trustline MXNe", async () => {
const address = "GABC1234567890WXYZ";
isConnected.mockResolvedValueOnce({ isConnected: true });
isAllowed.mockResolvedValueOnce({ isAllowed: true });
getAddress.mockResolvedValueOnce({ address });
getConnectedAddress.mockResolvedValueOnce(address);
obtenerEstadoTrustlineMXNe.mockResolvedValueOnce({
aplica: true,
tieneTrustline: false,
Expand All @@ -117,9 +98,7 @@ describe("ConectarWallet", () => {

it("no muestra banner cuando ya tiene trustline", async () => {
const address = "GABC1234567890WXYZ";
isConnected.mockResolvedValueOnce({ isConnected: true });
isAllowed.mockResolvedValueOnce({ isAllowed: true });
getAddress.mockResolvedValueOnce({ address });
getConnectedAddress.mockResolvedValueOnce(address);

render(<ConectarWallet autoConectar />);

Expand All @@ -129,9 +108,7 @@ describe("ConectarWallet", () => {

it("muestra instrucciones de Friendbot cuando no hay XLM suficiente", async () => {
const address = "GABC1234567890WXYZ";
isConnected.mockResolvedValueOnce({ isConnected: true });
isAllowed.mockResolvedValueOnce({ isAllowed: true });
getAddress.mockResolvedValueOnce({ address });
getConnectedAddress.mockResolvedValueOnce(address);
obtenerEstadoTrustlineMXNe.mockResolvedValueOnce({
aplica: true,
tieneTrustline: false,
Expand All @@ -149,9 +126,7 @@ describe("ConectarWallet", () => {

it("crea trustline al hacer clic en habilitar", async () => {
const address = "GABC1234567890WXYZ";
isConnected.mockResolvedValueOnce({ isConnected: true });
isAllowed.mockResolvedValueOnce({ isAllowed: true });
getAddress.mockResolvedValueOnce({ address });
getConnectedAddress.mockResolvedValueOnce(address);
obtenerEstadoTrustlineMXNe.mockResolvedValueOnce({
aplica: true,
tieneTrustline: false,
Expand Down
5 changes: 5 additions & 0 deletions bimex-frontend/src/test/accessibility.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ vi.mock('passkey-kit', () => ({
PasskeyKit: class { sign = vi.fn() }
}));

vi.mock('../stellar/walletKit.js', () => ({
getConnectedAddress: vi.fn().mockResolvedValue(null),
openWalletModal: vi.fn().mockResolvedValue(null),
}));

import { render, waitFor } from '@testing-library/react';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
Expand Down
14 changes: 7 additions & 7 deletions bimex-frontend/src/test/contrato.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';

const mockLoadAccount = vi.fn();
const mockSubmitTransaction = vi.fn();
const mockSignTransaction = vi.fn();
const mockSignWithWalletKit = vi.fn();

vi.mock('@stellar/stellar-sdk', () => ({
Contract: vi.fn(),
Expand Down Expand Up @@ -42,7 +42,6 @@ vi.mock('@stellar/stellar-sdk', () => ({
}));

vi.mock('@stellar/freighter-api', () => ({
signTransaction: (...args) => mockSignTransaction(...args),
isConnected: vi.fn(),
isAllowed: vi.fn(),
requestAccess: vi.fn(),
Expand All @@ -55,6 +54,10 @@ vi.mock('passkey-kit', () => ({
PasskeyKit: class { sign = vi.fn() }
}));

vi.mock('../stellar/walletKit.js', () => ({
signWithWalletKit: (...args) => mockSignWithWalletKit(...args),
}));

import {
stroopsAMXNe,
mxneAStroops,
Expand Down Expand Up @@ -190,15 +193,12 @@ describe('trustline MXNe', () => {
sequenceNumber: () => '1',
});

mockSignTransaction.mockResolvedValueOnce({
signedTxXdr: 'signed-xdr',
error: null,
});
mockSignWithWalletKit.mockResolvedValueOnce('signed-xdr');
mockSubmitTransaction.mockResolvedValueOnce({ hash: 'trust-hash' });

const resultado = await crearTrustlineMXNe('GABC1234567890WXYZ');
expect(resultado.hash).toBe('trust-hash');
expect(mockSignTransaction).toHaveBeenCalled();
expect(mockSignWithWalletKit).toHaveBeenCalled();
expect(mockSubmitTransaction).toHaveBeenCalled();
});

Expand Down
9 changes: 8 additions & 1 deletion bimex-frontend/src/test/metaTags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,25 @@ describe("Bimex metadata", () => {
id: 42,
nombre: "Biblioteca Solar",
meta: 200000000n,
aportado: 100000000n,
estado: "Liberado",
});

aplicarMeta(meta);

expect(document.title).toBe("Biblioteca Solar — Bimex");
expect(document.head.querySelector('meta[property="og:title"]')?.content).toBe("Biblioteca Solar — Bimex");
expect(document.head.querySelector('meta[property="og:description"]')?.content).toBe("Meta: 20 MXNe · Liberado");
expect(document.head.querySelector('meta[property="og:description"]')?.content).toBe("10 MXNe recaudados · faltan 10 MXNe para la meta");
expect(document.head.querySelector('meta[property="og:url"]')?.content).toBe("https://bimex-frontend.vercel.app/proyectos/42");
expect(document.head.querySelector('meta[name="twitter:image"]')?.content).toBe("https://bimex-frontend.vercel.app/og-image.png");
});

it("defaults missing aportado/meta to zero without crashing on BigInt arithmetic", () => {
const meta = crearMetaProyecto({ id: 7, nombre: "Sin datos" });

expect(meta.description).toBe("0 MXNe recaudados · faltan 0 MXNe para la meta");
});

it("parses project IDs from share paths", () => {
expect(leerProyectoIdDesdePath("/proyectos/42")).toBe(42);
expect(leerProyectoIdDesdePath("/proyectos/42/")).toBe(42);
Expand Down
4 changes: 2 additions & 2 deletions bimex-frontend/src/test/stellar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { esDireccionValida, esContractIdValido } from '../utils/stellar.js';
// valid base32 charset) so StrKey can decode them properly.

// Valid Ed25519 public key (G-address) — 56 chars, correct checksum.
const DIRECCION_VALIDA = 'GAAHI26FKE4GGBHMMXFYUTGOXJB6ZGLHOMVQIBOIIZH2KFZJBVFCMVH';
const DIRECCION_VALIDA = 'GCWXHQPEOQEVUJ2NLIVNAOMYOJ5N5L4W753EPV7R4PU7WWNBSEJMZSU2';

// Valid Soroban contract ID (C-address) — 56 chars, correct checksum.
const CONTRACT_ID_VALIDO = 'CAHJJJKK6EXBRUXZGPQE5ZDDPVB5ZFJFUQKDNQZUBQLBQ6HB5PQHGJZ';
const CONTRACT_ID_VALIDO = 'CCWXHQPEOQEVUJ2NLIVNAOMYOJ5N5L4W753EPV7R4PU7WWNBSEJMYWRD';

describe('esDireccionValida', () => {
it('returns true for a valid G-address', () => {
Expand Down
6 changes: 4 additions & 2 deletions bimex-frontend/src/utils/metaTags.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ function setMetaTag(selector, attrs) {
export function crearMetaProyecto(proyecto) {
if (!proyecto) return DEFAULT_META;
const nombre = proyecto.nombre || "Proyecto Bimex";
const raised = stroopsAMXNe(proyecto.aportado ?? 0);
const remaining = stroopsAMXNe((proyecto.meta ?? 0) > (proyecto.aportado ?? 0) ? (proyecto.meta - proyecto.aportado) : 0n);
const meta = BigInt(proyecto.meta ?? 0);
const aportado = BigInt(proyecto.aportado ?? 0);
const raised = stroopsAMXNe(aportado);
const remaining = stroopsAMXNe(meta > aportado ? meta - aportado : 0n);
return {
title: `${nombre} — Bimex`,
description: `${raised} recaudados · faltan ${remaining} para la meta`,
Expand Down
Loading