fix: harden auto uploads and table layouts
This commit is contained in:
@@ -2,6 +2,7 @@ import { defineConfig } from "@playwright/test";
|
||||
|
||||
const viewports = [
|
||||
{ name: "desktop-1920", width: 1920, height: 1080 },
|
||||
{ name: "desktop-1552", width: 1552, height: 761 },
|
||||
{ name: "desktop-1366", width: 1366, height: 768 },
|
||||
{ name: "tablet-1024", width: 1024, height: 768 },
|
||||
{ name: "tablet-768", width: 768, height: 1024 },
|
||||
@@ -21,7 +22,8 @@ export default defineConfig({
|
||||
trace: "retain-on-failure"
|
||||
},
|
||||
webServer: {
|
||||
command: "VITE_DISABLE_DEVTOOLS=1 npm run dev -- --host 127.0.0.1 --port 47173 --strictPort",
|
||||
command: "npm run dev -- --host 127.0.0.1 --port 47173 --strictPort",
|
||||
env: { VITE_DISABLE_DEVTOOLS: "1" },
|
||||
url: "http://127.0.0.1:47173",
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000
|
||||
|
||||
@@ -355,19 +355,24 @@ button { font-family: inherit; cursor: pointer; }
|
||||
|
||||
/* tables */
|
||||
.el-table {
|
||||
--el-table-border-color: transparent; --el-table-header-bg-color: transparent;
|
||||
--el-table-bg-color: transparent; --el-table-row-hover-bg-color: var(--surface-hover);
|
||||
--el-table-current-row-bg-color: var(--accent-soft); --el-fill-color-blank: transparent;
|
||||
background: transparent;
|
||||
--el-table-border-color: transparent; --el-table-header-bg-color: var(--surface-muted);
|
||||
--el-table-bg-color: var(--surface); --el-table-tr-bg-color: var(--surface);
|
||||
--el-table-row-hover-bg-color: var(--surface-hover);
|
||||
--el-table-current-row-bg-color: var(--accent-soft); --el-fill-color-blank: var(--surface);
|
||||
background: var(--surface);
|
||||
}
|
||||
.el-table::before, .el-table__inner-wrapper::before { display: none; }
|
||||
.el-table th.el-table__cell { padding: 11px 16px; border-bottom: 1px solid var(--border-subtle); background: var(--surface-muted); }
|
||||
html[data-theme="dark"] .el-table th.el-table__cell { background: var(--surface-muted); }
|
||||
.el-table th.el-table__cell > .cell { color: var(--text-muted); font-size: 12px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; }
|
||||
.el-table td.el-table__cell { padding: 13px 16px; border-bottom: 1px solid var(--border-subtle); background: transparent; }
|
||||
.el-table tr { background: transparent; }
|
||||
.el-table tbody tr:nth-child(even) { background: rgba(15, 23, 42, 0.018); }
|
||||
html[data-theme="dark"] .el-table tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.02); }
|
||||
.el-table td.el-table__cell { padding: 13px 16px; border-bottom: 1px solid var(--border-subtle); background: var(--surface); }
|
||||
.el-table tr { background: var(--surface); }
|
||||
.el-table tbody tr:nth-child(even) > td.el-table__cell { background: color-mix(in srgb, var(--surface-strong) 24%, var(--surface)); }
|
||||
.el-table tbody tr:hover > td.el-table__cell { background: var(--surface-hover); }
|
||||
.el-table .el-table-fixed-column--right,
|
||||
.el-table .el-table-fixed-column--left { background: inherit; }
|
||||
.el-table .el-table-fixed-column--right.is-first-column { box-shadow: -10px 0 18px -16px rgba(23, 35, 58, .55); }
|
||||
html[data-theme="dark"] .el-table .el-table-fixed-column--right.is-first-column { box-shadow: -10px 0 20px -15px rgba(0, 0, 0, .9); }
|
||||
.premium-table { width: 100%; }
|
||||
|
||||
/* descriptions */
|
||||
|
||||
@@ -653,6 +653,15 @@ export interface OpenListConnectionTestResult {
|
||||
success: boolean;
|
||||
version?: string;
|
||||
message: string;
|
||||
sourcePath?: OpenListPathCheck;
|
||||
destinationPath?: OpenListPathCheck;
|
||||
}
|
||||
|
||||
export interface OpenListPathCheck {
|
||||
path: string;
|
||||
success: boolean;
|
||||
canWrite: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface OpenListDirectoryItem {
|
||||
|
||||
@@ -53,6 +53,7 @@ const changingPassword = ref(false);
|
||||
const testingEmail = ref(false);
|
||||
const testingWebhook = ref(false);
|
||||
const testingOpenList = ref(false);
|
||||
const openListTestResult = ref<OpenListConnectionTestResult | null>(null);
|
||||
const openListDirectoryLoading = ref(false);
|
||||
const openListDirectoryDialogVisible = ref(false);
|
||||
const openListDirectoryPickerTarget = ref<"source" | "destination">("source");
|
||||
@@ -568,7 +569,9 @@ function buildOpenListConnectionPayload() {
|
||||
return {
|
||||
baseUrl: form.openListUpload.baseUrl,
|
||||
username: form.openListUpload.username,
|
||||
password: form.openListUpload.password
|
||||
password: form.openListUpload.password,
|
||||
sourcePath: form.openListUpload.sourcePath,
|
||||
destinationPath: form.openListUpload.destinationPath
|
||||
};
|
||||
}
|
||||
|
||||
@@ -580,8 +583,10 @@ async function testOpenListConnection() {
|
||||
"/settings/openlist/test",
|
||||
buildOpenListConnectionPayload()
|
||||
);
|
||||
openListTestResult.value = data;
|
||||
ElMessage[data.success ? "success" : "warning"](data.message);
|
||||
} catch (error) {
|
||||
openListTestResult.value = null;
|
||||
ElMessage.error(getApiErrorMessage(error, "OpenList 连接测试失败"));
|
||||
} finally {
|
||||
testingOpenList.value = false;
|
||||
@@ -1646,6 +1651,29 @@ onBeforeRouteLeave(async () => {
|
||||
<el-button :loading="testingOpenList" @click="testOpenListConnection">测试连接</el-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="openListTestResult"
|
||||
class="openlist-test-result"
|
||||
:class="{ 'openlist-test-result--failed': !openListTestResult.success }"
|
||||
>
|
||||
<div class="openlist-test-result__summary">
|
||||
<el-tag :type="openListTestResult.success ? 'success' : 'warning'">
|
||||
{{ openListTestResult.success ? "配置可用" : "配置异常" }}
|
||||
</el-tag>
|
||||
<span>{{ openListTestResult.message }}</span>
|
||||
</div>
|
||||
<div v-if="openListTestResult.sourcePath" class="openlist-test-result__path">
|
||||
<el-tag :type="openListTestResult.sourcePath.success ? 'success' : 'danger'" size="small">源目录</el-tag>
|
||||
<code>{{ openListTestResult.sourcePath.path }}</code>
|
||||
<span>{{ openListTestResult.sourcePath.message }}</span>
|
||||
</div>
|
||||
<div v-if="openListTestResult.destinationPath" class="openlist-test-result__path">
|
||||
<el-tag :type="openListTestResult.destinationPath.success ? 'success' : 'danger'" size="small">目标目录</el-tag>
|
||||
<code>{{ openListTestResult.destinationPath.path }}</code>
|
||||
<span>{{ openListTestResult.destinationPath.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
@@ -1965,6 +1993,14 @@ onBeforeRouteLeave(async () => {
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="form.enableFileUpload && form.enableAutoUpload && form.enableSegmentCompletedScript"
|
||||
class="event-script-conflict-alert"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="内置自动上传与分片完成脚本会独立执行。请勿在该脚本中重复实现上传,以免重复提交或因脚本故障干扰完成事件。"
|
||||
/>
|
||||
<el-form-item v-if="form.segmentCompletedScriptMode === 'path'" label="脚本路径">
|
||||
<el-input v-model="form.segmentCompletedScriptPath" placeholder="/app/scripts/segment-completed.sh" />
|
||||
</el-form-item>
|
||||
@@ -2828,6 +2864,41 @@ onBeforeRouteLeave(async () => {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.openlist-test-result {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--success) 30%, var(--border-subtle));
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--success-soft);
|
||||
}
|
||||
|
||||
.openlist-test-result--failed {
|
||||
border-color: color-mix(in srgb, var(--warning) 34%, var(--border-subtle));
|
||||
background: var(--warning-soft);
|
||||
}
|
||||
|
||||
.openlist-test-result__summary,
|
||||
.openlist-test-result__path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.openlist-test-result__path code {
|
||||
color: var(--text-primary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.event-script-conflict-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.template-help {
|
||||
margin-top: 8px;
|
||||
padding: 16px 18px;
|
||||
|
||||
@@ -377,6 +377,13 @@ test("live room table, drawer and dialog retain their final actions", async ({ p
|
||||
await expect(actionHeader).toBeVisible();
|
||||
const box = await actionHeader.boundingBox();
|
||||
expect(box && box.x + box.width).toBeLessThanOrEqual((page.viewportSize()?.width ?? 0) + 1);
|
||||
const fixedActionCell = page.locator("td.el-table-fixed-column--right").first();
|
||||
await expect(fixedActionCell).toBeVisible();
|
||||
const fixedBackground = await fixedActionCell.evaluate((element) =>
|
||||
window.getComputedStyle(element).backgroundColor
|
||||
);
|
||||
expect(fixedBackground).not.toBe("transparent");
|
||||
expect(fixedBackground).not.toMatch(/rgba\([^)]*,\s*0\s*\)$/);
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: /^查看/ }).first().click();
|
||||
@@ -677,7 +684,7 @@ test("recording failure recovery adapts its actions and long paths to each viewp
|
||||
await expect(page.getByText(recordingFailure.filePath, { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "确认有效" })).toBeVisible();
|
||||
|
||||
if ((page.viewportSize()?.width ?? 0) <= 767) {
|
||||
if ((page.viewportSize()?.width ?? 0) <= 768) {
|
||||
await expect(page.locator(".failure-card")).toHaveCount(1);
|
||||
await expect(page.getByRole("columnheader", { name: "操作" })).toHaveCount(0);
|
||||
} else {
|
||||
@@ -794,3 +801,49 @@ test("settings layer advanced controls without clipping the quick bar", async ({
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "settings");
|
||||
});
|
||||
|
||||
test("OpenList test distinguishes login success from a broken destination mount", async ({ page }) => {
|
||||
await page.route("**/api/settings", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enableFileUpload: true,
|
||||
enableAutoUpload: true,
|
||||
uploadTarget: 3,
|
||||
openListUpload: {
|
||||
baseUrl: "https://openlist.example.com",
|
||||
username: "tester",
|
||||
password: "secret",
|
||||
basePath: "/archive",
|
||||
sourcePath: "/source",
|
||||
destinationPath: "/archive"
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
await page.route("**/api/settings/openlist/test", async (route) => {
|
||||
const request = route.request().postDataJSON();
|
||||
expect(request.sourcePath).toBe("/source");
|
||||
expect(request.destinationPath).toBe("/archive");
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
version: "4.1.10",
|
||||
message: "OpenList 登录成功,但一个或多个配置目录不可用。",
|
||||
sourcePath: { path: "/source", success: true, canWrite: false, message: "目录可访问。" },
|
||||
destinationPath: { path: "/archive", success: false, canWrite: false, message: "provider timeout" }
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/settings/upload");
|
||||
await expect(page.getByRole("heading", { name: "OpenList 服务端复制" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "测试连接" }).click();
|
||||
await expect(page.getByText("配置异常", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("目录可访问。", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("provider timeout", { exact: true })).toBeVisible();
|
||||
await expectNoDocumentOverflow(page);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user