补齐管理控制台安全退出入口及会话交互回归测试
This commit is contained in:
parent
5f4ca12d6c
commit
0175d7b41e
@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import "./DeploymentResult.test.js";
|
||||
import "./DashboardLogout.test.js";
|
||||
import test from "node:test";
|
||||
import { nextTick, ref } from "vue";
|
||||
import { useDashboard } from "./Dashboard.js";
|
||||
|
||||
100
frontend/src/views/Dashboard/DashboardLogout.test.js
Normal file
100
frontend/src/views/Dashboard/DashboardLogout.test.js
Normal file
@ -0,0 +1,100 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import * as Vue from "vue";
|
||||
import { compileScript, compileTemplate, parse } from "@vue/compiler-sfc";
|
||||
import { renderToString } from "@vue/server-renderer";
|
||||
import { useAccessDenied } from "../AccessDenied/AccessDenied.js";
|
||||
import { deferred, mountComposable } from "../../test-support/mount.js";
|
||||
|
||||
test("管理侧栏始终提供退出入口,并展示忙碌和失败状态", async () => {
|
||||
const source = await readFile(new URL("./DashboardNavigation.vue", import.meta.url), "utf8");
|
||||
const { descriptor, errors } = parse(source);
|
||||
assert.deepEqual(errors, []);
|
||||
assert.ok(compileScript(descriptor, { id: "dashboard-logout" }));
|
||||
assert.match(descriptor.scriptSetup.content, /const\s*\{\s*loggingOut,\s*error,\s*logout\s*\}\s*=\s*useAccessDenied\(\)/);
|
||||
assert.match(descriptor.template.content, /@click="logout"/);
|
||||
const compiled = compileTemplate({
|
||||
source: descriptor.template.content,
|
||||
filename: "DashboardNavigation.vue",
|
||||
id: "dashboard-logout",
|
||||
compilerOptions: { mode: "function" },
|
||||
});
|
||||
assert.deepEqual(compiled.errors, []);
|
||||
const render = new Function("Vue", compiled.code)(Vue);
|
||||
for (const state of [
|
||||
{ loggingOut: false, error: "" },
|
||||
{ loggingOut: true, error: "" },
|
||||
{ loggingOut: false, error: "退出登录失败" },
|
||||
]) {
|
||||
const html = await renderToString(Vue.createSSRApp({
|
||||
setup: () => ({ activeSection: "overview", sections: [], logout() {}, ...state }),
|
||||
render,
|
||||
}));
|
||||
assert.match(html, /class="navigation-session"/);
|
||||
assert.match(html, state.loggingOut ? /退出中/ : /退出登录/);
|
||||
const button = html.match(/<button[^>]*navigation-logout[^>]*>/)?.[0];
|
||||
assert.ok(button);
|
||||
assert.equal(/\sdisabled(?:[\s=>])/.test(button), state.loggingOut);
|
||||
if (state.error) assert.match(html, /role="alert"[^>]*>退出登录失败/);
|
||||
else assert.doesNotMatch(html, /role="alert"/);
|
||||
}
|
||||
});
|
||||
|
||||
test("退出登录携带会话且成功后跳转登录页", async (t) => {
|
||||
const calls = [];
|
||||
const { state } = await mountComposable(t, useAccessDenied, async (...args) => {
|
||||
calls.push(args);
|
||||
return Response.json({ success: true });
|
||||
});
|
||||
const redirect = t.mock.method(window.location, "assign");
|
||||
await state.logout();
|
||||
assert.deepEqual(calls, [["/api/v1/auth/logout", { method: "POST", credentials: "include" }]]);
|
||||
assert.equal(redirect.mock.callCount(), 1);
|
||||
assert.deepEqual(redirect.mock.calls[0].arguments, ["/login"]);
|
||||
assert.equal(state.loggingOut.value, false);
|
||||
assert.equal(state.error.value, "");
|
||||
});
|
||||
|
||||
test("退出登录请求期间阻止重复提交", async (t) => {
|
||||
const pending = deferred();
|
||||
let calls = 0;
|
||||
const { state } = await mountComposable(t, useAccessDenied, () => {
|
||||
calls += 1;
|
||||
return pending.promise;
|
||||
});
|
||||
const first = state.logout();
|
||||
assert.equal(state.loggingOut.value, true);
|
||||
await state.logout();
|
||||
assert.equal(calls, 1);
|
||||
pending.resolve(Response.json({ success: true }));
|
||||
await first;
|
||||
assert.equal(state.loggingOut.value, false);
|
||||
});
|
||||
|
||||
test("退出失败不跳转,展示服务端错误并允许重试", async (t) => {
|
||||
let calls = 0;
|
||||
const { state } = await mountComposable(t, useAccessDenied, async () => {
|
||||
calls += 1;
|
||||
return calls === 1
|
||||
? Response.json({ error: "会话服务暂不可用" }, { status: 503 })
|
||||
: Response.json({ success: true });
|
||||
});
|
||||
const redirect = t.mock.method(window.location, "assign");
|
||||
await state.logout();
|
||||
assert.equal(redirect.mock.callCount(), 0);
|
||||
assert.equal(state.error.value, "会话服务暂不可用");
|
||||
assert.equal(state.loggingOut.value, false);
|
||||
await state.logout();
|
||||
assert.equal(state.error.value, "");
|
||||
assert.equal(redirect.mock.callCount(), 1);
|
||||
});
|
||||
|
||||
test("退出接口返回非 JSON 错误时保留中文提示和当前页面", async (t) => {
|
||||
const { state } = await mountComposable(t, useAccessDenied, async () => new Response("Bad Gateway", { status: 502 }));
|
||||
const redirect = t.mock.method(window.location, "assign");
|
||||
await state.logout();
|
||||
assert.equal(state.error.value, "退出登录失败");
|
||||
assert.equal(state.loggingOut.value, false);
|
||||
assert.equal(redirect.mock.callCount(), 0);
|
||||
});
|
||||
@ -104,11 +104,29 @@
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.navigation-footer {
|
||||
margin: auto 8px 0;
|
||||
.navigation-session {
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
.navigation-error {
|
||||
margin: 0 8px 8px;
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.navigation-logout:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.navigation-footer {
|
||||
margin: 8px 8px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.dashboard-navigation {
|
||||
position: static;
|
||||
@ -133,4 +151,9 @@
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.navigation-session {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
<script setup>
|
||||
import { useAccessDenied } from "../AccessDenied/AccessDenied.js";
|
||||
|
||||
const { loggingOut, error, logout } = useAccessDenied();
|
||||
|
||||
defineProps({
|
||||
activeSection: { type: String, required: true },
|
||||
sections: { type: Array, required: true },
|
||||
@ -72,7 +76,22 @@ defineEmits(["select", "settings"]);
|
||||
</svg>
|
||||
<span>系统设置</span>
|
||||
</button>
|
||||
<p class="navigation-footer">SSL 证书管理</p>
|
||||
<div class="navigation-session">
|
||||
<p v-if="error" class="navigation-error" role="alert">{{ error }}</p>
|
||||
<button
|
||||
class="navigation-item navigation-logout"
|
||||
type="button"
|
||||
:disabled="loggingOut"
|
||||
:aria-busy="loggingOut"
|
||||
@click="logout"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M9 4H4v16h5m6-13 5 5-5 5M9 12h11" />
|
||||
</svg>
|
||||
<span>{{ loggingOut ? "退出中…" : "退出登录" }}</span>
|
||||
</button>
|
||||
<p class="navigation-footer">SSL 证书管理</p>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user