Rust 新增 s3.rs:SigV4 签名 + PUT Object(reqwest blocking+rustls, 签名对照 AWS 官方示例向量逐字一致;同份代码对本地模拟 S3 端到端 实测通过)。命令 s3_upload_image / s3_test_connection(总数 30 对账)。 配置存 UiState.s3(本机偏好),设置弹窗含测试连接与自动上传开关。 未上传判定 = src 非 http/data/blob 的本地引用;上传成功后同 src 的 图片节点一次事务回换为 S3 地址,本地 .assets 文件保留。 入口:粘贴/插入后自动上传回换(扑空重试一次、失败必提示);右键 命中本地图片多一项「上传到 S3」;文件菜单 + 状态栏批量上传带进度 弹窗。验证:vitest 133+2、tsc、构建通过,设置弹窗截图核对。
50 lines
2.3 KiB
TypeScript
50 lines
2.3 KiB
TypeScript
// S3 设置弹窗(docs/30):字段回填、编辑后保存的载荷、自动上传开关。
|
||
import { describe, it, expect, vi } from 'vitest'
|
||
import { createRoot } from 'react-dom/client'
|
||
import { act } from 'react-dom/test-utils'
|
||
import { S3SettingsModal } from '../src/components/S3SettingsModal'
|
||
import { DEFAULT_S3_SETTINGS } from '../src/services/s3'
|
||
|
||
function setInput(input: HTMLInputElement, value: string) {
|
||
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
|
||
setter?.call(input, value)
|
||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||
}
|
||
|
||
describe('S3SettingsModal', () => {
|
||
it('填写桶/区域/密钥与自动上传开关后,保存回传完整设置', async () => {
|
||
const container = document.createElement('div')
|
||
document.body.appendChild(container)
|
||
const root = createRoot(container)
|
||
const onSave = vi.fn()
|
||
await act(async () => {
|
||
root.render(<S3SettingsModal initial={DEFAULT_S3_SETTINGS} onSave={onSave} onClose={() => undefined} />)
|
||
})
|
||
const inputs = [...container.querySelectorAll('input')]
|
||
const byPlaceholder = (ph: string) => inputs.find((i) => i.placeholder === ph) as HTMLInputElement
|
||
await act(async () => {
|
||
setInput(byPlaceholder('us-east-1'), 'ap-northeast-1')
|
||
setInput(byPlaceholder('my-images'), 'my-images')
|
||
setInput(byPlaceholder('notes/'), 'blog/')
|
||
})
|
||
const textInputs = inputs.filter((i) => i.type === 'text' || i.type === 'password')
|
||
// Access Key / Secret Key 是无 placeholder 的两个输入(第 4、5 个字段)
|
||
await act(async () => {
|
||
setInput(textInputs[3], 'AKID')
|
||
setInput(textInputs[4], 'SECRET')
|
||
})
|
||
const checkbox = inputs.find((i) => i.type === 'checkbox') as HTMLInputElement
|
||
await act(async () => { checkbox.click() })
|
||
const saveBtn = [...container.querySelectorAll('button')].find((b) => b.textContent === '保存') as HTMLButtonElement
|
||
await act(async () => { saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
|
||
expect(onSave).toHaveBeenCalledTimes(1)
|
||
const saved = onSave.mock.calls[0][0]
|
||
expect(saved).toMatchObject({
|
||
region: 'ap-northeast-1', bucket: 'my-images', accessKey: 'AKID', secretKey: 'SECRET',
|
||
prefix: 'blog/', autoUpload: true,
|
||
})
|
||
await act(async () => { root.unmount() })
|
||
container.remove()
|
||
})
|
||
})
|