Skip to content

Commit acfcf35

Browse files
committed
test: add PendingTxList storybook stories and e2e screenshots
- Add pending-tx-list.stories.tsx with 6 stories (Default, Broadcasting, Broadcasted, Failed, Empty, MultipleStates) - Add storybook-e2e/pending-tx-list.spec.ts for visual regression testing - Generate 5 screenshots for different pending tx states - All storybook tests passing (6 tests)
1 parent 82f32d2 commit acfcf35

7 files changed

Lines changed: 343 additions & 0 deletions
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import { PendingTxList } from './pending-tx-list'
3+
import type { PendingTx } from '@/services/transaction'
4+
5+
const meta: Meta<typeof PendingTxList> = {
6+
title: 'Transaction/PendingTxList',
7+
component: PendingTxList,
8+
tags: ['autodocs'],
9+
parameters: {
10+
layout: 'padded',
11+
},
12+
decorators: [
13+
(Story) => (
14+
<div className="max-w-md">
15+
<Story />
16+
</div>
17+
),
18+
],
19+
}
20+
21+
export default meta
22+
type Story = StoryObj<typeof PendingTxList>
23+
24+
const now = Date.now()
25+
26+
const mockPendingTxs: PendingTx[] = [
27+
{
28+
id: 'tx-1',
29+
walletId: 'wallet-1',
30+
chainId: 'bfmeta',
31+
fromAddress: 'bXXX1234567890',
32+
status: 'broadcasting',
33+
rawTx: { signature: 'sig-1' },
34+
meta: {
35+
type: 'transfer',
36+
displayAmount: '100.5',
37+
displaySymbol: 'BFM',
38+
displayToAddress: 'bYYY0987654321',
39+
},
40+
retryCount: 0,
41+
createdAt: now - 30000,
42+
updatedAt: now - 10000,
43+
},
44+
{
45+
id: 'tx-2',
46+
walletId: 'wallet-1',
47+
chainId: 'bfmeta',
48+
fromAddress: 'bXXX1234567890',
49+
status: 'broadcasted',
50+
txHash: '0xabc123def456...',
51+
rawTx: { signature: 'sig-2' },
52+
meta: {
53+
type: 'transfer',
54+
displayAmount: '50',
55+
displaySymbol: 'CPCC',
56+
displayToAddress: 'bZZZ1122334455',
57+
},
58+
retryCount: 0,
59+
createdAt: now - 120000,
60+
updatedAt: now - 60000,
61+
},
62+
{
63+
id: 'tx-3',
64+
walletId: 'wallet-1',
65+
chainId: 'bfmeta',
66+
fromAddress: 'bXXX1234567890',
67+
status: 'failed',
68+
rawTx: { signature: 'sig-3' },
69+
meta: {
70+
type: 'transfer',
71+
displayAmount: '200',
72+
displaySymbol: 'BFM',
73+
displayToAddress: 'bAAA9988776655',
74+
},
75+
errorCode: '001-11028',
76+
errorMessage: '资产余额不足',
77+
retryCount: 2,
78+
createdAt: now - 300000,
79+
updatedAt: now - 180000,
80+
},
81+
]
82+
83+
export const Default: Story = {
84+
args: {
85+
transactions: mockPendingTxs,
86+
onRetry: (tx) => console.log('Retry:', tx.id),
87+
onDelete: (tx) => console.log('Delete:', tx.id),
88+
},
89+
}
90+
91+
export const Broadcasting: Story = {
92+
args: {
93+
transactions: [mockPendingTxs[0]],
94+
onRetry: (tx) => console.log('Retry:', tx.id),
95+
onDelete: (tx) => console.log('Delete:', tx.id),
96+
},
97+
}
98+
99+
export const Broadcasted: Story = {
100+
args: {
101+
transactions: [mockPendingTxs[1]],
102+
onRetry: (tx) => console.log('Retry:', tx.id),
103+
onDelete: (tx) => console.log('Delete:', tx.id),
104+
},
105+
}
106+
107+
export const Failed: Story = {
108+
args: {
109+
transactions: [mockPendingTxs[2]],
110+
onRetry: (tx) => console.log('Retry:', tx.id),
111+
onDelete: (tx) => console.log('Delete:', tx.id),
112+
},
113+
}
114+
115+
export const Empty: Story = {
116+
args: {
117+
transactions: [],
118+
onRetry: (tx) => console.log('Retry:', tx.id),
119+
onDelete: (tx) => console.log('Delete:', tx.id),
120+
},
121+
}
122+
123+
export const MultipleStates: Story = {
124+
args: {
125+
transactions: [
126+
{
127+
...mockPendingTxs[0],
128+
id: 'tx-broadcasting',
129+
status: 'broadcasting',
130+
},
131+
{
132+
...mockPendingTxs[1],
133+
id: 'tx-broadcasted',
134+
status: 'broadcasted',
135+
},
136+
{
137+
...mockPendingTxs[2],
138+
id: 'tx-failed-1',
139+
status: 'failed',
140+
errorMessage: '资产余额不足',
141+
},
142+
{
143+
...mockPendingTxs[2],
144+
id: 'tx-failed-2',
145+
status: 'failed',
146+
errorMessage: '手续费不足',
147+
meta: {
148+
type: 'burn',
149+
displayAmount: '1000',
150+
displaySymbol: 'BFM',
151+
},
152+
},
153+
],
154+
onRetry: (tx) => console.log('Retry:', tx.id),
155+
onDelete: (tx) => console.log('Delete:', tx.id),
156+
},
157+
}
11.7 KB
Loading
11.6 KB
Loading
13.8 KB
Loading
24.7 KB
Loading
30.5 KB
Loading
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { expect, test } from '@playwright/test'
2+
import { createReadStream } from 'node:fs'
3+
import { stat } from 'node:fs/promises'
4+
import { createServer, type Server } from 'node:http'
5+
import path from 'node:path'
6+
import { fileURLToPath } from 'node:url'
7+
8+
const storybookStaticDir = path.resolve(
9+
path.dirname(fileURLToPath(import.meta.url)),
10+
'../storybook-static'
11+
)
12+
13+
function getContentType(filePath: string): string {
14+
const ext = path.extname(filePath).toLowerCase()
15+
switch (ext) {
16+
case '.html':
17+
return 'text/html; charset=utf-8'
18+
case '.js':
19+
return 'text/javascript; charset=utf-8'
20+
case '.css':
21+
return 'text/css; charset=utf-8'
22+
case '.json':
23+
return 'application/json; charset=utf-8'
24+
case '.svg':
25+
return 'image/svg+xml'
26+
case '.png':
27+
return 'image/png'
28+
case '.jpg':
29+
case '.jpeg':
30+
return 'image/jpeg'
31+
case '.webp':
32+
return 'image/webp'
33+
case '.ico':
34+
return 'image/x-icon'
35+
case '.woff':
36+
return 'font/woff'
37+
case '.woff2':
38+
return 'font/woff2'
39+
case '.ttf':
40+
return 'font/ttf'
41+
case '.map':
42+
return 'application/json; charset=utf-8'
43+
default:
44+
return 'application/octet-stream'
45+
}
46+
}
47+
48+
async function startStorybookServer(): Promise<{ server: Server; baseUrl: string }> {
49+
await stat(path.join(storybookStaticDir, 'index.html'))
50+
51+
const server = createServer(async (req, res) => {
52+
const url = new URL(req.url ?? '/', 'http://localhost')
53+
const rawPath = decodeURIComponent(url.pathname)
54+
const relativePath = rawPath === '/' ? '/index.html' : rawPath
55+
const filePath = path.normalize(path.join(storybookStaticDir, relativePath))
56+
57+
if (!filePath.startsWith(storybookStaticDir)) {
58+
res.statusCode = 403
59+
res.end('Forbidden')
60+
return
61+
}
62+
63+
try {
64+
const stats = await stat(filePath)
65+
if (!stats.isFile()) {
66+
res.statusCode = 404
67+
res.end('Not found')
68+
return
69+
}
70+
71+
res.statusCode = 200
72+
res.setHeader('Content-Type', getContentType(filePath))
73+
createReadStream(filePath).pipe(res)
74+
} catch {
75+
res.statusCode = 404
76+
res.end('Not found')
77+
}
78+
})
79+
80+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()))
81+
const address = server.address()
82+
if (!address || typeof address === 'string') throw new Error('Unexpected server address')
83+
84+
return { server, baseUrl: `http://127.0.0.1:${address.port}` }
85+
}
86+
87+
function storyUrl(baseUrl: string, storyId: string): string {
88+
return `${baseUrl}/?path=/story/${storyId}`
89+
}
90+
91+
test.describe('PendingTxList Storybook', () => {
92+
let server: Server
93+
let baseUrl: string
94+
95+
test.beforeAll(async () => {
96+
const started = await startStorybookServer()
97+
server = started.server
98+
baseUrl = started.baseUrl
99+
})
100+
101+
test.afterAll(async () => {
102+
await new Promise<void>((resolve, reject) =>
103+
server.close((err) => (err ? reject(err) : resolve()))
104+
)
105+
})
106+
107+
test('Default renders multiple pending transactions', async ({ page }) => {
108+
const frame = page.frameLocator('#storybook-preview-iframe')
109+
110+
await page.goto(storyUrl(baseUrl, 'transaction-pendingtxlist--default'))
111+
112+
// Wait for the pending tx list to be visible
113+
await expect(frame.getByText('待处理交易')).toBeVisible({ timeout: 10000 })
114+
115+
// Check that transactions are visible
116+
await expect(frame.getByText('100.5 BFM')).toBeVisible()
117+
await expect(frame.getByText('50 CPCC')).toBeVisible()
118+
await expect(frame.getByText('200 BFM')).toBeVisible()
119+
120+
// Take screenshot
121+
await expect(frame.locator('#storybook-root')).toHaveScreenshot('pending-tx-list-default.png')
122+
})
123+
124+
test('Broadcasting state shows spinner animation', async ({ page }) => {
125+
const frame = page.frameLocator('#storybook-preview-iframe')
126+
127+
await page.goto(storyUrl(baseUrl, 'transaction-pendingtxlist--broadcasting'))
128+
129+
// Wait for the pending tx list to be visible
130+
await expect(frame.getByText('待处理交易')).toBeVisible({ timeout: 10000 })
131+
132+
// Check broadcasting state
133+
await expect(frame.getByText('广播中')).toBeVisible()
134+
135+
// Take screenshot
136+
await expect(frame.locator('#storybook-root')).toHaveScreenshot('pending-tx-broadcasting.png')
137+
})
138+
139+
test('Broadcasted state shows waiting confirmation', async ({ page }) => {
140+
const frame = page.frameLocator('#storybook-preview-iframe')
141+
142+
await page.goto(storyUrl(baseUrl, 'transaction-pendingtxlist--broadcasted'))
143+
144+
// Wait for the pending tx list to be visible
145+
await expect(frame.getByText('待处理交易')).toBeVisible({ timeout: 10000 })
146+
147+
// Check broadcasted state
148+
await expect(frame.getByText('等待上链')).toBeVisible()
149+
150+
// Take screenshot
151+
await expect(frame.locator('#storybook-root')).toHaveScreenshot('pending-tx-broadcasted.png')
152+
})
153+
154+
test('Failed state shows error message and retry button', async ({ page }) => {
155+
const frame = page.frameLocator('#storybook-preview-iframe')
156+
157+
await page.goto(storyUrl(baseUrl, 'transaction-pendingtxlist--failed'))
158+
159+
// Wait for the pending tx list to be visible
160+
await expect(frame.getByText('待处理交易')).toBeVisible({ timeout: 10000 })
161+
162+
// Check failed state
163+
await expect(frame.getByText('广播失败')).toBeVisible()
164+
await expect(frame.getByText('资产余额不足')).toBeVisible()
165+
166+
// Take screenshot
167+
await expect(frame.locator('#storybook-root')).toHaveScreenshot('pending-tx-failed.png')
168+
})
169+
170+
test('MultipleStates shows all status types', async ({ page }) => {
171+
const frame = page.frameLocator('#storybook-preview-iframe')
172+
173+
await page.goto(storyUrl(baseUrl, 'transaction-pendingtxlist--multiple-states'))
174+
175+
// Wait for the pending tx list to be visible
176+
await expect(frame.getByText('待处理交易')).toBeVisible({ timeout: 10000 })
177+
178+
// Check all states are visible
179+
await expect(frame.getByText('广播中')).toBeVisible()
180+
await expect(frame.getByText('等待上链')).toBeVisible()
181+
await expect(frame.getByText('广播失败').first()).toBeVisible()
182+
183+
// Take screenshot
184+
await expect(frame.locator('#storybook-root')).toHaveScreenshot('pending-tx-multiple-states.png')
185+
})
186+
})

0 commit comments

Comments
 (0)