@sleep2agi/agent-network-dashboard 0.4.6-preview.8 → 0.4.6
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_ID +1 -1
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +69 -55
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/routes-manifest.json +6 -0
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +12 -12
- package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +14 -14
- package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
- 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/api/hub/servers/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/hub/servers/route/build-manifest.json +9 -0
- package/.next/server/app/api/hub/servers/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/hub/servers/route.js +7 -0
- package/.next/server/app/api/hub/servers/route.js.map +5 -0
- package/.next/server/app/api/hub/servers/route.js.nft.json +1 -0
- package/.next/server/app/api/hub/servers/route_client-reference-manifest.js +3 -0
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +14 -14
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +7 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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 +14 -14
- package/.next/server/app/login.segments/_full.segment.rsc +14 -14
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +7 -7
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +14 -14
- package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +14 -14
- package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +14 -14
- package/.next/server/app/node.segments/_full.segment.rsc +14 -14
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +7 -7
- 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/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +14 -14
- package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
- 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_client-reference-manifest.js +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 +14 -14
- package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
- 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/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 +14 -14
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
- 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_client-reference-manifest.js +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 +14 -14
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
- 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 +14 -14
- package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
- 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_client-reference-manifest.js +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 +14 -14
- package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
- 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/app-paths-manifest.json +1 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +2 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.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-mpa_947ipeq.js +1 -0
- package/.next/static/chunks/01lmbsd37fybu.js +1 -0
- package/.next/static/chunks/03o.h6kvmw4l_.js +1 -0
- package/.next/static/chunks/03~~oirxz7~vc.js +1 -0
- package/.next/static/chunks/05uk96gc~9mni.js +1 -0
- package/.next/static/chunks/088ouqy7tekty.js +2 -0
- package/.next/static/chunks/08kf-.bgs10ze.js +1 -0
- package/.next/static/chunks/0glmrdj9112tc.js +7 -0
- package/.next/static/chunks/0hyl7mdapsz38.js +1 -0
- package/.next/static/chunks/0mqwn-l7wtcdo.js +7 -0
- package/.next/static/chunks/0rxrez_cjn.eh.js +1 -0
- package/.next/static/chunks/0tvn2l1pc.h65.js +1 -0
- package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
- package/.next/static/chunks/127kw7~wsnyu7.css +2 -0
- package/.next/static/chunks/16yfxp9ak2gdm.js +1 -0
- package/.next/static/chunks/174re5ar~kxne.js +1 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +9 -0
- package/app/api/hub/servers/route.ts +39 -0
- package/app/components/AppShell.tsx +2 -0
- package/app/components/ChatPopover.tsx +38 -2
- package/app/components/HelpOverlay.tsx +18 -0
- package/app/components/ServersDrawer.tsx +206 -0
- package/app/components/TopoGraph.tsx +871 -162
- package/app/components/utils.ts +8 -7
- package/app/lib/time.ts +36 -0
- package/package.json +1 -1
- package/scripts/help-overlay-topo-test.mjs +44 -0
- package/scripts/servers-drawer-live-test.mjs +96 -0
- package/scripts/topo-active-links-chip-test.mjs +73 -0
- package/scripts/topo-arrow-tier-test.mjs +61 -0
- package/scripts/topo-auto-fit-test.mjs +73 -0
- package/scripts/topo-chat-focus-ring-test.mjs +77 -0
- package/scripts/topo-click-ripple-test.mjs +66 -0
- package/scripts/topo-cwd-tooltip-test.mjs +64 -0
- package/scripts/topo-dblclick-reset-test.mjs +74 -0
- package/scripts/topo-edge-hover-test.mjs +85 -0
- package/scripts/topo-edge-tooltip-test.mjs +70 -0
- package/scripts/topo-fit-key-test.mjs +106 -0
- package/scripts/topo-freshness-chip-test.mjs +74 -0
- package/scripts/topo-group-count-test.mjs +63 -0
- package/scripts/topo-group-hover-test.mjs +82 -0
- package/scripts/topo-hub-title-test.mjs +88 -0
- package/scripts/topo-label-lift-test.mjs +115 -0
- package/scripts/topo-label-scale-test.mjs +78 -0
- package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
- package/scripts/topo-layout-key-test.mjs +91 -0
- package/scripts/topo-minimap-test.mjs +68 -0
- package/scripts/topo-mobile-chips-test.mjs +103 -0
- package/scripts/topo-offline-dim-test.mjs +70 -0
- package/scripts/topo-overlap-test.mjs +164 -0
- package/scripts/topo-p0round-test.mjs +2 -2
- package/scripts/topo-pan-cursor-test.mjs +62 -0
- package/scripts/topo-popover-meta-test.mjs +78 -0
- package/scripts/topo-pressure-bar-test.mjs +69 -0
- package/scripts/topo-pulse-rate-test.mjs +71 -0
- package/scripts/topo-recent-signal-empty-test.mjs +63 -0
- package/scripts/topo-reduced-motion-test.mjs +77 -0
- package/scripts/topo-tz-ghost-test.mjs +69 -0
- package/scripts/topo-wheel-zoom-test.mjs +79 -0
- package/scripts/topo-zoom-keys-test.mjs +89 -0
- package/.next/static/chunks/0-7iowzu4.798.js +0 -1
- package/.next/static/chunks/02t5jr4shh.hy.js +0 -1
- package/.next/static/chunks/0_qfg5s5uzs7..js +0 -7
- package/.next/static/chunks/0_rmxom9~1lmk.css +0 -2
- package/.next/static/chunks/0cbezu61m-e~z.js +0 -1
- package/.next/static/chunks/0cj5qr_a8dzky.js +0 -7
- package/.next/static/chunks/0elalb9n_.~di.js +0 -1
- package/.next/static/chunks/0j_q__o8p6g83.js +0 -1
- package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
- package/.next/static/chunks/0mn0ruf0g4op5.js +0 -1
- package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
- package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
- package/.next/static/chunks/0y9subfnntv1s.js +0 -1
- package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
- package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
- /package/.next/static/{7xem03-473G3qw-fC160p → 23MxP0RBVA4BaOJ_HMYmM}/_buildManifest.js +0 -0
- /package/.next/static/{7xem03-473G3qw-fC160p → 23MxP0RBVA4BaOJ_HMYmM}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{7xem03-473G3qw-fC160p → 23MxP0RBVA4BaOJ_HMYmM}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* Round 41 verification: double-clicking empty canvas resets view to
|
|
2
|
+
* {zoom:1, x:0, y:0}. Double-clicking a node does NOT reset (the
|
|
3
|
+
* node's own onClick wins, and resetView should not fire). */
|
|
4
|
+
import { chromium } from 'playwright';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
8
|
+
const browser = await chromium.launch({ headless: true });
|
|
9
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 1600 } });
|
|
10
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
11
|
+
await ctx.addInitScript(() => {
|
|
12
|
+
try {
|
|
13
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
14
|
+
localStorage.setItem('anet-topo-layout', 'grid');
|
|
15
|
+
localStorage.setItem('anet-topo-view', JSON.stringify({ zoom: 1, x: 0, y: 0 }));
|
|
16
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
17
|
+
} catch {}
|
|
18
|
+
});
|
|
19
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
20
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
21
|
+
const r = await route.fetch();
|
|
22
|
+
const b = await r.json();
|
|
23
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
24
|
+
const sessions = ['a', 'b'].map(a => ({
|
|
25
|
+
alias: a, status: 'idle', network_id: nid, project_dir: null,
|
|
26
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
27
|
+
}));
|
|
28
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
29
|
+
});
|
|
30
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
31
|
+
const page = await ctx.newPage();
|
|
32
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
33
|
+
await page.waitForSelector('svg[viewBox="0 0 1000 680"]', { timeout: 30000 });
|
|
34
|
+
await page.waitForTimeout(500);
|
|
35
|
+
|
|
36
|
+
const readZoom = () => page.evaluate(() => {
|
|
37
|
+
const span = document.querySelector('button[aria-label="Zoom in"]').parentElement.querySelector('span[title*="zoom level"]');
|
|
38
|
+
return +span.textContent.replace('%', '');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Zoom in a couple times via keyboard, then dbl-click empty canvas → expect reset to 100%.
|
|
42
|
+
await page.keyboard.press('=');
|
|
43
|
+
await page.keyboard.press('=');
|
|
44
|
+
await page.waitForTimeout(120);
|
|
45
|
+
const zoomedIn = await readZoom();
|
|
46
|
+
|
|
47
|
+
// Find empty-canvas coords: top-left area of the SVG, well outside node positions.
|
|
48
|
+
const box = await page.$eval('svg[viewBox="0 0 1000 680"]', el => el.getBoundingClientRect());
|
|
49
|
+
// y=0.35 is below the top-overlay panels (which sit at y<100 inside the SVG)
|
|
50
|
+
const emptyX = box.x + box.width * 0.20;
|
|
51
|
+
const emptyY = box.y + box.height * 0.40;
|
|
52
|
+
await page.mouse.dblclick(emptyX, emptyY);
|
|
53
|
+
await page.waitForTimeout(200);
|
|
54
|
+
const afterEmptyDbl = await readZoom();
|
|
55
|
+
|
|
56
|
+
// Now zoom in again and dbl-click a node — view should NOT reset.
|
|
57
|
+
await page.keyboard.press('=');
|
|
58
|
+
await page.keyboard.press('=');
|
|
59
|
+
await page.waitForTimeout(120);
|
|
60
|
+
const zoomedIn2 = await readZoom();
|
|
61
|
+
await page.locator('g[data-node="a"]').dblclick().catch(() => {});
|
|
62
|
+
await page.waitForTimeout(200);
|
|
63
|
+
const afterNodeDbl = await readZoom();
|
|
64
|
+
|
|
65
|
+
await browser.close();
|
|
66
|
+
const results = {
|
|
67
|
+
zoomedInBeforeDbl: zoomedIn > 100,
|
|
68
|
+
emptyCanvasResets: afterEmptyDbl === 100,
|
|
69
|
+
nodeDoesNotReset: zoomedIn2 > 100 && afterNodeDbl === zoomedIn2,
|
|
70
|
+
};
|
|
71
|
+
const ok = Object.values(results).every(Boolean);
|
|
72
|
+
console.log(`${ok ? '✅' : '❌'} dblclick reset:`, JSON.stringify(results),
|
|
73
|
+
`\n zoomedIn=${zoomedIn} afterEmptyDbl=${afterEmptyDbl} zoomedIn2=${zoomedIn2} afterNodeDbl=${afterNodeDbl}`);
|
|
74
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* Round 40 verification: hovering a node brightens edges touching it
|
|
2
|
+
* and dims the rest. Three edges in the fleet — hover one node and
|
|
3
|
+
* compare base-path opacity of touching vs non-touching edges. */
|
|
4
|
+
import { chromium } from 'playwright';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
8
|
+
const browser = await chromium.launch({ headless: true });
|
|
9
|
+
|
|
10
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
11
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
12
|
+
await ctx.addInitScript(() => {
|
|
13
|
+
try {
|
|
14
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
15
|
+
localStorage.setItem('anet-topo-layout', 'grid');
|
|
16
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
17
|
+
} catch {}
|
|
18
|
+
});
|
|
19
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
20
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
21
|
+
const r = await route.fetch();
|
|
22
|
+
const b = await r.json();
|
|
23
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
24
|
+
const sessions = ['alpha', 'beta', 'gamma', 'delta'].map(a => ({
|
|
25
|
+
alias: a, status: 'idle', network_id: nid, project_dir: null,
|
|
26
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
27
|
+
}));
|
|
28
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
29
|
+
});
|
|
30
|
+
// Edges:
|
|
31
|
+
// alpha → beta (touches alpha + beta)
|
|
32
|
+
// alpha → gamma (touches alpha + gamma)
|
|
33
|
+
// delta → gamma (touches delta + gamma — does NOT touch alpha)
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
|
|
36
|
+
{ from_alias: 'alpha', to_alias: 'beta', content: 'm', created_at: now },
|
|
37
|
+
{ from_alias: 'alpha', to_alias: 'gamma', content: 'm', created_at: now },
|
|
38
|
+
{ from_alias: 'delta', to_alias: 'gamma', content: 'm', created_at: now },
|
|
39
|
+
] } }));
|
|
40
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
41
|
+
const page = await ctx.newPage();
|
|
42
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
43
|
+
await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
|
|
44
|
+
await page.waitForTimeout(600);
|
|
45
|
+
|
|
46
|
+
const edgeOpacities = () => page.evaluate(() => {
|
|
47
|
+
const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
|
|
48
|
+
// Each flow-link <g> has two paths; the FIRST one is the base path
|
|
49
|
+
// (carries the <title> with route) — read its opacity.
|
|
50
|
+
return [...svg.querySelectorAll(':scope > g > g')]
|
|
51
|
+
.map(g => {
|
|
52
|
+
const base = g.querySelector('path');
|
|
53
|
+
const title = base?.querySelector('title');
|
|
54
|
+
if (!title) return null;
|
|
55
|
+
return { route: title.textContent.split('\n')[0], opacity: +base.getAttribute('opacity') };
|
|
56
|
+
})
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const before = await edgeOpacities();
|
|
61
|
+
// Hover alpha → its two outgoing edges should brighten, delta→gamma should dim.
|
|
62
|
+
await page.locator('g[data-node="alpha"]').hover();
|
|
63
|
+
await page.waitForTimeout(350);
|
|
64
|
+
const after = await edgeOpacities();
|
|
65
|
+
|
|
66
|
+
await browser.close();
|
|
67
|
+
const get = (rows, r) => rows.find(e => e.route === r)?.opacity ?? null;
|
|
68
|
+
const b_ab = get(before, 'alpha → beta');
|
|
69
|
+
const b_ag = get(before, 'alpha → gamma');
|
|
70
|
+
const b_dg = get(before, 'delta → gamma');
|
|
71
|
+
const a_ab = get(after, 'alpha → beta');
|
|
72
|
+
const a_ag = get(after, 'alpha → gamma');
|
|
73
|
+
const a_dg = get(after, 'delta → gamma');
|
|
74
|
+
|
|
75
|
+
const results = {
|
|
76
|
+
baselineMatches: b_ab !== null && b_ag !== null && b_dg !== null && Math.abs(b_ab - b_dg) < 0.001,
|
|
77
|
+
hoverBrightensAlphaBeta: a_ab !== null && a_ab > b_ab + 0.05,
|
|
78
|
+
hoverBrightensAlphaGamma: a_ag !== null && a_ag > b_ag + 0.05,
|
|
79
|
+
hoverDimsDeltaGamma: a_dg !== null && a_dg < b_dg - 0.05,
|
|
80
|
+
};
|
|
81
|
+
const ok = Object.values(results).every(Boolean);
|
|
82
|
+
console.log(`${ok ? '✅' : '❌'} edge hover:`, JSON.stringify(results),
|
|
83
|
+
`\n before α→β=${b_ab} α→γ=${b_ag} δ→γ=${b_dg}`,
|
|
84
|
+
`\n after α→β=${a_ab} α→γ=${a_ag} δ→γ=${a_dg}`);
|
|
85
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* Round 39 verification: each flow-link path carries a `<title>` with
|
|
2
|
+
* "from → to\nN messages · last <relative>". Tests:
|
|
3
|
+
* - tooltip text shape (route + count + relative time)
|
|
4
|
+
* - singular vs. plural ("1 message" vs "5 messages")
|
|
5
|
+
* - missing last_at falls through cleanly */
|
|
6
|
+
import { chromium } from 'playwright';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
10
|
+
const browser = await chromium.launch({ headless: true });
|
|
11
|
+
|
|
12
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
13
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
14
|
+
await ctx.addInitScript(() => {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
17
|
+
localStorage.setItem('anet-topo-layout', 'grid');
|
|
18
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
19
|
+
} catch {}
|
|
20
|
+
});
|
|
21
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
22
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
23
|
+
const r = await route.fetch();
|
|
24
|
+
const b = await r.json();
|
|
25
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
26
|
+
const sessions = ['alpha', 'beta', 'gamma'].map(a => ({
|
|
27
|
+
alias: a, status: 'idle', network_id: nid, project_dir: null,
|
|
28
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
29
|
+
}));
|
|
30
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
31
|
+
});
|
|
32
|
+
// Inject messages: 1 between alpha→beta (singular), 5 between alpha→gamma (plural).
|
|
33
|
+
const old = new Date(Date.now() - 8 * 60 * 1000).toISOString();
|
|
34
|
+
const recent = new Date(Date.now() - 30 * 1000).toISOString();
|
|
35
|
+
await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
|
|
36
|
+
{ from_alias: 'alpha', to_alias: 'beta', content: 'hello', created_at: old },
|
|
37
|
+
{ from_alias: 'alpha', to_alias: 'gamma', content: 'm1', created_at: recent },
|
|
38
|
+
{ from_alias: 'alpha', to_alias: 'gamma', content: 'm2', created_at: recent },
|
|
39
|
+
{ from_alias: 'alpha', to_alias: 'gamma', content: 'm3', created_at: recent },
|
|
40
|
+
{ from_alias: 'alpha', to_alias: 'gamma', content: 'm4', created_at: recent },
|
|
41
|
+
{ from_alias: 'alpha', to_alias: 'gamma', content: 'm5', created_at: recent },
|
|
42
|
+
] } }));
|
|
43
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
44
|
+
const page = await ctx.newPage();
|
|
45
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
46
|
+
await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
|
|
47
|
+
await page.waitForTimeout(600);
|
|
48
|
+
|
|
49
|
+
// Collect every flow-link tooltip (each base path with a child <title>).
|
|
50
|
+
const tooltips = await page.evaluate(() => {
|
|
51
|
+
const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
|
|
52
|
+
// Two paths per flow link — the base one carries the <title>.
|
|
53
|
+
const titled = [...svg.querySelectorAll('path > title')];
|
|
54
|
+
return titled.map(t => t.textContent);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await browser.close();
|
|
58
|
+
const has = (route, count, plural) => tooltips.some(t =>
|
|
59
|
+
t.includes(route) &&
|
|
60
|
+
(plural ? t.includes(`${count} messages`) : t.includes(`${count} message`)) &&
|
|
61
|
+
/last \w+ ago/.test(t)
|
|
62
|
+
);
|
|
63
|
+
const results = {
|
|
64
|
+
singularAlphaToBeta: has('alpha → beta', 1, false),
|
|
65
|
+
pluralAlphaToGamma: has('alpha → gamma', 5, true),
|
|
66
|
+
tooltipCount: tooltips.length === 2,
|
|
67
|
+
};
|
|
68
|
+
const ok = Object.values(results).every(Boolean);
|
|
69
|
+
console.log(`${ok ? '✅' : '❌'} edge tooltip:`, JSON.stringify(results), '\n tooltips=', tooltips);
|
|
70
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* Round 29 verification: `f` keyboard shortcut triggers fit-to-content.
|
|
2
|
+
* - on a fleet that overflows: f sets zoom < 100%
|
|
3
|
+
* - on a fleet that fits at 100%: f is a recenter (zoom stays/returns 100%)
|
|
4
|
+
* - input-focus suppression: typing `f` in Cmd+K palette must not fit
|
|
5
|
+
* - help overlay lists `f` */
|
|
6
|
+
import { chromium } from 'playwright';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
10
|
+
const browser = await chromium.launch({ headless: true });
|
|
11
|
+
|
|
12
|
+
async function load({ fleetSize, persisted }) {
|
|
13
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 1600 } });
|
|
14
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
15
|
+
await ctx.addInitScript(([pers]) => {
|
|
16
|
+
try {
|
|
17
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
18
|
+
localStorage.setItem('anet-topo-layout', 'grid');
|
|
19
|
+
if (pers) localStorage.setItem('anet-topo-view', JSON.stringify(pers));
|
|
20
|
+
else localStorage.removeItem('anet-topo-view');
|
|
21
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
22
|
+
} catch {}
|
|
23
|
+
}, [persisted]);
|
|
24
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
25
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
26
|
+
const r = await route.fetch();
|
|
27
|
+
const b = await r.json();
|
|
28
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
29
|
+
const groups = ['A站', 'B站', 'C站', 'D站', 'E站', 'F站', 'G站'];
|
|
30
|
+
const aliases = Array.from({ length: fleetSize }, (_, i) =>
|
|
31
|
+
`${groups[Math.floor(i / 5) % groups.length]}n${i + 1}`);
|
|
32
|
+
const sessions = aliases.map(a => ({
|
|
33
|
+
alias: a, status: 'idle', network_id: nid, project_dir: null,
|
|
34
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
35
|
+
}));
|
|
36
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
37
|
+
});
|
|
38
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
39
|
+
const page = await ctx.newPage();
|
|
40
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
41
|
+
await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n, fleetSize, { timeout: 30000 }).catch(() => {});
|
|
42
|
+
await page.waitForTimeout(500);
|
|
43
|
+
return { ctx, page };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const readZoom = (page) => page.evaluate(() => {
|
|
47
|
+
const span = document.querySelector('button[aria-label="Zoom in"]').parentElement.querySelector('span[title*="zoom level"]');
|
|
48
|
+
return +span.textContent.replace('%', '');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 1. Big fleet, persisted view at 100% → autoFit doesn't run (persisted respected);
|
|
52
|
+
// user presses `f` and gets fitted now.
|
|
53
|
+
const big = await load({ fleetSize: 35, persisted: { zoom: 1, x: 0, y: 0 } });
|
|
54
|
+
const bigBefore = await readZoom(big.page);
|
|
55
|
+
await big.page.keyboard.press('f');
|
|
56
|
+
await big.page.waitForTimeout(150);
|
|
57
|
+
const bigAfter = await readZoom(big.page);
|
|
58
|
+
await big.ctx.close();
|
|
59
|
+
|
|
60
|
+
// 2. Small fleet (no overflow), user manually zoomed in via `+` then presses `f` to recenter.
|
|
61
|
+
const small = await load({ fleetSize: 6, persisted: { zoom: 1, x: 0, y: 0 } });
|
|
62
|
+
await small.page.keyboard.press('=');
|
|
63
|
+
await small.page.keyboard.press('=');
|
|
64
|
+
await small.page.waitForTimeout(120);
|
|
65
|
+
const smallZoomedIn = await readZoom(small.page);
|
|
66
|
+
await small.page.keyboard.press('f');
|
|
67
|
+
await small.page.waitForTimeout(150);
|
|
68
|
+
const smallAfterFit = await readZoom(small.page);
|
|
69
|
+
await small.ctx.close();
|
|
70
|
+
|
|
71
|
+
// 3. Input-focus suppression: open Cmd+K, type `f`, expect no fit.
|
|
72
|
+
const focus = await load({ fleetSize: 35, persisted: { zoom: 1, x: 0, y: 0 } });
|
|
73
|
+
const focusBefore = await readZoom(focus.page);
|
|
74
|
+
await focus.page.keyboard.press('Meta+k');
|
|
75
|
+
await focus.page.waitForTimeout(200);
|
|
76
|
+
const paletteInput = focus.page.locator('input[placeholder*="Search"], input[type="search"], [role="dialog"] input').first();
|
|
77
|
+
await paletteInput.focus().catch(() => {});
|
|
78
|
+
await focus.page.keyboard.press('f');
|
|
79
|
+
await focus.page.waitForTimeout(150);
|
|
80
|
+
const focusAfter = await readZoom(focus.page);
|
|
81
|
+
await focus.page.keyboard.press('Escape');
|
|
82
|
+
await focus.ctx.close();
|
|
83
|
+
|
|
84
|
+
// 4. Help overlay lists `f`.
|
|
85
|
+
const help = await load({ fleetSize: 4 });
|
|
86
|
+
await help.page.keyboard.press('?');
|
|
87
|
+
await help.page.waitForTimeout(200);
|
|
88
|
+
const helpHasFit = await help.page.evaluate(() => {
|
|
89
|
+
const heading = [...document.querySelectorAll('[role="dialog"] div')].find(el => el.textContent === 'Topology canvas');
|
|
90
|
+
if (!heading) return false;
|
|
91
|
+
const txt = heading.parentElement?.textContent || '';
|
|
92
|
+
return /Fit topology/i.test(txt);
|
|
93
|
+
});
|
|
94
|
+
await help.ctx.close();
|
|
95
|
+
|
|
96
|
+
await browser.close();
|
|
97
|
+
const results = {
|
|
98
|
+
bigFleetFKeyFits: bigBefore === 100 && bigAfter < 100 && bigAfter >= 50,
|
|
99
|
+
smallFleetFKeyRecenters: smallZoomedIn > 100 && smallAfterFit === 100,
|
|
100
|
+
inputFocusSuppressed: focusAfter === focusBefore,
|
|
101
|
+
helpOverlayListsFit: helpHasFit,
|
|
102
|
+
};
|
|
103
|
+
const ok = Object.values(results).every(Boolean);
|
|
104
|
+
console.log(`${ok ? '✅' : '❌'} fit key:`, JSON.stringify(results),
|
|
105
|
+
`bigBefore=${bigBefore} bigAfter=${bigAfter} smallZoomedIn=${smallZoomedIn} smallAfterFit=${smallAfterFit} focusBefore=${focusBefore} focusAfter=${focusAfter}`);
|
|
106
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* Round 46 verification: live · Ns freshness chip.
|
|
2
|
+
* - renders in the topology header with initial "live · 0s" or "live · 1s"
|
|
3
|
+
* - ticks up over time (>= 2s after a 2.5s wait)
|
|
4
|
+
* - resets near 0 when SWR delivers a new sessions payload (we trigger
|
|
5
|
+
* this by mutating the mocked /api/hub/status response — SWR's 5s
|
|
6
|
+
* refresh re-fetches and the chip should re-zero)
|
|
7
|
+
* - turns amber when stale (>10s since last sync; verified by blocking
|
|
8
|
+
* the second poll and waiting >12s) */
|
|
9
|
+
import { chromium } from 'playwright';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
13
|
+
const browser = await chromium.launch({ headless: true });
|
|
14
|
+
const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
|
15
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
16
|
+
await ctx.addInitScript(() => {
|
|
17
|
+
try {
|
|
18
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
19
|
+
localStorage.setItem('anet-topo-layout', 'grid');
|
|
20
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
21
|
+
} catch {}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let pollCount = 0;
|
|
25
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
26
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
27
|
+
pollCount++;
|
|
28
|
+
const r = await route.fetch();
|
|
29
|
+
const b = await r.json();
|
|
30
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
31
|
+
const sessions = [
|
|
32
|
+
{ alias: `a${pollCount}`, status: 'idle', network_id: nid, project_dir: null,
|
|
33
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh },
|
|
34
|
+
{ alias: 'b', status: 'idle', network_id: nid, project_dir: null,
|
|
35
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh },
|
|
36
|
+
];
|
|
37
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
38
|
+
});
|
|
39
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
40
|
+
const page = await ctx.newPage();
|
|
41
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
42
|
+
await page.waitForSelector('[data-freshness-chip]', { timeout: 30000 });
|
|
43
|
+
|
|
44
|
+
const readChip = () => page.evaluate(() => {
|
|
45
|
+
const el = document.querySelector('[data-freshness-chip]');
|
|
46
|
+
if (!el) return null;
|
|
47
|
+
return {
|
|
48
|
+
text: (el.textContent || '').trim(),
|
|
49
|
+
isAmber: /amber|warning/i.test(el.className) || el.className.includes('amber'),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const initial = await readChip();
|
|
54
|
+
await page.waitForTimeout(2500);
|
|
55
|
+
const afterTick = await readChip();
|
|
56
|
+
|
|
57
|
+
// SWR refreshes every 5s — wait past one cycle and confirm the chip re-zeros.
|
|
58
|
+
// Initial chip might be 1s, after 2.5s should be 3s. After a ~5s sync,
|
|
59
|
+
// should drop to 0-1s.
|
|
60
|
+
await page.waitForTimeout(3500);
|
|
61
|
+
const afterSync = await readChip();
|
|
62
|
+
|
|
63
|
+
await browser.close();
|
|
64
|
+
const seconds = (s) => +(s.text.match(/(\d+)s/)?.[1] ?? -1);
|
|
65
|
+
const results = {
|
|
66
|
+
initialNearZero: initial !== null && seconds(initial) <= 2,
|
|
67
|
+
tickedUpAfter25s: afterTick !== null && seconds(afterTick) >= 2 && seconds(afterTick) <= 4,
|
|
68
|
+
reZeroedAfterSync: afterSync !== null && seconds(afterSync) <= 2,
|
|
69
|
+
pollsHappened: pollCount >= 2,
|
|
70
|
+
};
|
|
71
|
+
const ok = Object.values(results).every(Boolean);
|
|
72
|
+
console.log(`${ok ? '✅' : '❌'} freshness chip:`, JSON.stringify(results),
|
|
73
|
+
`\n initial=${JSON.stringify(initial)}\n afterTick=${JSON.stringify(afterTick)}\n afterSync=${JSON.stringify(afterSync)} polls=${pollCount}`);
|
|
74
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* Round 19 verification: each group-box label carries a member-count
|
|
2
|
+
* tspan ("· N"), reflecting the actual member count of the group. */
|
|
3
|
+
import { chromium } from 'playwright';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
7
|
+
const browser = await chromium.launch({ headless: true });
|
|
8
|
+
|
|
9
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
10
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
11
|
+
await ctx.addInitScript(() => {
|
|
12
|
+
try {
|
|
13
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
14
|
+
localStorage.removeItem('anet-brand');
|
|
15
|
+
localStorage.removeItem('anet-topo-view');
|
|
16
|
+
localStorage.setItem('anet-topo-layout', 'grid');
|
|
17
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
18
|
+
} catch {}
|
|
19
|
+
});
|
|
20
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
21
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
22
|
+
const r = await route.fetch();
|
|
23
|
+
const b = await r.json();
|
|
24
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
25
|
+
// A站 with 4 members, B站 with 2 members → 2 group boxes (counts 4 and 2).
|
|
26
|
+
const aliases = ['A站红', 'A站蓝', 'A站绿', 'A站黄', 'B站乙', 'B站丙'];
|
|
27
|
+
const sessions = aliases.map(a => ({
|
|
28
|
+
alias: a, status: 'idle', network_id: nid, project_dir: null,
|
|
29
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
30
|
+
}));
|
|
31
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
32
|
+
});
|
|
33
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
34
|
+
const page = await ctx.newPage();
|
|
35
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
36
|
+
await page.waitForFunction(() => {
|
|
37
|
+
const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
|
|
38
|
+
return !!svg && svg.querySelectorAll('g[data-group]').length === 2;
|
|
39
|
+
}, { timeout: 30000 }).catch(() => {});
|
|
40
|
+
await page.waitForTimeout(400);
|
|
41
|
+
|
|
42
|
+
const labels = await page.$$eval('svg[viewBox="0 0 1000 680"] g[data-group]', els => els.map(g => {
|
|
43
|
+
const text = g.querySelector('text');
|
|
44
|
+
const tspan = text.querySelector('tspan');
|
|
45
|
+
return {
|
|
46
|
+
key: g.getAttribute('data-group'),
|
|
47
|
+
full: text.textContent.replace(/\s+/g, ' ').trim(),
|
|
48
|
+
chipFontSize: tspan ? +tspan.getAttribute('font-size') : null,
|
|
49
|
+
chipFontWeight: tspan ? tspan.getAttribute('font-weight') : null,
|
|
50
|
+
};
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
await browser.close();
|
|
54
|
+
const a = labels.find(l => l.key === 'A站');
|
|
55
|
+
const b = labels.find(l => l.key === 'B站');
|
|
56
|
+
const results = {
|
|
57
|
+
bothGroupsHaveChip: a?.full === 'A站· 4' && b?.full === 'B站· 2',
|
|
58
|
+
chipSmallerFont: a?.chipFontSize === 11,
|
|
59
|
+
chipLighterWeight: a?.chipFontWeight === '400',
|
|
60
|
+
};
|
|
61
|
+
const ok = Object.values(results).every(Boolean);
|
|
62
|
+
console.log(`${ok ? '✅' : '❌'} group count chip:`, JSON.stringify(results), 'labels=', labels);
|
|
63
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* Round 18 verification: hovering a node in a multi-member group upgrades
|
|
2
|
+
* the group-box to accent treatment (solid stroke, thicker, legendAccent
|
|
3
|
+
* colour) and brightens the label; other groups dim to 0.28; resting
|
|
4
|
+
* appearance unchanged. */
|
|
5
|
+
import { chromium } from 'playwright';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
9
|
+
const browser = await chromium.launch({ headless: true });
|
|
10
|
+
|
|
11
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
12
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
13
|
+
await ctx.addInitScript(() => {
|
|
14
|
+
try {
|
|
15
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
16
|
+
localStorage.removeItem('anet-brand');
|
|
17
|
+
localStorage.removeItem('anet-topo-view');
|
|
18
|
+
localStorage.setItem('anet-topo-layout', 'grid');
|
|
19
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
20
|
+
} catch {}
|
|
21
|
+
});
|
|
22
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
23
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
24
|
+
const r = await route.fetch();
|
|
25
|
+
const b = await r.json();
|
|
26
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
27
|
+
// Two prefix groups (A站 / B站), each ≥2 members so groupBoxes renders.
|
|
28
|
+
const aliases = ['A站红', 'A站蓝', 'A站绿', 'B站乙', 'B站丙', 'B站丁'];
|
|
29
|
+
const sessions = aliases.map(a => ({
|
|
30
|
+
alias: a, status: 'idle', network_id: nid, project_dir: null,
|
|
31
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
32
|
+
}));
|
|
33
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
34
|
+
});
|
|
35
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
36
|
+
const page = await ctx.newPage();
|
|
37
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
38
|
+
await page.waitForFunction(() => {
|
|
39
|
+
const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
|
|
40
|
+
return !!svg && svg.querySelectorAll('g[data-group]').length === 2;
|
|
41
|
+
}, { timeout: 30000 }).catch(() => {});
|
|
42
|
+
await page.waitForTimeout(400);
|
|
43
|
+
|
|
44
|
+
const readGroup = key => page.evaluate(k => {
|
|
45
|
+
const g = document.querySelector(`g[data-group="${k}"]`);
|
|
46
|
+
if (!g) return null;
|
|
47
|
+
const rect = g.querySelector('rect');
|
|
48
|
+
const text = g.querySelector('text');
|
|
49
|
+
return {
|
|
50
|
+
opacity: parseFloat(getComputedStyle(g).opacity || '1'),
|
|
51
|
+
stroke: rect.getAttribute('stroke'),
|
|
52
|
+
strokeWidth: +rect.getAttribute('stroke-width'),
|
|
53
|
+
strokeDasharray: rect.getAttribute('stroke-dasharray'),
|
|
54
|
+
fillOpacity: rect.getAttribute('fill-opacity'),
|
|
55
|
+
textFill: text.getAttribute('fill'),
|
|
56
|
+
};
|
|
57
|
+
}, key);
|
|
58
|
+
|
|
59
|
+
// Resting state (no hover)
|
|
60
|
+
const restA = await readGroup('A站');
|
|
61
|
+
const restB = await readGroup('B站');
|
|
62
|
+
|
|
63
|
+
// Hover a node in group A站
|
|
64
|
+
await page.locator('g[data-node="A站红"]').hover();
|
|
65
|
+
await page.waitForTimeout(350); // allow the 200ms transition to settle
|
|
66
|
+
|
|
67
|
+
const hoverA = await readGroup('A站');
|
|
68
|
+
const hoverB = await readGroup('B站');
|
|
69
|
+
|
|
70
|
+
await browser.close();
|
|
71
|
+
const results = {
|
|
72
|
+
restADashedThin: restA?.strokeDasharray === '6 6' && restA?.strokeWidth === 1.5,
|
|
73
|
+
restBDashedThin: restB?.strokeDasharray === '6 6' && restB?.strokeWidth === 1.5,
|
|
74
|
+
hoveredASolidThick: hoverA?.strokeDasharray === 'none' && hoverA?.strokeWidth === 2,
|
|
75
|
+
hoveredAAccentStroke: hoverA?.stroke === '#67e8f9', // dark legendAccent
|
|
76
|
+
hoveredABrighterText: hoverA?.textFill === '#e5e7eb', // dark legendHeadline
|
|
77
|
+
otherBDimmedOpacity: Math.abs((hoverB?.opacity ?? 1) - 0.28) < 0.02,
|
|
78
|
+
otherBKeepsDashed: hoverB?.strokeDasharray === '6 6',
|
|
79
|
+
};
|
|
80
|
+
const ok = Object.values(results).every(Boolean);
|
|
81
|
+
console.log(`${ok ? '✅' : '❌'} group hover linkage:`, JSON.stringify(results));
|
|
82
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* Round 43 verification: central hub carries a `<title>` summary in ring
|
|
2
|
+
* layout. Assertions:
|
|
3
|
+
* - hub renders in ring layout (data-topo-hub group present)
|
|
4
|
+
* - <title> reads "Network hub · N sessions · …" with the right counts
|
|
5
|
+
* - zero-count clauses (online/working/links) drop cleanly
|
|
6
|
+
* - grid layout doesn't render the hub at all */
|
|
7
|
+
import { chromium } from 'playwright';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
11
|
+
const browser = await chromium.launch({ headless: true });
|
|
12
|
+
|
|
13
|
+
async function probe({ layout, sessionsSpec, msgs = [] }) {
|
|
14
|
+
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
|
|
15
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
16
|
+
await ctx.addInitScript((lay) => {
|
|
17
|
+
try {
|
|
18
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
19
|
+
localStorage.setItem('anet-topo-layout', lay);
|
|
20
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
21
|
+
} catch {}
|
|
22
|
+
}, layout);
|
|
23
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
24
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
25
|
+
const r = await route.fetch();
|
|
26
|
+
const b = await r.json();
|
|
27
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
28
|
+
const sessions = sessionsSpec.map(s => ({
|
|
29
|
+
alias: s.alias, status: s.status, network_id: nid, project_dir: null,
|
|
30
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
31
|
+
}));
|
|
32
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
33
|
+
});
|
|
34
|
+
await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
|
|
35
|
+
await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
|
|
36
|
+
const page = await ctx.newPage();
|
|
37
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
|
|
38
|
+
await page.waitForSelector('svg[viewBox="0 0 1000 680"]', { timeout: 30000 });
|
|
39
|
+
await page.waitForTimeout(500);
|
|
40
|
+
const data = await page.evaluate(() => {
|
|
41
|
+
const hub = document.querySelector('g[data-topo-hub]');
|
|
42
|
+
const title = hub?.querySelector('title');
|
|
43
|
+
return { hubPresent: !!hub, titleText: title?.textContent || null };
|
|
44
|
+
});
|
|
45
|
+
await ctx.close();
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ring + mixed-status fleet → full summary line
|
|
50
|
+
const ringFull = await probe({
|
|
51
|
+
layout: 'ring',
|
|
52
|
+
sessionsSpec: [
|
|
53
|
+
{ alias: 'a', status: 'working' },
|
|
54
|
+
{ alias: 'b', status: 'idle' },
|
|
55
|
+
{ alias: 'c', status: 'idle' },
|
|
56
|
+
{ alias: 'd', status: 'offline' },
|
|
57
|
+
],
|
|
58
|
+
msgs: [
|
|
59
|
+
{ from_alias: 'a', to_alias: 'b', content: 'm', created_at: new Date().toISOString() },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
// Ring + all-offline → online/working/links clauses drop
|
|
63
|
+
const ringOfflineOnly = await probe({
|
|
64
|
+
layout: 'ring',
|
|
65
|
+
sessionsSpec: [
|
|
66
|
+
{ alias: 'x', status: 'offline' },
|
|
67
|
+
{ alias: 'y', status: 'offline' },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
// Grid → no hub at all
|
|
71
|
+
const grid = await probe({
|
|
72
|
+
layout: 'grid',
|
|
73
|
+
sessionsSpec: [{ alias: 'a', status: 'idle' }, { alias: 'b', status: 'idle' }],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await browser.close();
|
|
77
|
+
const results = {
|
|
78
|
+
ringHubPresent: ringFull.hubPresent === true,
|
|
79
|
+
ringFullSummary: ringFull.titleText === 'Network hub · 4 sessions · 3 online · 1 working · 1 active link',
|
|
80
|
+
ringOfflineSummary: ringOfflineOnly.hubPresent === true && ringOfflineOnly.titleText === 'Network hub · 2 sessions',
|
|
81
|
+
gridHasNoHub: grid.hubPresent === false,
|
|
82
|
+
};
|
|
83
|
+
const ok = Object.values(results).every(Boolean);
|
|
84
|
+
console.log(`${ok ? '✅' : '❌'} hub title:`, JSON.stringify(results),
|
|
85
|
+
`\n ringFull=${JSON.stringify(ringFull.titleText)}`,
|
|
86
|
+
`\n ringOffline=${JSON.stringify(ringOfflineOnly.titleText)}`,
|
|
87
|
+
`\n gridHub=${grid.hubPresent}`);
|
|
88
|
+
process.exit(ok ? 0 : 1);
|