@sleep2agi/agent-network-dashboard 0.6.3-preview.51 → 0.6.3-preview.52
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 +46 -46
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- 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 +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +15 -15
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +8 -8
- 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 +15 -15
- package/.next/server/app/login.segments/_full.segment.rsc +15 -15
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +8 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/node.segments/_full.segment.rsc +15 -15
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +8 -8
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +15 -15
- package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- 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]__0dcez4x._.js +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0jgqpfv._.js → [root-of-the-server]__0gyqilt._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0gyqilt._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__11wln8.._.js → [root-of-the-server]__0oavw6j._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0oavw6j._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0rct4.k._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__02o~-py._.js.map → [root-of-the-server]__0rct4.k._.js.map} +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js.map +1 -1
- 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 +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- 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/0-ot~88nectgp.js +2 -0
- package/.next/static/chunks/00adqh_rd04uf.js +9 -0
- package/.next/static/chunks/{0advoo2tzdcyi.js → 051p0pw4z~6ag.js} +1 -1
- package/.next/static/chunks/{0uk67ty_0h_36.js → 0lf8e-h91s10h.js} +1 -1
- package/.next/static/chunks/{0dkk_l872ocgm.js → 0ohx4jpm9~61..js} +1 -1
- package/.next/static/chunks/0pg4pce.q5ne_.js +9 -0
- package/.next/static/chunks/{0fz7zk.ek.154.js → 0u5y.gmi~jkw6.js} +1 -1
- package/.next/static/chunks/0wf03_efeii.w.css +1 -0
- package/.next/static/chunks/0ws0p1n62l~ub.js +1 -0
- package/.next/static/chunks/{06q7-vhy~1-y3.js → 0~rx9_~1wupcp.js} +2 -2
- package/.next/static/chunks/{0z_4d13crd_af.js → 14x094g58kg61.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +26 -2
- package/app/components/NodeList.tsx +14 -4
- package/app/components/TaskChatPanel.tsx +156 -31
- package/app/messages/page.tsx +22 -22
- package/app/nodes/NodesView.tsx +52 -11
- package/package.json +1 -1
- package/tests/e2e-9-nodes-team-tag-filter.spec.ts +14 -7
- package/tests/e2e-messages-spec-visual-align.spec.ts +244 -0
- package/tests/e2e-pin-mute-attr-always-present.spec.ts +171 -0
- package/tests/e2e-stage-a-node-list.spec.ts +14 -7
- package/tests/e2e-stage-b-chat-pane.spec.ts +74 -10
- package/tests/e2e-status-labels.spec.ts +218 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +0 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +0 -1
- package/.next/static/chunks/05~lgfzsv.p~p.js +0 -2
- package/.next/static/chunks/092zqb~4un32l.css +0 -1
- package/.next/static/chunks/11zz15q37~bz5.js +0 -7
- package/.next/static/chunks/17qkc714xy04..js +0 -1
- /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → RiXBfeE2zY5tzXRmF7kBD}/_buildManifest.js +0 -0
- /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → RiXBfeE2zY5tzXRmF7kBD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → RiXBfeE2zY5tzXRmF7kBD}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { test, expect, Page, BrowserContext } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// /messages visual-alignment guardrail — pins the numbers that SPEC.md
|
|
4
|
+
// §2/§5/§6 dictate for the chat area.
|
|
5
|
+
//
|
|
6
|
+
// Method (per 07-31 通信龙 判据 "断言事实,不是声明"):
|
|
7
|
+
// values are read from getComputedStyle() on the real rendered DOM,
|
|
8
|
+
// NOT from any data-* attribute that would just restate the class
|
|
9
|
+
// value. If the visual token drifts, the number drifts, the assertion
|
|
10
|
+
// goes red — assertion side is same-source with reality (the browser's
|
|
11
|
+
// computed style), not with the declaration (the Tailwind class).
|
|
12
|
+
//
|
|
13
|
+
// Witnessed-red discipline (per 07-31 通信龙 判据 "一条一条打,不是整体打"):
|
|
14
|
+
// each assertion is its own `test(...)` block so witnessed-red on any
|
|
15
|
+
// ONE token (revert only that class, re-run) turns exactly ONE test
|
|
16
|
+
// red. If reverting shell bg turned two assertions red, that would
|
|
17
|
+
// prove they're coupled — not each guarding its own thing.
|
|
18
|
+
|
|
19
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3200';
|
|
20
|
+
|
|
21
|
+
const MOCK_MESSAGES = {
|
|
22
|
+
messages: [
|
|
23
|
+
// First bubble in the list — outgoing task alpha→beta, so bubble
|
|
24
|
+
// background hits the SPEC #1E3A5F "self" color.
|
|
25
|
+
{
|
|
26
|
+
id: 'm1',
|
|
27
|
+
type: 'task',
|
|
28
|
+
from_alias: 'alpha',
|
|
29
|
+
to_alias: 'beta',
|
|
30
|
+
priority: 'normal',
|
|
31
|
+
content: 'ALPHA_TO_BETA_BODY',
|
|
32
|
+
created_at: '2026-07-31 12:00:00',
|
|
33
|
+
task_id: 't1',
|
|
34
|
+
},
|
|
35
|
+
// Second — incoming reply from beta→alpha, so we can also probe
|
|
36
|
+
// the SPEC #1E293B "other" color separately.
|
|
37
|
+
{
|
|
38
|
+
id: 'm2',
|
|
39
|
+
type: 'reply',
|
|
40
|
+
from_alias: 'beta',
|
|
41
|
+
to_alias: 'alpha',
|
|
42
|
+
priority: 'normal',
|
|
43
|
+
content: 'BETA_TO_ALPHA_BODY',
|
|
44
|
+
created_at: '2026-07-31 12:00:05',
|
|
45
|
+
},
|
|
46
|
+
// Third — broadcast, verifies the SPEC §5 "left 2px accent bar"
|
|
47
|
+
// pattern that we reused for broadcast distinction.
|
|
48
|
+
{
|
|
49
|
+
id: 'm3',
|
|
50
|
+
type: 'broadcast',
|
|
51
|
+
from_alias: 'admin',
|
|
52
|
+
priority: 'high',
|
|
53
|
+
content: 'BROADCAST_BODY',
|
|
54
|
+
created_at: '2026-07-31 12:00:10',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Rate-limit avoidance (10/15min/IP): one login per worker, reuse the
|
|
60
|
+
// cookie across all tests in this file. With --workers=1 the module
|
|
61
|
+
// var is truly shared. If the first login gets 429 (limit exhausted),
|
|
62
|
+
// throw immediately — otherwise every subsequent test would re-attempt
|
|
63
|
+
// and burn budget further while all failing the same way.
|
|
64
|
+
let cachedSessionCookie: string | null = null;
|
|
65
|
+
let loginErr: Error | null = null;
|
|
66
|
+
async function login(context: BrowserContext) {
|
|
67
|
+
if (loginErr) throw loginErr;
|
|
68
|
+
if (!cachedSessionCookie) {
|
|
69
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
70
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
71
|
+
});
|
|
72
|
+
if (res.status() === 429) {
|
|
73
|
+
loginErr = new Error(
|
|
74
|
+
'Login rate-limited (10/15min). Restart dev to clear the counter.',
|
|
75
|
+
);
|
|
76
|
+
throw loginErr;
|
|
77
|
+
}
|
|
78
|
+
// playwright merges multi-value headers with newlines in headers();
|
|
79
|
+
// set-cookie is often multi-value so parse from headersArray instead.
|
|
80
|
+
const headers = res.headersArray();
|
|
81
|
+
const cookies = headers.filter((h) => h.name.toLowerCase() === 'set-cookie');
|
|
82
|
+
for (const c of cookies) {
|
|
83
|
+
const m = c.value.match(/anet_dashboard_session=([^;]+)/);
|
|
84
|
+
if (m) {
|
|
85
|
+
cachedSessionCookie = decodeURIComponent(m[1]);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!cachedSessionCookie) {
|
|
90
|
+
loginErr = new Error(
|
|
91
|
+
`login 200 but no anet_dashboard_session cookie in headers: ${JSON.stringify(cookies)}`,
|
|
92
|
+
);
|
|
93
|
+
throw loginErr;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await context.addCookies([
|
|
97
|
+
{
|
|
98
|
+
name: 'anet_dashboard_session',
|
|
99
|
+
value: cachedSessionCookie,
|
|
100
|
+
domain: new URL(BASE).hostname,
|
|
101
|
+
path: '/',
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function mockAll(page: Page) {
|
|
107
|
+
await page.route('**/api/hub/messages**', (r) =>
|
|
108
|
+
r.fulfill({
|
|
109
|
+
status: 200,
|
|
110
|
+
contentType: 'application/json',
|
|
111
|
+
body: JSON.stringify(MOCK_MESSAGES),
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
await page.route('**/api/hub/status**', (r) =>
|
|
115
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: '{"sessions":[]}' }),
|
|
116
|
+
);
|
|
117
|
+
await page.route('**/api/hub/nodes**', (r) =>
|
|
118
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: '{"nodes":[]}' }),
|
|
119
|
+
);
|
|
120
|
+
await page.route('**/api/hub/health', (r) =>
|
|
121
|
+
r.fulfill({
|
|
122
|
+
status: 200,
|
|
123
|
+
contentType: 'application/json',
|
|
124
|
+
body: '{"ok":true,"sse_sessions":{}}',
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function gotoMessages(page: Page) {
|
|
130
|
+
await login(page.context());
|
|
131
|
+
await mockAll(page);
|
|
132
|
+
await page.setViewportSize({ width: 1440, height: 900 });
|
|
133
|
+
await page.goto(`${BASE}/messages`, { waitUntil: 'domcontentloaded' });
|
|
134
|
+
// Wait for the actual mocked message content — NOT just any
|
|
135
|
+
// rounded-2xl, because the loading skeleton is also rounded-2xl
|
|
136
|
+
// (h-20 rounded-2xl bg-gray-800/20). Waiting for the literal
|
|
137
|
+
// mock body text pins that SWR has settled and rendered.
|
|
138
|
+
// See feedback_checker_scope_bug_vacuous_pass.md.
|
|
139
|
+
await page.waitForFunction(
|
|
140
|
+
() => document.body.textContent?.includes('ALPHA_TO_BETA_BODY') || false,
|
|
141
|
+
undefined,
|
|
142
|
+
// Turbopack sometimes rebuilds after a source edit — first mount
|
|
143
|
+
// can take 10-20s. Give the ceiling headroom for CI-cold dev servers.
|
|
144
|
+
{ timeout: 25_000 },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Read computedStyle from the bubble that contains a known mock text.
|
|
149
|
+
// Text-anchored so the query is stable against other rounded-2xl chrome
|
|
150
|
+
// (headers, nav, etc.) — and independent of DOM ordering.
|
|
151
|
+
async function bubbleStyleByContent(page: Page, needle: string) {
|
|
152
|
+
return await page.evaluate((n) => {
|
|
153
|
+
const bubbles = Array.from(document.querySelectorAll('div.rounded-2xl'));
|
|
154
|
+
const bubble = bubbles.find(
|
|
155
|
+
(el) =>
|
|
156
|
+
(el.textContent || '').includes(n) &&
|
|
157
|
+
Array.from(el.classList).some((c) => c.startsWith('bg-')),
|
|
158
|
+
);
|
|
159
|
+
if (!bubble) return null;
|
|
160
|
+
const cs = getComputedStyle(bubble);
|
|
161
|
+
return {
|
|
162
|
+
maxWidth: cs.maxWidth,
|
|
163
|
+
backgroundColor: cs.backgroundColor,
|
|
164
|
+
borderLeftColor: cs.borderLeftColor,
|
|
165
|
+
borderLeftWidth: cs.borderLeftWidth,
|
|
166
|
+
};
|
|
167
|
+
}, needle);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function broadcastBubbleStyle(page: Page) {
|
|
171
|
+
return bubbleStyleByContent(page, 'BROADCAST_BODY');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
test.describe('/messages · SPEC visual alignment', () => {
|
|
175
|
+
// ── 1. SHELL BG · SPEC §2 chat area #141826 ──────────────────
|
|
176
|
+
test('shell bg is SPEC #141826 (rgb 20,24,38)', async ({ page }) => {
|
|
177
|
+
await gotoMessages(page);
|
|
178
|
+
const bg = await page.evaluate(() => {
|
|
179
|
+
const shell = document.querySelector('[data-testid="messages-shell"]');
|
|
180
|
+
if (!shell) return null;
|
|
181
|
+
return getComputedStyle(shell).backgroundColor;
|
|
182
|
+
});
|
|
183
|
+
// #141826 rendered as computed rgb(20, 24, 38) by every browser.
|
|
184
|
+
expect(bg).toBe('rgb(20, 24, 38)');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── 2. BUBBLE MAX-WIDTH · SPEC §5 min(640px, 65%) ───────────
|
|
188
|
+
test('bubble max-width is SPEC min(640px, 65%)', async ({ page }) => {
|
|
189
|
+
await gotoMessages(page);
|
|
190
|
+
// Non-broadcast bubble — mock m1 body "ALPHA_TO_BETA_BODY".
|
|
191
|
+
const s = await bubbleStyleByContent(page, 'ALPHA_TO_BETA_BODY');
|
|
192
|
+
expect(s).not.toBeNull();
|
|
193
|
+
// Browser normalizes without spaces around the comma.
|
|
194
|
+
expect(s!.maxWidth).toBe('min(640px, 65%)');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ── 3. BUBBLE BG · SPEC §6 tokens (either #1E3A5F self or #1E293B other) ──
|
|
198
|
+
test('bubble bg is one of the SPEC bubble tokens', async ({ page }) => {
|
|
199
|
+
await gotoMessages(page);
|
|
200
|
+
// Non-broadcast bubble — mock m1 body "ALPHA_TO_BETA_BODY".
|
|
201
|
+
const s = await bubbleStyleByContent(page, 'ALPHA_TO_BETA_BODY');
|
|
202
|
+
expect(s).not.toBeNull();
|
|
203
|
+
// Accept-set = exactly the SPEC-defined bubble bg tokens; NOT a
|
|
204
|
+
// shape match, NOT "any dark color" (see
|
|
205
|
+
// feedback_tolerant_assertion_accepts_noncompliance.md +
|
|
206
|
+
// feedback_allowlist_must_be_exact_value_not_shape_match.md).
|
|
207
|
+
expect(['rgb(30, 41, 59)', 'rgb(30, 58, 95)']).toContain(s!.backgroundColor);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── 4. BROADCAST · SPEC §5 left 2px accent bar #7DD3FC ──────
|
|
211
|
+
test('broadcast bubble has 2px left accent bar (#7DD3FC)', async ({ page }) => {
|
|
212
|
+
await gotoMessages(page);
|
|
213
|
+
const s = await broadcastBubbleStyle(page);
|
|
214
|
+
expect(s).not.toBeNull();
|
|
215
|
+
// #1E293B = the "other" bubble token — broadcast is not self.
|
|
216
|
+
expect(s!.backgroundColor).toBe('rgb(30, 41, 59)');
|
|
217
|
+
// 2px accent bar in SPEC accent color #7DD3FC = rgb(125, 211, 252).
|
|
218
|
+
expect(s!.borderLeftWidth).toBe('2px');
|
|
219
|
+
expect(s!.borderLeftColor).toBe('rgb(125, 211, 252)');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── 5. TIMESTAMP · SPEC §5 11px / #64748B ───────────────────
|
|
223
|
+
test('timestamp is 11px in SPEC #64748B', async ({ page }) => {
|
|
224
|
+
await gotoMessages(page);
|
|
225
|
+
// The timestamp span sits at the end of each bubble header — pinned
|
|
226
|
+
// by its ml-auto + text-[11px] combo. Query by containing formatted
|
|
227
|
+
// time text ("ago" wording from timeAgo). We assert on ANY such
|
|
228
|
+
// span — pipeline gives us multiple.
|
|
229
|
+
const stamp = await page.evaluate(() => {
|
|
230
|
+
const spans = Array.from(document.querySelectorAll('span'));
|
|
231
|
+
// Pick the first span whose Tailwind class list includes the
|
|
232
|
+
// 11px arbitrary utility. That class is set only by the
|
|
233
|
+
// timestamp render sites we changed.
|
|
234
|
+
const el = spans.find((s) => s.className.includes('text-[11px]'));
|
|
235
|
+
if (!el) return null;
|
|
236
|
+
const cs = getComputedStyle(el);
|
|
237
|
+
return { fontSize: cs.fontSize, color: cs.color };
|
|
238
|
+
});
|
|
239
|
+
expect(stamp).not.toBeNull();
|
|
240
|
+
expect(stamp!.fontSize).toBe('11px');
|
|
241
|
+
// #64748B rendered as rgb(100, 116, 139).
|
|
242
|
+
expect(stamp!.color).toBe('rgb(100, 116, 139)');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3100';
|
|
4
|
+
|
|
5
|
+
// 通信龙 07-31: attribute-always-present guarantee for pin/mute.
|
|
6
|
+
//
|
|
7
|
+
// The wire that USED to emit `data-node-pinned="true" | undefined` made a
|
|
8
|
+
// gate script that counts "rows with the attribute" indistinguishable
|
|
9
|
+
// from a wire that was ripped out — both looked like zero. This spec
|
|
10
|
+
// enforces the new invariant AND proves the attribute tracks real state
|
|
11
|
+
// (not a hardcoded literal that would also pass the capability check).
|
|
12
|
+
//
|
|
13
|
+
// TWO assertions, both required:
|
|
14
|
+
// (A) capability — every rail row emits data-node-pinned AND
|
|
15
|
+
// data-node-muted, and the value is 'true' | 'false'
|
|
16
|
+
// (never absent, never something else). If a future
|
|
17
|
+
// refactor drops the emit, THIS reds.
|
|
18
|
+
// (B) real state — toggling pin on one row flips ITS attribute from
|
|
19
|
+
// 'false' to 'true' — and leaves the others alone.
|
|
20
|
+
// If someone hardcodes both to 'false' (would pass
|
|
21
|
+
// A), THIS reds. Same shape for muted.
|
|
22
|
+
//
|
|
23
|
+
// Rate-limit guard (10 logins / 15min / IP): per-worker cachedCookie
|
|
24
|
+
// (same as e2e-batch-list-actions.spec.ts).
|
|
25
|
+
|
|
26
|
+
let cachedCookie: string | undefined;
|
|
27
|
+
|
|
28
|
+
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
29
|
+
if (cachedCookie) {
|
|
30
|
+
await context.addCookies([{
|
|
31
|
+
name: 'anet_dashboard_session',
|
|
32
|
+
value: cachedCookie,
|
|
33
|
+
domain: new URL(BASE).hostname,
|
|
34
|
+
path: '/',
|
|
35
|
+
}]);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
39
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
40
|
+
});
|
|
41
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
42
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
43
|
+
if (m) {
|
|
44
|
+
cachedCookie = decodeURIComponent(m[1]);
|
|
45
|
+
await context.addCookies([{
|
|
46
|
+
name: 'anet_dashboard_session',
|
|
47
|
+
value: cachedCookie,
|
|
48
|
+
domain: new URL(BASE).hostname,
|
|
49
|
+
path: '/',
|
|
50
|
+
}]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const FIXTURE_SESSIONS = {
|
|
55
|
+
sessions: [
|
|
56
|
+
{ alias: 'node-alpha', status: 'idle', network_id: 'net_x' },
|
|
57
|
+
{ alias: 'node-beta', status: 'idle', network_id: 'net_x' },
|
|
58
|
+
{ alias: 'node-gamma', status: 'idle', network_id: 'net_x' },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
const FIXTURE_NODES = {
|
|
62
|
+
nodes: [
|
|
63
|
+
{ alias: 'node-alpha', node_id: 'n_alpha', team: null, tags: [], attrs_revision: 0 },
|
|
64
|
+
{ alias: 'node-beta', node_id: 'n_beta', team: null, tags: [], attrs_revision: 0 },
|
|
65
|
+
{ alias: 'node-gamma', node_id: 'n_gamma', team: null, tags: [], attrs_revision: 0 },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
async function stubHub(page: Page) {
|
|
70
|
+
await page.route('**/api/hub/status**', r =>
|
|
71
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_SESSIONS) }));
|
|
72
|
+
await page.route('**/api/hub/nodes**', r =>
|
|
73
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_NODES) }));
|
|
74
|
+
await page.route('**/api/hub/networks**', r =>
|
|
75
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ networks: [{ network_id: 'net_x', network_name: 'net_x' }] }) }));
|
|
76
|
+
await page.route('**/api/hub/health', r =>
|
|
77
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, version: '0.9.0-test', sse_connections: 0, sse_sessions: {} }) }));
|
|
78
|
+
await page.route('**/api/hub/tasks**', r =>
|
|
79
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [] }) }));
|
|
80
|
+
await page.route('**/api/hub/stats**', r =>
|
|
81
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
|
|
82
|
+
await page.route('**/api/hub/messages**', r =>
|
|
83
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
84
|
+
await page.route('**/api/hub/events**', r =>
|
|
85
|
+
r.fulfill({ status: 200, contentType: 'text/event-stream', body: '' }));
|
|
86
|
+
await page.route('**/api/anet/node-config**', r =>
|
|
87
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ model: '', flags: {}, channels: [], config_revision: 0 }) }));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test.describe('Pin/mute attr — always present + tracks real state', () => {
|
|
91
|
+
test('every rail row emits data-node-pinned AND data-node-muted with value ∈ {true,false}', async ({ page, context }) => {
|
|
92
|
+
await login(context);
|
|
93
|
+
await stubHub(page);
|
|
94
|
+
// Ensure no leftover state from prior runs would tint the "false"
|
|
95
|
+
// starting point — clear both storage keys before navigating.
|
|
96
|
+
await page.addInitScript(() => {
|
|
97
|
+
try { window.localStorage.removeItem('anet_chat_pin_v1'); } catch {}
|
|
98
|
+
try { window.localStorage.removeItem('anet_chat_mute_v1'); } catch {}
|
|
99
|
+
});
|
|
100
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
101
|
+
await expect(page.locator('[data-node-list-alias="node-alpha"]').first()).toBeVisible({ timeout: 15000 });
|
|
102
|
+
|
|
103
|
+
const rows = page.locator('[data-node-list-item]');
|
|
104
|
+
const count = await rows.count();
|
|
105
|
+
expect(count).toBeGreaterThanOrEqual(3); // premise: fixture landed
|
|
106
|
+
|
|
107
|
+
const pinnedVals: string[] = [];
|
|
108
|
+
const mutedVals: string[] = [];
|
|
109
|
+
for (let i = 0; i < count; i++) {
|
|
110
|
+
const row = rows.nth(i);
|
|
111
|
+
const pin = await row.getAttribute('data-node-pinned');
|
|
112
|
+
const mute = await row.getAttribute('data-node-muted');
|
|
113
|
+
pinnedVals.push(pin ?? '__ABSENT__');
|
|
114
|
+
mutedVals.push(mute ?? '__ABSENT__');
|
|
115
|
+
}
|
|
116
|
+
// Report cardinality alongside failure — if this reds, we want to
|
|
117
|
+
// see whether N of N were absent (wire ripped) vs 1 of N was 'foo'
|
|
118
|
+
// (someone typo'd the emit).
|
|
119
|
+
for (const v of pinnedVals) {
|
|
120
|
+
expect(v, `pinned attr values across ${count} rows: ${JSON.stringify(pinnedVals)}`)
|
|
121
|
+
.toMatch(/^(true|false)$/);
|
|
122
|
+
}
|
|
123
|
+
for (const v of mutedVals) {
|
|
124
|
+
expect(v, `muted attr values across ${count} rows: ${JSON.stringify(mutedVals)}`)
|
|
125
|
+
.toMatch(/^(true|false)$/);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('toggling pin on node-beta flips its data-node-pinned from false to true (others unchanged)', async ({ page, context }) => {
|
|
130
|
+
await login(context);
|
|
131
|
+
await stubHub(page);
|
|
132
|
+
await page.addInitScript(() => {
|
|
133
|
+
try { window.localStorage.removeItem('anet_chat_pin_v1'); } catch {}
|
|
134
|
+
try { window.localStorage.removeItem('anet_chat_mute_v1'); } catch {}
|
|
135
|
+
});
|
|
136
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
137
|
+
await expect(page.locator('[data-node-list-alias="node-beta"]').first()).toBeVisible({ timeout: 15000 });
|
|
138
|
+
|
|
139
|
+
// Baseline — all three rows read 'false' on pinned.
|
|
140
|
+
// This is the PREMISE, not the conclusion — if the fixture happens
|
|
141
|
+
// to have someone already pinned, the toggle would flip 'true' →
|
|
142
|
+
// 'false' and the conclusion assert reverses direction. Assert
|
|
143
|
+
// premise first to catch that.
|
|
144
|
+
const alphaBefore = await page.locator('[data-node-list-alias="node-alpha"]').getAttribute('data-node-pinned');
|
|
145
|
+
const betaBefore = await page.locator('[data-node-list-alias="node-beta"]').getAttribute('data-node-pinned');
|
|
146
|
+
const gammaBefore = await page.locator('[data-node-list-alias="node-gamma"]').getAttribute('data-node-pinned');
|
|
147
|
+
expect(alphaBefore).toBe('false');
|
|
148
|
+
expect(betaBefore).toBe('false');
|
|
149
|
+
expect(gammaBefore).toBe('false');
|
|
150
|
+
|
|
151
|
+
// Toggle pin on node-beta via the context menu (real user path —
|
|
152
|
+
// togglePin from ../lib/chat-pin). Right-click → ctx-pin.
|
|
153
|
+
await page.locator('[data-node-list-alias="node-beta"]').click({ button: 'right' });
|
|
154
|
+
await expect(page.locator('[data-testid="node-context-menu"]')).toBeVisible({ timeout: 3000 });
|
|
155
|
+
await page.locator('[data-testid="ctx-pin"]').click();
|
|
156
|
+
// Storage event → useSyncExternalStore rerender — allow a tick.
|
|
157
|
+
await page.waitForTimeout(200);
|
|
158
|
+
|
|
159
|
+
// 🔴 Core: node-beta flipped from 'false' → 'true'. If the attribute
|
|
160
|
+
// were hardcoded to the literal 'false' (which would pass the
|
|
161
|
+
// capability test above), THIS reds precisely.
|
|
162
|
+
const alphaAfter = await page.locator('[data-node-list-alias="node-alpha"]').getAttribute('data-node-pinned');
|
|
163
|
+
const betaAfter = await page.locator('[data-node-list-alias="node-beta"]').getAttribute('data-node-pinned');
|
|
164
|
+
const gammaAfter = await page.locator('[data-node-list-alias="node-gamma"]').getAttribute('data-node-pinned');
|
|
165
|
+
expect(betaAfter).toBe('true');
|
|
166
|
+
// Others must NOT have changed — sanity that the toggle scoped to
|
|
167
|
+
// the right alias (not e.g. accidentally pinning every row).
|
|
168
|
+
expect(alphaAfter).toBe('false');
|
|
169
|
+
expect(gammaAfter).toBe('false');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -40,16 +40,23 @@ const HUB_HEALTH = {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// Login rate limit is 10/15min/IP (app/lib/rate-limit.ts): with 4 spec
|
|
44
|
+
// files in one combined run, per-test logins blow the budget and late
|
|
45
|
+
// tests land on the sign-in page (false red). Login once per worker.
|
|
46
|
+
let cachedSessionCookie: string | null = null;
|
|
43
47
|
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
if (!cachedSessionCookie) {
|
|
49
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
50
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
51
|
+
});
|
|
52
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
53
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
54
|
+
if (m) cachedSessionCookie = decodeURIComponent(m[1]);
|
|
55
|
+
}
|
|
56
|
+
if (cachedSessionCookie) {
|
|
50
57
|
await context.addCookies([{
|
|
51
58
|
name: 'anet_dashboard_session',
|
|
52
|
-
value:
|
|
59
|
+
value: cachedSessionCookie,
|
|
53
60
|
domain: new URL(BASE).hostname,
|
|
54
61
|
path: '/',
|
|
55
62
|
}]);
|
|
@@ -153,22 +153,86 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
153
153
|
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
test('< 768px:
|
|
156
|
+
test('< 768px: two-level nav — list fullscreen, tap opens chat as a PAGE (no overlay), URL-addressable', async ({ page, context }) => {
|
|
157
157
|
await login(context);
|
|
158
158
|
await mockCommon(page);
|
|
159
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '0'));
|
|
159
160
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
160
161
|
await page.goto(`${BASE}/nodes`);
|
|
161
|
-
|
|
162
|
+
// Level 1: conversation list fullscreen; the table does NOT render;
|
|
163
|
+
// and mobile does NOT auto-select (desktop-only behavior — 通信龙:
|
|
164
|
+
// two correct behaviors, don't test one shape with the other's
|
|
165
|
+
// assertions). URL must stay /nodes.
|
|
166
|
+
const rail = page.locator('[data-testid="node-list-rail"]');
|
|
167
|
+
await expect(rail).toBeVisible({ timeout: 15000 });
|
|
168
|
+
await page.waitForTimeout(900);
|
|
169
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
170
|
+
const railBox = await rail.boundingBox();
|
|
171
|
+
expect(Math.abs(railBox!.width - 390)).toBeLessThanOrEqual(2);
|
|
172
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(0);
|
|
173
|
+
await expect(page.locator('[data-node-list-item]')).toHaveCount(3);
|
|
162
174
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
await expect(page
|
|
166
|
-
await expect(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
await expect(
|
|
170
|
-
|
|
175
|
+
// Level 2: tap → chat page (not overlay), back arrow present.
|
|
176
|
+
await page.locator('[data-node-list-item][data-node-list-alias="stage-b-1"]').click();
|
|
177
|
+
await expect(pane(page)).toBeVisible();
|
|
178
|
+
await expect(page.locator('.animate-slide-in')).toHaveCount(0);
|
|
179
|
+
const paneBox = await pane(page).boundingBox();
|
|
180
|
+
expect(Math.abs(paneBox!.width - 390)).toBeLessThanOrEqual(2);
|
|
181
|
+
await expect(rail).toHaveCount(0); // one level at a time
|
|
182
|
+
await expect(page.locator('[data-testid="chat-back"]')).toBeVisible();
|
|
171
183
|
await expect(page).toHaveURL(`${BASE}/nodes/stage-b-1`);
|
|
184
|
+
|
|
185
|
+
// Back arrow returns to the list.
|
|
186
|
+
await page.locator('[data-testid="chat-back"]').click();
|
|
187
|
+
await expect(rail).toBeVisible();
|
|
188
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('< 768px: back (arrow AND browser) restores list scroll position within 200px', async ({ page, context }) => {
|
|
192
|
+
await login(context);
|
|
193
|
+
await mockWide(page); // 60 nodes — enough to have a real scroll offset
|
|
194
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '0'));
|
|
195
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
196
|
+
await page.goto(`${BASE}/nodes`);
|
|
197
|
+
const scroll = page.locator('[data-testid="node-list-scroll"]');
|
|
198
|
+
await expect(page.locator('[data-node-list-item]')).toHaveCount(60, { timeout: 15000 });
|
|
199
|
+
|
|
200
|
+
const target = page.locator('[data-node-list-item][data-node-list-alias="stage-d-50"]');
|
|
201
|
+
await target.scrollIntoViewIfNeeded();
|
|
202
|
+
const before = await scroll.evaluate(el => el.scrollTop);
|
|
203
|
+
expect(before).toBeGreaterThan(500); // sanity: we really are deep in the list
|
|
204
|
+
|
|
205
|
+
await target.click();
|
|
206
|
+
await expect(pane(page)).toBeVisible();
|
|
207
|
+
await page.locator('[data-testid="chat-back"]').click();
|
|
208
|
+
await expect(scroll).toBeVisible();
|
|
209
|
+
const afterArrow = await scroll.evaluate(el => el.scrollTop);
|
|
210
|
+
expect(Math.abs(afterArrow - before)).toBeLessThanOrEqual(200);
|
|
211
|
+
|
|
212
|
+
// Browser back behaves the same (判据: 行为一致).
|
|
213
|
+
await target.click();
|
|
214
|
+
await expect(pane(page)).toBeVisible();
|
|
215
|
+
await page.goBack();
|
|
216
|
+
await expect(scroll).toBeVisible();
|
|
217
|
+
const afterBack = await scroll.evaluate(el => el.scrollTop);
|
|
218
|
+
expect(Math.abs(afterBack - before)).toBeLessThanOrEqual(200);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('< 768px: manage mode still reaches the table, with a way back to conversations', async ({ page, context }) => {
|
|
222
|
+
await login(context);
|
|
223
|
+
await mockCommon(page);
|
|
224
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
|
|
225
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
226
|
+
await page.goto(`${BASE}/nodes`);
|
|
227
|
+
// Fact-side positive: the table is genuinely there in manage mode
|
|
228
|
+
// (guards against "mobile hides table" being green because the table
|
|
229
|
+
// broke everywhere).
|
|
230
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
|
|
231
|
+
await expect(page.locator('[data-testid="manage-view-toggle-mobile"]')).toBeVisible();
|
|
232
|
+
|
|
233
|
+
await page.locator('[data-testid="manage-view-toggle-mobile"]').click();
|
|
234
|
+
await expect(page.locator('[data-testid="node-list-rail"]')).toBeVisible();
|
|
235
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(0);
|
|
172
236
|
});
|
|
173
237
|
|
|
174
238
|
test('slow-load regression: 1.5s-delayed first /api/hub/status on a deep link → no crash page', async ({ page, context }) => {
|