test: add full playwright ui regression coverage
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 52s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 32s
ui-regression / playwright-regression (push) Failing after 14m24s
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 52s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 32s
ui-regression / playwright-regression (push) Failing after 14m24s
This commit is contained in:
@@ -58,6 +58,24 @@ const defaultMetadataForm: MediaMetadataFormState = {
|
||||
notes: '',
|
||||
}
|
||||
|
||||
function normalizeMediaTags(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return value
|
||||
.filter((item): item is string => typeof item === 'string')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizeMediaItem(item: AdminMediaObjectResponse): AdminMediaObjectResponse {
|
||||
return {
|
||||
...item,
|
||||
tags: normalizeMediaTags(item.tags),
|
||||
}
|
||||
}
|
||||
|
||||
function toMetadataForm(item: AdminMediaObjectResponse | null): MediaMetadataFormState {
|
||||
if (!item) {
|
||||
return defaultMetadataForm
|
||||
@@ -67,7 +85,7 @@ function toMetadataForm(item: AdminMediaObjectResponse | null): MediaMetadataFor
|
||||
title: item.title ?? '',
|
||||
altText: item.alt_text ?? '',
|
||||
caption: item.caption ?? '',
|
||||
tags: item.tags.join(', '),
|
||||
tags: normalizeMediaTags(item.tags).join(', '),
|
||||
notes: item.notes ?? '',
|
||||
}
|
||||
}
|
||||
@@ -111,8 +129,9 @@ export function MediaPage() {
|
||||
}
|
||||
const prefix = prefixFilter === 'all' ? undefined : prefixFilter
|
||||
const result = await adminApi.listMediaObjects({ prefix, limit: 200 })
|
||||
const normalizedItems = result.items.map(normalizeMediaItem)
|
||||
startTransition(() => {
|
||||
setItems(result.items)
|
||||
setItems(normalizedItems)
|
||||
setProvider(result.provider)
|
||||
setBucket(result.bucket)
|
||||
})
|
||||
@@ -219,6 +238,7 @@ export function MediaPage() {
|
||||
<Button
|
||||
variant="danger"
|
||||
disabled={!selectedKeys.length || batchDeleting}
|
||||
data-testid="media-batch-delete"
|
||||
onClick={async () => {
|
||||
if (!window.confirm(`确定批量删除 ${selectedKeys.length} 个对象吗?`)) {
|
||||
return
|
||||
@@ -267,6 +287,7 @@ export function MediaPage() {
|
||||
<option value="uploads/">上传到通用目录</option>
|
||||
</Select>
|
||||
<Input
|
||||
data-testid="media-search"
|
||||
placeholder="按对象 key 搜索"
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
@@ -275,6 +296,7 @@ export function MediaPage() {
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-[1fr_auto_auto_auto]">
|
||||
<Input
|
||||
data-testid="media-upload-input"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
@@ -300,6 +322,7 @@ export function MediaPage() {
|
||||
/>
|
||||
<Button
|
||||
disabled={!uploadFiles.length || uploading}
|
||||
data-testid="media-upload"
|
||||
onClick={async () => {
|
||||
try {
|
||||
setUploading(true)
|
||||
@@ -399,6 +422,7 @@ export function MediaPage() {
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
disabled={metadataSaving}
|
||||
data-testid="media-save-metadata"
|
||||
onClick={async () => {
|
||||
if (!activeItem) {
|
||||
return
|
||||
@@ -423,7 +447,7 @@ export function MediaPage() {
|
||||
title: result.title,
|
||||
alt_text: result.alt_text,
|
||||
caption: result.caption,
|
||||
tags: result.tags,
|
||||
tags: normalizeMediaTags(result.tags),
|
||||
notes: result.notes,
|
||||
}
|
||||
: item,
|
||||
@@ -473,10 +497,12 @@ export function MediaPage() {
|
||||
{filteredItems.map((item, index) => {
|
||||
const selected = selectedKeys.includes(item.key)
|
||||
const replaceInputId = `replace-media-${index}`
|
||||
const itemTags = normalizeMediaTags(item.tags)
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={item.key}
|
||||
data-testid={`media-item-${index}`}
|
||||
className={`overflow-hidden ${activeKey === item.key ? 'ring-1 ring-primary/40' : ''}`}
|
||||
>
|
||||
<div className="relative aspect-[16/9] overflow-hidden bg-muted/30">
|
||||
@@ -504,9 +530,9 @@ export function MediaPage() {
|
||||
{item.last_modified ? <span>{item.last_modified}</span> : null}
|
||||
</div>
|
||||
{item.title ? <p className="text-sm text-foreground">{item.title}</p> : null}
|
||||
{item.tags.length ? (
|
||||
{itemTags.length ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.tags.slice(0, 4).map((tag) => (
|
||||
{itemTags.slice(0, 4).map((tag) => (
|
||||
<Badge key={`${item.key}-${tag}`} variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
@@ -515,7 +541,12 @@ export function MediaPage() {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => setActiveKey(item.key)}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setActiveKey(item.key)}
|
||||
data-testid={`media-edit-${index}`}
|
||||
>
|
||||
元数据
|
||||
</Button>
|
||||
<Button
|
||||
@@ -541,6 +572,7 @@ export function MediaPage() {
|
||||
</Button>
|
||||
<input
|
||||
id={replaceInputId}
|
||||
data-testid={`media-replace-input-${index}`}
|
||||
className="hidden"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@@ -583,6 +615,7 @@ export function MediaPage() {
|
||||
size="sm"
|
||||
variant="danger"
|
||||
disabled={deletingKey === item.key || replacingKey === item.key}
|
||||
data-testid={`media-delete-${index}`}
|
||||
onClick={async () => {
|
||||
if (!window.confirm(`确定删除 ${item.key} 吗?`)) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user