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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 31 additions & 13 deletions miniapps/forge/e2e/ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { UI_TEXT } from './helpers/i18n'

const mockApiResponses = `
// Mock fetch for API calls
// Real endpoints: /cot/recharge/support, /cot/recharge/V2
const originalFetch = window.fetch
window.fetch = async (url, options) => {
if (url.includes('getSupport')) {
const urlStr = typeof url === 'string' ? url : url.toString()

// Match real endpoint: /cot/recharge/support
if (urlStr.includes('/cot/recharge/support') || urlStr.includes('/recharge/support')) {
return {
ok: true,
json: () => Promise.resolve({
Expand All @@ -24,7 +28,8 @@ const mockApiResponses = `
}),
}
}
if (url.includes('rechargeV2')) {
// Match real endpoint: /cot/recharge/V2
if (urlStr.includes('/cot/recharge/V2') || urlStr.includes('/recharge/V2')) {
return {
ok: true,
json: () => Promise.resolve({ orderId: 'order-123456' }),
Expand Down Expand Up @@ -53,7 +58,7 @@ const mockBioSDK = `
return { data: '0xsigned-tx-data-456' }
}
if (method === 'bio_signMessage') {
return 'signature-789'
return { signature: 'signature-789', publicKey: 'pubkey-abc123' }
}
return {}
}
Expand Down Expand Up @@ -123,8 +128,8 @@ test.describe('Forge UI', () => {
await page.click('button:has-text("ETH")')
await expect(page.locator(`text=${UI_TEXT.token.select.source}`).first()).toBeVisible()

// Should show available tokens
await expect(page.locator('text=Ethereum')).toBeVisible()
// Should show available tokens (use heading to be specific)
await expect(page.getByRole('heading', { name: 'Ethereum' })).toBeVisible()

await expect(page).toHaveScreenshot('04-token-picker.png')
})
Expand All @@ -150,14 +155,25 @@ test.describe('Forge UI', () => {
})

test('06 - error state without bio SDK', async ({ page }) => {
// No bio SDK mock
// Mock bio SDK that throws connection error
await page.addInitScript(`
window.bio = {
request: async ({ method }) => {
if (method === 'bio_closeSplashScreen') return {}
throw new Error('连接失败')
}
}
`)
await page.goto('/')
await page.waitForLoadState('networkidle')

await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click()
// Wait for button to be enabled (config loaded)
const connectBtn = page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first()
await expect(connectBtn).toBeEnabled({ timeout: 10000 })
await connectBtn.click()

// Should show error
await expect(page.locator(`text=${UI_TEXT.error.sdkNotInit.source}`)).toBeVisible({ timeout: 5000 })
// Should show error message (bio SDK throws error)
await expect(page.locator('text=连接失败')).toBeVisible({ timeout: 5000 })

await expect(page).toHaveScreenshot('06-error.png')
})
Expand Down Expand Up @@ -220,12 +236,14 @@ test.describe('Forge UI', () => {
await page.click('button:has-text("ETH")')
await expect(page.locator(`text=${UI_TEXT.token.select.source}`)).toBeVisible()

// Select different token (BNB on BSC)
const bnbOption = page.locator('text=BNB').first()
// Select different token (BNB → BFM option under BNB Chain)
const bnbOption = page.locator('text=BNB → BFM').first()
if (await bnbOption.isVisible()) {
await bnbOption.click()
// Picker should close and new token should be selected
await expect(page.locator('button:has-text("BNB")')).toBeVisible({ timeout: 5000 })
// Picker should close, wait a moment for state update
await page.waitForTimeout(500)
// Token selector should now show BNB
await expect(page.locator('button:has-text("BNB")').first()).toBeVisible({ timeout: 5000 })
}
})
})
6 changes: 5 additions & 1 deletion miniapps/forge/scripts/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ async function main() {
// Start vite dev server
const vite = spawn('pnpm', ['vite', '--port', String(port)], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env },
env: {
...process.env,
// E2E 测试使用 mock API,设置一个占位 base URL
VITE_COT_API_BASE_URL: process.env.VITE_COT_API_BASE_URL || 'https://e2e-mock.test',
},
})

let serverReady = false
Expand Down
24 changes: 21 additions & 3 deletions miniapps/forge/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
/**
* API Configuration
* TODO: Base URL needs to be confirmed with backend team
*
* IMPORTANT: Base URL must be configured via VITE_COT_API_BASE_URL environment variable.
* The COT Recharge API host has not been confirmed - do not use a hardcoded default.
*/

/** API Base URL - to be configured via environment or runtime */
export const API_BASE_URL = import.meta.env.VITE_COT_API_BASE_URL || 'https://api.eth-metaverse.com'
/** API Base URL - must be configured via environment variable */
function getApiBaseUrl(): string {
const url = import.meta.env.VITE_COT_API_BASE_URL
if (!url) {
// Fail-fast in development to catch missing configuration early
if (import.meta.env.DEV) {
console.error(
'[Forge API] VITE_COT_API_BASE_URL is not configured. ' +
'Please set this environment variable to the COT Recharge API base URL.'
)
}
// Return empty string - API calls will fail with clear error
return ''
}
return url
}

export const API_BASE_URL = getApiBaseUrl()

/** API Endpoints */
export const API_ENDPOINTS = {
Expand Down
10 changes: 5 additions & 5 deletions miniapps/forge/src/hooks/useForge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('useForge', () => {
mockBio.request
.mockResolvedValueOnce({ txHash: 'unsigned123' }) // bio_createTransaction
.mockResolvedValueOnce({ data: '0xsigned123' }) // bio_signTransaction
.mockResolvedValueOnce('signature123') // bio_signMessage
.mockResolvedValueOnce({ signature: 'signature123', publicKey: 'pubkey123' }) // bio_signMessage

vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order123' })

Expand All @@ -66,7 +66,7 @@ describe('useForge', () => {
expect(result.current.orderId).toBe('order123')
expect(result.current.error).toBeNull()

// Verify API calls
// Verify API calls (3 calls: createTx, signTx, signMessage)
expect(mockBio.request).toHaveBeenCalledTimes(3)
expect(rechargeApi.submitRecharge).toHaveBeenCalledTimes(1)
})
Expand Down Expand Up @@ -126,7 +126,7 @@ describe('useForge', () => {
mockBio.request
.mockResolvedValueOnce({ txHash: 'unsigned123' })
.mockResolvedValueOnce({ data: '0xsigned123' })
.mockResolvedValueOnce('signature123')
.mockResolvedValueOnce({ signature: 'signature123', publicKey: 'pubkey123' })

vi.mocked(rechargeApi.submitRecharge).mockRejectedValue(new Error('Server error'))

Expand Down Expand Up @@ -169,7 +169,7 @@ describe('useForge', () => {
mockBio.request
.mockResolvedValueOnce({ txHash: 'unsigned' })
.mockResolvedValueOnce({ data: '0xsignedEthTx' })
.mockResolvedValueOnce('sig')
.mockResolvedValueOnce({ signature: 'sig', publicKey: 'pubkey' })

vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order' })

Expand All @@ -192,7 +192,7 @@ describe('useForge', () => {
mockBio.request
.mockResolvedValueOnce({ txHash: 'unsigned' })
.mockResolvedValueOnce({ data: '0xsignedBscTx' })
.mockResolvedValueOnce('sig')
.mockResolvedValueOnce({ signature: 'sig', publicKey: 'pubkey' })

vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order' })

Expand Down
9 changes: 4 additions & 5 deletions miniapps/forge/src/hooks/useForge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,19 @@ export function useForge() {
timestamp: rechargeMessage.timestamp,
})

const signature = await window.bio.request<string>({
// bio_signMessage 返回 { signature, publicKey },publicKey 为 hex 格式
const signResult = await window.bio.request<{ signature: string; publicKey: string }>({
method: 'bio_signMessage',
params: [{
message: messageToSign,
address: internalAccount.address,
}],
})

// Build signature info
// Note: publicKey format needs to be confirmed with backend
const signatureInfo: SignatureInfo = {
timestamp: rechargeMessage.timestamp,
signature,
publicKey: internalAccount.address, // TODO: Get actual public key
signature: signResult.signature,
publicKey: signResult.publicKey,
}

// Step 3: Submit recharge request
Expand Down
1 change: 1 addition & 0 deletions miniapps/forge/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './index.css'
import './i18n'
import '@biochain/bio-sdk'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading