@sleep2agi/agent-network-dashboard 0.6.3-preview.50 → 0.6.3-preview.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_COMMIT +1 -1
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +14 -14
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page.js.nft.json +1 -1
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +2 -2
- package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/logs/page.js.nft.json +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +2 -2
- package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/messages/page.js.nft.json +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +2 -2
- package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
- package/.next/server/app/node/page.js.nft.json +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +2 -2
- package/.next/server/app/node.segments/_full.segment.rsc +2 -2
- package/.next/server/app/node.segments/_head.segment.rsc +1 -1
- package/.next/server/app/node.segments/_index.segment.rsc +2 -2
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/node.segments/node.segment.rsc +1 -1
- package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes/page.js.nft.json +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +3 -3
- package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
- package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page.js.nft.json +1 -1
- package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +2 -2
- package/.next/server/app/providers.rsc +2 -2
- package/.next/server/app/providers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/providers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
- package/.next/server/app/server-logs/page.js.nft.json +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +2 -2
- package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
- package/.next/server/app/servers/page.js.nft.json +1 -1
- package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +2 -2
- package/.next/server/app/servers.rsc +2 -2
- package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
- package/.next/server/app/settings/networks/page.js.nft.json +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +3 -3
- package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page.js.nft.json +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +2 -2
- package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/.next/server/chunks/ssr/{0ld5_next_dist_00v047f._.js → 0ld5_next_dist_04cce2g._.js} +2 -2
- package/.next/server/chunks/ssr/0ld5_next_dist_04cce2g._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__00n84ca._.js → [root-of-the-server]__0x9nh~a._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0x9nh~a._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- package/.next/server/chunks/ssr/{dash-stageb-wt_0hzfl6x._.js → dash-stageb-wt_app_page_tsx_0iyrtvj._.js} +3 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_page_tsx_0iyrtvj._.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/05~lgfzsv.p~p.js +2 -0
- package/.next/static/chunks/092zqb~4un32l.css +1 -0
- package/.next/static/chunks/0aaakzcw7tey0.js +0 -0
- package/.next/static/chunks/0advoo2tzdcyi.js +1 -0
- package/.next/static/chunks/0dkk_l872ocgm.js +1 -0
- package/.next/static/chunks/0fz7zk.ek.154.js +1 -0
- package/.next/static/chunks/0njt8wek.9aq6.js +1 -0
- package/.next/static/chunks/{0hm52b0y~k0~z.js → 0uk67ty_0h_36.js} +1 -1
- package/.next/static/chunks/{0ep78sq90x_yc.js → 0z_4d13crd_af.js} +1 -1
- package/.next/static/chunks/0zljk8~2jqw93.js +4 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +108 -16
- package/app/components/NodeList.tsx +198 -46
- package/app/components/NodeListContextMenu.tsx +187 -0
- package/app/components/NodeSettingsPanel.tsx +29 -3
- package/app/nodes/NodesView.tsx +58 -0
- package/package.json +1 -1
- package/tests/e2e-9-nodes-team-tag-filter.spec.ts +3 -0
- package/tests/e2e-batch-list-actions.spec.ts +234 -0
- package/tests/e2e-stage-a-node-list.spec.ts +4 -0
- package/tests/e2e-stage-b-chat-pane.spec.ts +103 -0
- package/.next/server/chunks/ssr/0ld5_next_dist_00v047f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__00n84ca._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_0hzfl6x._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js.map +0 -1
- package/.next/static/chunks/02.5_nifak.uu.js +0 -1
- package/.next/static/chunks/03hzj129k~odb.js +0 -1
- package/.next/static/chunks/0cey4i.y0a8~j.js +0 -1
- package/.next/static/chunks/0mhaf6lrw5nn..css +0 -1
- package/.next/static/chunks/0s293geid.j7e.js +0 -4
- package/.next/static/chunks/0zj_navpyhltx.js +0 -2
- package/.next/static/chunks/14y4x3de1lbug.js +0 -1
- package/.next/static/chunks/15ka68xi.w-i-.js +0 -1
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → JL_QjNKv1KhsCF68ZK28x}/_buildManifest.js +0 -0
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → JL_QjNKv1KhsCF68ZK28x}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → JL_QjNKv1KhsCF68ZK28x}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3100';
|
|
4
|
+
|
|
5
|
+
// 07-31 batch (通信龙): 3 hard tests for the NodeList/ChatPane batch.
|
|
6
|
+
// Each test asserts an interaction that has no error surface if broken
|
|
7
|
+
// (would ship as "点了没反应" / "打了草稿没了" / "菜单少两项"):
|
|
8
|
+
//
|
|
9
|
+
// ① settings drawer open/close does NOT unmount TaskChatPanel
|
|
10
|
+
// (draft-in-input survives). Witnessed-red by mutating the drawer
|
|
11
|
+
// to KEY-remount the chat (parent-controlled key change → React
|
|
12
|
+
// remount → draft lost).
|
|
13
|
+
// ② keyboard focus discipline: ↓ moves URL when focus is on the
|
|
14
|
+
// rail, ↓ does NOT move URL when focus is in a text input.
|
|
15
|
+
// ③ right-click near list bottom → context menu flips upward
|
|
16
|
+
// (menu.getBoundingClientRect().bottom <= viewport.bottom).
|
|
17
|
+
//
|
|
18
|
+
// Not covered here (already visible in vf.mjs on prod real-data run):
|
|
19
|
+
// - pin/mute badge color swap (unit-level; SPEC §11.3 tokens hardcoded)
|
|
20
|
+
// - view-tasks / search buttons render (trivially visible)
|
|
21
|
+
|
|
22
|
+
// Per-worker login cache — dashboard rate-limit is 10 logins / 15min / IP.
|
|
23
|
+
let cachedCookie: string | undefined;
|
|
24
|
+
|
|
25
|
+
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
26
|
+
if (cachedCookie) {
|
|
27
|
+
await context.addCookies([{
|
|
28
|
+
name: 'anet_dashboard_session',
|
|
29
|
+
value: cachedCookie,
|
|
30
|
+
domain: new URL(BASE).hostname,
|
|
31
|
+
path: '/',
|
|
32
|
+
}]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
36
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
37
|
+
});
|
|
38
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
39
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
40
|
+
if (m) {
|
|
41
|
+
cachedCookie = decodeURIComponent(m[1]);
|
|
42
|
+
await context.addCookies([{
|
|
43
|
+
name: 'anet_dashboard_session',
|
|
44
|
+
value: cachedCookie,
|
|
45
|
+
domain: new URL(BASE).hostname,
|
|
46
|
+
path: '/',
|
|
47
|
+
}]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const FIXTURE_SESSIONS = {
|
|
52
|
+
sessions: [
|
|
53
|
+
{ alias: 'node-alpha', status: 'idle', network_id: 'net_x' },
|
|
54
|
+
{ alias: 'node-beta', status: 'idle', network_id: 'net_x' },
|
|
55
|
+
{ alias: 'node-gamma', status: 'idle', network_id: 'net_x' },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
const FIXTURE_NODES = {
|
|
59
|
+
nodes: [
|
|
60
|
+
{ alias: 'node-alpha', node_id: 'n_alpha', team: null, tags: [], attrs_revision: 0 },
|
|
61
|
+
{ alias: 'node-beta', node_id: 'n_beta', team: null, tags: [], attrs_revision: 0 },
|
|
62
|
+
{ alias: 'node-gamma', node_id: 'n_gamma', team: null, tags: [], attrs_revision: 0 },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
const FIXTURE_HEALTH = {
|
|
66
|
+
ok: true, version: '0.9.0-test', sse_connections: 3,
|
|
67
|
+
sse_sessions: {
|
|
68
|
+
'net_x:node-alpha': 1, 'net_x:node-beta': 1, 'net_x:node-gamma': 1,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function stubHub(page: Page) {
|
|
73
|
+
await page.route('**/api/hub/status**', r =>
|
|
74
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_SESSIONS) }));
|
|
75
|
+
await page.route('**/api/hub/nodes**', r =>
|
|
76
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_NODES) }));
|
|
77
|
+
await page.route('**/api/hub/networks**', r =>
|
|
78
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ networks: [{ network_id: 'net_x', network_name: 'net_x' }] }) }));
|
|
79
|
+
await page.route('**/api/hub/health', r =>
|
|
80
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_HEALTH) }));
|
|
81
|
+
await page.route('**/api/hub/tasks**', r =>
|
|
82
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [] }) }));
|
|
83
|
+
await page.route('**/api/hub/stats**', r =>
|
|
84
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
|
|
85
|
+
await page.route('**/api/hub/messages**', r =>
|
|
86
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
87
|
+
await page.route('**/api/hub/events**', r =>
|
|
88
|
+
r.fulfill({ status: 200, contentType: 'text/event-stream', body: '' }));
|
|
89
|
+
// NodeSettingsPanel loads a per-node config snapshot — return an
|
|
90
|
+
// empty stub so the drawer mounts without console noise.
|
|
91
|
+
await page.route('**/api/anet/node-config**', r =>
|
|
92
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ model: '', flags: {}, channels: [], config_revision: 0 }) }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test.describe('Batch — list actions (07-31)', () => {
|
|
96
|
+
test('① settings drawer open/close does NOT unmount TaskChatPanel (draft survives)', async ({ page, context }) => {
|
|
97
|
+
await login(context);
|
|
98
|
+
await stubHub(page);
|
|
99
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
100
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
101
|
+
|
|
102
|
+
// Find the chat's text input. TaskChatPanel renders one primary
|
|
103
|
+
// <textarea> / <input> for message composition — grab the first
|
|
104
|
+
// textbox inside the chat pane.
|
|
105
|
+
const chatInput = page.locator('[data-testid="chat-pane"] textarea, [data-testid="chat-pane"] input[type="text"]').first();
|
|
106
|
+
await expect(chatInput).toBeVisible({ timeout: 10000 });
|
|
107
|
+
// Type a distinctive draft that would be hard to reproduce by accident.
|
|
108
|
+
const DRAFT = 'preserve-across-settings-toggle-abc123';
|
|
109
|
+
await chatInput.fill(DRAFT);
|
|
110
|
+
await expect(chatInput).toHaveValue(DRAFT);
|
|
111
|
+
|
|
112
|
+
// Open settings drawer → close → text MUST still be there.
|
|
113
|
+
await page.locator('[data-testid="chat-pane-settings"]').click();
|
|
114
|
+
await expect(page.locator('[role="dialog"][aria-label*="节点设置"]').first()).toBeVisible({ timeout: 5000 });
|
|
115
|
+
// Close via the backdrop (absolute-positioned inside chat pane).
|
|
116
|
+
await page.keyboard.press('Escape');
|
|
117
|
+
// Wait a tick for React to reconcile.
|
|
118
|
+
await page.waitForTimeout(200);
|
|
119
|
+
|
|
120
|
+
// 🔴 The core assertion — the input still holds the same text.
|
|
121
|
+
// If TaskChatPanel got unmounted+remounted, state resets and this
|
|
122
|
+
// reads as empty (or reset to any autosaved draft, which is
|
|
123
|
+
// different — the point is "same value, no interruption").
|
|
124
|
+
await expect(chatInput).toHaveValue(DRAFT);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('② kbd focus discipline — ↓ moves URL from rail focus, does NOT from input focus', async ({ page, context }) => {
|
|
128
|
+
await login(context);
|
|
129
|
+
await stubHub(page);
|
|
130
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
131
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
132
|
+
|
|
133
|
+
// Wait for rail to actually have entries — the kbd handler
|
|
134
|
+
// bails on sessions.length === 0, so pressing arrow before SWR
|
|
135
|
+
// populates would test nothing.
|
|
136
|
+
await expect(page.locator('[data-node-list-item]').first()).toBeVisible({ timeout: 10000 });
|
|
137
|
+
const railCount = await page.locator('[data-node-list-item]').count();
|
|
138
|
+
expect(railCount).toBeGreaterThanOrEqual(3);
|
|
139
|
+
|
|
140
|
+
// Capture browser console to diagnose whether the handler fires.
|
|
141
|
+
const consoleMsgs: string[] = [];
|
|
142
|
+
page.on('console', msg => consoleMsgs.push(`${msg.type()}: ${msg.text()}`));
|
|
143
|
+
|
|
144
|
+
// Phase 1 — focus a NON-editable element (a rail entry button).
|
|
145
|
+
// TaskChatPanel refocuses its textarea via setTimeout(300) on
|
|
146
|
+
// mount (TaskChatPanel.tsx:745), so we MUST wait past that
|
|
147
|
+
// window before setting our own focus — otherwise the textarea
|
|
148
|
+
// steals it back mid-test. Wait 500ms > 300ms delay, then focus
|
|
149
|
+
// the button and press the key with no interceding await so no
|
|
150
|
+
// subsequent focus event can preempt.
|
|
151
|
+
await page.waitForTimeout(500);
|
|
152
|
+
await page.locator('[data-node-list-item]').first().focus();
|
|
153
|
+
const activeTag = await page.evaluate(() => document.activeElement?.tagName);
|
|
154
|
+
expect(activeTag).toBe('BUTTON');
|
|
155
|
+
|
|
156
|
+
const beforeUrl = new URL(page.url()).pathname;
|
|
157
|
+
expect(beforeUrl).toContain('/nodes/');
|
|
158
|
+
await page.keyboard.press('ArrowDown');
|
|
159
|
+
await page.waitForTimeout(500);
|
|
160
|
+
const afterUrl = new URL(page.url()).pathname;
|
|
161
|
+
expect(afterUrl).not.toBe(beforeUrl);
|
|
162
|
+
expect(afterUrl).toContain('node-beta');
|
|
163
|
+
|
|
164
|
+
// Phase 2 — dispatch same ArrowDown but with document.activeElement
|
|
165
|
+
// pointing at a text control. My handler bails when active.tagName
|
|
166
|
+
// is INPUT/TEXTAREA. Verify URL does NOT change.
|
|
167
|
+
const chatInput = page.locator('[data-testid="chat-pane"] textarea, [data-testid="chat-pane"] input[type="text"]').first();
|
|
168
|
+
await chatInput.focus();
|
|
169
|
+
await page.waitForTimeout(100);
|
|
170
|
+
const inputUrl = new URL(page.url()).pathname;
|
|
171
|
+
// Sanity: confirm the input actually took focus BEFORE dispatching,
|
|
172
|
+
// otherwise this test would spuriously pass (URL wouldn't change
|
|
173
|
+
// because the handler THINKS active is body → moves URL, but I'd
|
|
174
|
+
// observe unchanged URL for a different reason).
|
|
175
|
+
const activeInInput = await page.evaluate(() => document.activeElement?.tagName);
|
|
176
|
+
expect(['INPUT', 'TEXTAREA']).toContain(activeInInput ?? 'NONE');
|
|
177
|
+
await page.evaluate(() => {
|
|
178
|
+
const ev = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true });
|
|
179
|
+
window.dispatchEvent(ev);
|
|
180
|
+
});
|
|
181
|
+
await page.waitForTimeout(400);
|
|
182
|
+
const stillInputUrl = new URL(page.url()).pathname;
|
|
183
|
+
expect(stillInputUrl).toBe(inputUrl);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('③ right-click near list bottom → menu flips upward + stays in viewport', async ({ page, context }) => {
|
|
187
|
+
await login(context);
|
|
188
|
+
await stubHub(page);
|
|
189
|
+
// Small viewport height so any entry the user right-clicks is
|
|
190
|
+
// "near the bottom" — a 120px menu rendered downward would
|
|
191
|
+
// overflow. This is the exact scenario the flip logic protects.
|
|
192
|
+
await page.setViewportSize({ width: 1280, height: 300 });
|
|
193
|
+
// Go to a node page (not /nodes) — the rail renders in both, but
|
|
194
|
+
// going through /nodes/<alias> exercises the exact code path
|
|
195
|
+
// tests 1 & 2 already validated the auth on. Then click the rail
|
|
196
|
+
// entry to force the "list-only" shape (chatAlias unchanged in
|
|
197
|
+
// URL is fine — the point is right-click on the last rail row).
|
|
198
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
199
|
+
await expect(page.locator('[data-node-list-alias="node-gamma"]').first()).toBeVisible({ timeout: 15000 });
|
|
200
|
+
|
|
201
|
+
// Right-click on the last rail entry (node-gamma). Playwright's
|
|
202
|
+
// .click({button:'right'}) fires a native contextmenu with the
|
|
203
|
+
// element's center as clientX/clientY — for the last entry in a
|
|
204
|
+
// 300px-tall viewport, that lands well into the "would overflow
|
|
205
|
+
// downward" zone.
|
|
206
|
+
await page.locator('[data-node-list-alias="node-gamma"]').click({ button: 'right' });
|
|
207
|
+
|
|
208
|
+
const menu = page.locator('[data-testid="node-context-menu"]');
|
|
209
|
+
await expect(menu).toBeVisible({ timeout: 3000 });
|
|
210
|
+
// Wait for the useLayoutEffect + re-render — flip only applies
|
|
211
|
+
// after the second render. If flip did NOT fire, this attribute
|
|
212
|
+
// stays absent → the timeout below reads "expected 'true', got null"
|
|
213
|
+
// and the test reds precisely on the flip contract.
|
|
214
|
+
await expect(menu).toHaveAttribute('data-flipped-y', 'true', { timeout: 2000 });
|
|
215
|
+
|
|
216
|
+
// 🔴 Core assertion — menu is fully inside viewport (bottom edge
|
|
217
|
+
// ≤ viewport bottom). Without the flip, the menu would render
|
|
218
|
+
// from clientY down and have bottom > viewport.height.
|
|
219
|
+
const rect = await menu.evaluate(el => {
|
|
220
|
+
const r = el.getBoundingClientRect();
|
|
221
|
+
return { top: r.top, bottom: r.bottom, left: r.left, right: r.right };
|
|
222
|
+
});
|
|
223
|
+
// Read current viewport (we set it to 1280×300 above).
|
|
224
|
+
const vp = page.viewportSize()!;
|
|
225
|
+
expect(rect.bottom).toBeLessThanOrEqual(vp.height);
|
|
226
|
+
expect(rect.top).toBeGreaterThanOrEqual(0);
|
|
227
|
+
expect(rect.left).toBeGreaterThanOrEqual(0);
|
|
228
|
+
expect(rect.right).toBeLessThanOrEqual(vp.width);
|
|
229
|
+
|
|
230
|
+
// Sibling positive — the flip attribute is set (proves it took
|
|
231
|
+
// the flip branch, not just happened to fit by luck).
|
|
232
|
+
await expect(menu).toHaveAttribute('data-flipped-y', 'true');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -57,6 +57,10 @@ async function login(context: Page['context'] extends () => infer C ? C : never)
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async function mockCommon(page: Page) {
|
|
60
|
+
// #Vincent auto-select (07-31): these suites exercise the table/filter
|
|
61
|
+
// (manage) view — opt into it explicitly now that /nodes defaults to
|
|
62
|
+
// conversation-first with auto-select.
|
|
63
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
|
|
60
64
|
await page.route('**/api/hub/status**', r =>
|
|
61
65
|
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(HUB_STATUS) }));
|
|
62
66
|
await page.route('**/api/hub/nodes**', r =>
|
|
@@ -84,6 +84,15 @@ async function mockCommon(page: Page) {
|
|
|
84
84
|
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
|
|
88
|
+
// #Vincent auto-select (07-31): desktop /nodes now auto-selects the first
|
|
89
|
+
// conversation. Tests that exercise the UNSELECTED table-on-entry state
|
|
90
|
+
// opt into the persisted manage-view mode — the product's explicit doorway
|
|
91
|
+
// to that state — instead of weakening assertions.
|
|
92
|
+
async function manageMode(page: Page) {
|
|
93
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
const pane = (page: Page) => page.locator('[data-testid="chat-pane"]');
|
|
88
97
|
const card = (page: Page, alias: string) => page.locator(`[data-node-card][data-node-alias="${alias}"]`);
|
|
89
98
|
|
|
@@ -91,6 +100,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
91
100
|
test('clicking a node renders chat inline in the right pane and rewrites the URL', async ({ page, context }) => {
|
|
92
101
|
await login(context);
|
|
93
102
|
await mockCommon(page);
|
|
103
|
+
await manageMode(page);
|
|
94
104
|
await page.goto(`${BASE}/nodes`);
|
|
95
105
|
await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
|
|
96
106
|
|
|
@@ -108,6 +118,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
108
118
|
test('clicking another node swaps the SAME pane in place (still exactly one)', async ({ page, context }) => {
|
|
109
119
|
await login(context);
|
|
110
120
|
await mockCommon(page);
|
|
121
|
+
await manageMode(page);
|
|
111
122
|
await page.goto(`${BASE}/nodes`);
|
|
112
123
|
await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
|
|
113
124
|
|
|
@@ -188,6 +199,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
188
199
|
test('#Stage C: selected = list+chat (table gone), unselected = table back — BOTH directions asserted', async ({ page, context }) => {
|
|
189
200
|
await login(context);
|
|
190
201
|
await mockCommon(page);
|
|
202
|
+
await manageMode(page);
|
|
191
203
|
await page.goto(`${BASE}/nodes`);
|
|
192
204
|
// Direction 1 (positive, same source): unselected /nodes renders the table.
|
|
193
205
|
await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
|
|
@@ -209,6 +221,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
209
221
|
test('#Stage C: dense-view user (rail hidden) selecting a node force-shows the rail', async ({ page, context }) => {
|
|
210
222
|
await login(context);
|
|
211
223
|
await mockCommon(page);
|
|
224
|
+
await manageMode(page);
|
|
212
225
|
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-rail', '0'));
|
|
213
226
|
await page.goto(`${BASE}/nodes`);
|
|
214
227
|
await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
|
|
@@ -314,6 +327,7 @@ test.describe('Stage D — per-column scrolling, page never scrolls', () => {
|
|
|
314
327
|
test('unselected + 60 nodes: document still does not scroll; the MIDDLE column scrolls to the last card', async ({ page, context }) => {
|
|
315
328
|
await login(context);
|
|
316
329
|
await mockWide(page);
|
|
330
|
+
await manageMode(page);
|
|
317
331
|
await page.goto(`${BASE}/nodes`);
|
|
318
332
|
await expect(page.locator('[data-node-card]')).toHaveCount(WIDE_N, { timeout: 15000 });
|
|
319
333
|
|
|
@@ -436,6 +450,7 @@ test.describe('Stage D ③ — rail-top node search', () => {
|
|
|
436
450
|
test('unselected state: rail search filters rail AND table together (Stage A invariant holds)', async ({ page, context }) => {
|
|
437
451
|
await login(context);
|
|
438
452
|
await mockSearch(page);
|
|
453
|
+
await manageMode(page);
|
|
439
454
|
await page.goto(`${BASE}/nodes`);
|
|
440
455
|
await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
|
|
441
456
|
|
|
@@ -507,3 +522,91 @@ test.describe('Stage D — chat column fixed, message area scrolls inside', () =
|
|
|
507
522
|
expect(box!.y + box!.height).toBeLessThanOrEqual(innerH);
|
|
508
523
|
});
|
|
509
524
|
});
|
|
525
|
+
|
|
526
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
527
|
+
// Vincent 插单 (07-31): desktop /nodes opens INTO a conversation.
|
|
528
|
+
// Traps pinned: ① replace-not-push (back must LEAVE /nodes, not bounce),
|
|
529
|
+
// ② the manage table keeps an explicit doorway, ③ an explicit ✕ is not
|
|
530
|
+
// answered by an immediate re-select, plus empty-list safety.
|
|
531
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
532
|
+
test.describe('auto-select first conversation on /nodes (desktop)', () => {
|
|
533
|
+
test('entering /nodes lands in the first conversation: URL rewritten, table yields', async ({ page, context }) => {
|
|
534
|
+
await login(context);
|
|
535
|
+
await mockCommon(page);
|
|
536
|
+
await page.goto(`${BASE}/nodes`);
|
|
537
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
538
|
+
await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
|
|
539
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(0);
|
|
540
|
+
// deterministic first pick under this fixture's sort
|
|
541
|
+
await expect(pane(page)).toHaveAttribute('data-chat-alias', 'stage-b-1');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('browser back LEAVES /nodes (auto-select uses replace, not push)', async ({ page, context }) => {
|
|
545
|
+
await login(context);
|
|
546
|
+
await mockCommon(page);
|
|
547
|
+
await page.goto(`${BASE}/tasks`);
|
|
548
|
+
await page.goto(`${BASE}/nodes`);
|
|
549
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
550
|
+
await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
|
|
551
|
+
|
|
552
|
+
await page.goBack();
|
|
553
|
+
// With push instead of replace this bounces straight back into
|
|
554
|
+
// /nodes/<alias> — the assertion below goes red (sabotage-verified).
|
|
555
|
+
await expect(page).toHaveURL(`${BASE}/tasks`, { timeout: 10000 });
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('explicit ✕ close sticks: no immediate re-select', async ({ page, context }) => {
|
|
559
|
+
await login(context);
|
|
560
|
+
await mockCommon(page);
|
|
561
|
+
await page.goto(`${BASE}/nodes`);
|
|
562
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
563
|
+
|
|
564
|
+
await page.locator('[data-testid="chat-pane-close"]').click();
|
|
565
|
+
await expect(pane(page)).toHaveCount(0);
|
|
566
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
567
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3);
|
|
568
|
+
await page.waitForTimeout(900);
|
|
569
|
+
await expect(pane(page)).toHaveCount(0); // still closed — ✕ won
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('manage-view doorway: toggle to table, persists across reload, toggles back to conversation', async ({ page, context }) => {
|
|
573
|
+
await login(context);
|
|
574
|
+
await mockCommon(page);
|
|
575
|
+
await page.goto(`${BASE}/nodes`);
|
|
576
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
577
|
+
|
|
578
|
+
await page.locator('[data-testid="manage-view-toggle"]').click();
|
|
579
|
+
await expect(pane(page)).toHaveCount(0);
|
|
580
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3);
|
|
581
|
+
|
|
582
|
+
await page.goto(`${BASE}/nodes`); // persisted preference: table default, no auto-select
|
|
583
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
|
|
584
|
+
await page.waitForTimeout(900);
|
|
585
|
+
await expect(pane(page)).toHaveCount(0);
|
|
586
|
+
|
|
587
|
+
await page.locator('[data-testid="manage-view-toggle"]').click(); // 对话视图
|
|
588
|
+
await expect(pane(page)).toBeVisible();
|
|
589
|
+
await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test('empty list: no crash, no loop, URL stays /nodes', async ({ page, context }) => {
|
|
593
|
+
await login(context);
|
|
594
|
+
await page.route('**/api/hub/status**', r =>
|
|
595
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ sessions: [] }) }));
|
|
596
|
+
await page.route('**/api/hub/nodes**', r =>
|
|
597
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ nodes: [] }) }));
|
|
598
|
+
await page.route('**/api/hub/health', r =>
|
|
599
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, sse_sessions: {} }) }));
|
|
600
|
+
await page.route('**/api/hub/tasks**', r =>
|
|
601
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [] }) }));
|
|
602
|
+
await page.route('**/api/hub/stats**', r =>
|
|
603
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
|
|
604
|
+
await page.route('**/api/hub/messages**', r =>
|
|
605
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
606
|
+
await page.goto(`${BASE}/nodes`);
|
|
607
|
+
await page.waitForTimeout(2500);
|
|
608
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
609
|
+
await expect(pane(page)).toHaveCount(0);
|
|
610
|
+
await expect(page.locator("text=This page couldn’t load")).toHaveCount(0);
|
|
611
|
+
});
|
|
612
|
+
});
|