code-coordinator 0.5.46__py3-none-any.whl
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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for the dev-only component gallery (#1546) — the acceptance
|
|
3
|
+
* bar is "every primitive renders correctly in both themes; the gallery
|
|
4
|
+
* proves it," so this renders the gallery, checks every primitive's marker
|
|
5
|
+
* text is on screen, and exercises the live theme toggle end-to-end
|
|
6
|
+
* (button click → data-theme attribute → localStorage persistence).
|
|
7
|
+
*
|
|
8
|
+
* Deliberately does not open the Radix-portalled overlays (Dialog/
|
|
9
|
+
* DropdownMenu/Sheet/Tooltip) here -- doing so needs jsdom polyfills for
|
|
10
|
+
* pointer capture / scrollIntoView that Radix's own test suite already
|
|
11
|
+
* covers; this test's job is "does our styling wrapper mount," not
|
|
12
|
+
* "does Radix work."
|
|
13
|
+
*/
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
15
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
16
|
+
import Gallery from '../Gallery'
|
|
17
|
+
import { THEME_STORAGE_KEY } from '@/components/ui/theme-provider'
|
|
18
|
+
|
|
19
|
+
describe('Gallery', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
window.localStorage.clear()
|
|
22
|
+
document.documentElement.removeAttribute('data-theme')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
window.localStorage.clear()
|
|
27
|
+
document.documentElement.removeAttribute('data-theme')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders every primitive section', () => {
|
|
31
|
+
render(<Gallery />)
|
|
32
|
+
|
|
33
|
+
// Button ("Primary"/"Outline"/"Destructive" also label buttons in the
|
|
34
|
+
// side-by-side preview and the toast triggers further down, hence
|
|
35
|
+
// getAllByRole rather than getByRole for those.
|
|
36
|
+
expect(screen.getAllByRole('button', { name: 'Primary' }).length).toBeGreaterThanOrEqual(1)
|
|
37
|
+
expect(screen.getAllByRole('button', { name: 'Destructive' }).length).toBeGreaterThanOrEqual(1)
|
|
38
|
+
expect(screen.getByRole('button', { name: 'Disabled' })).toBeDisabled()
|
|
39
|
+
|
|
40
|
+
// Badge ("Passed"/"Failed" also appear in the side-by-side preview).
|
|
41
|
+
expect(screen.getAllByText('Passed').length).toBeGreaterThanOrEqual(1)
|
|
42
|
+
expect(screen.getAllByText('Failed').length).toBeGreaterThanOrEqual(1)
|
|
43
|
+
|
|
44
|
+
// Card
|
|
45
|
+
expect(screen.getByText('W1-1: design tokens')).toBeInTheDocument()
|
|
46
|
+
|
|
47
|
+
// Tabs
|
|
48
|
+
expect(screen.getByRole('tab', { name: /Overview/ })).toHaveAttribute(
|
|
49
|
+
'aria-selected',
|
|
50
|
+
'true',
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Dialog / dropdown-menu / sheet / tooltip triggers
|
|
54
|
+
expect(screen.getByRole('button', { name: 'Open dialog' })).toBeInTheDocument()
|
|
55
|
+
expect(screen.getByRole('button', { name: 'More actions' })).toBeInTheDocument()
|
|
56
|
+
expect(screen.getByRole('button', { name: 'Open action sheet' })).toBeInTheDocument()
|
|
57
|
+
expect(screen.getByRole('button', { name: 'Help' })).toBeInTheDocument()
|
|
58
|
+
|
|
59
|
+
// Empty state
|
|
60
|
+
expect(screen.getByText('No issues match this filter')).toBeInTheDocument()
|
|
61
|
+
|
|
62
|
+
// Toast triggers
|
|
63
|
+
expect(screen.getByRole('button', { name: 'Success' })).toBeInTheDocument()
|
|
64
|
+
|
|
65
|
+
// Side-by-side dark/light preview (plus the header's current-theme label,
|
|
66
|
+
// which also reads "dark" -- hence getAllByText, not getByText).
|
|
67
|
+
expect(screen.getAllByText('dark').length).toBeGreaterThanOrEqual(1)
|
|
68
|
+
expect(screen.getByText('light')).toBeInTheDocument()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('the live theme toggle flips data-theme and persists the choice', () => {
|
|
72
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, 'dark')
|
|
73
|
+
render(<Gallery />)
|
|
74
|
+
|
|
75
|
+
expect(document.documentElement.getAttribute('data-theme')).toBe('dark')
|
|
76
|
+
|
|
77
|
+
fireEvent.click(screen.getByRole('button', { name: 'Switch to light theme' }))
|
|
78
|
+
|
|
79
|
+
expect(document.documentElement.getAttribute('data-theme')).toBe('light')
|
|
80
|
+
expect(window.localStorage.getItem(THEME_STORAGE_KEY)).toBe('light')
|
|
81
|
+
expect(screen.getByRole('button', { name: 'Switch to dark theme' })).toBeInTheDocument()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for the Home screen's "Live sessions" section (#1067) and
|
|
3
|
+
* the Active tab's in-progress/done grouping (#1218).
|
|
4
|
+
*
|
|
5
|
+
* Mocks @/api/client entirely; wraps renders in a QueryClientProvider +
|
|
6
|
+
* MemoryRouter so useQuery / useNavigate work correctly, matching
|
|
7
|
+
* Detail.test.tsx's pattern.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
10
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
11
|
+
import userEvent from '@testing-library/user-event'
|
|
12
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
13
|
+
import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
|
14
|
+
import Home from '@/components/Home'
|
|
15
|
+
import { ThemeProvider } from '@/components/ui/theme-provider'
|
|
16
|
+
import { type PipelineView, type SessionInfo } from '@/api/client'
|
|
17
|
+
|
|
18
|
+
// ── Mock API client + navigate ────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
vi.mock('@/api/client', () => ({
|
|
21
|
+
fetchPipeline: vi.fn(),
|
|
22
|
+
fetchSessions: vi.fn(),
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
import { fetchPipeline, fetchSessions } from '@/api/client'
|
|
26
|
+
|
|
27
|
+
const navigateSpy = vi.fn()
|
|
28
|
+
vi.mock('react-router-dom', async () => {
|
|
29
|
+
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
|
|
30
|
+
return { ...actual, useNavigate: () => navigateSpy }
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// ── Test helpers ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function makeView(overrides: Partial<PipelineView> = {}): PipelineView {
|
|
40
|
+
return {
|
|
41
|
+
assignment_id: 'work-1',
|
|
42
|
+
issue_number: 42,
|
|
43
|
+
issue_title: 'Fix the thing',
|
|
44
|
+
repo_name: 'myrepo',
|
|
45
|
+
machine_name: 'laptop',
|
|
46
|
+
current_stage: 'coding',
|
|
47
|
+
stages: [
|
|
48
|
+
{ name: 'coding', status: 'active', is_current: true },
|
|
49
|
+
{ name: 'review', status: 'waiting', is_current: false },
|
|
50
|
+
{ name: 'smoke', status: 'waiting', is_current: false },
|
|
51
|
+
{ name: 'merge', status: 'waiting', is_current: false },
|
|
52
|
+
],
|
|
53
|
+
available_gates: [],
|
|
54
|
+
progress_pct: 10,
|
|
55
|
+
review_findings_pending: false,
|
|
56
|
+
review_verdict: null,
|
|
57
|
+
// #1456: null = the reviewer's own verdict stands (the normal case).
|
|
58
|
+
review_verdict_original: null,
|
|
59
|
+
review_verdict_override_reason: null,
|
|
60
|
+
review_findings_body: null,
|
|
61
|
+
test_verdict: null,
|
|
62
|
+
needs_attention: false,
|
|
63
|
+
needs_attention_reason: null,
|
|
64
|
+
needs_attention_detail: null,
|
|
65
|
+
finished_at: null,
|
|
66
|
+
...overrides,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeSession(overrides: Partial<SessionInfo> = {}): SessionInfo {
|
|
71
|
+
return {
|
|
72
|
+
session_id: 'work-2',
|
|
73
|
+
session_name: 'coord-work-2',
|
|
74
|
+
machine: 'desktop',
|
|
75
|
+
host: 'desktop.tailnet.ts.net',
|
|
76
|
+
repo: 'otherrepo',
|
|
77
|
+
issue: 7,
|
|
78
|
+
issue_title: 'Live session issue',
|
|
79
|
+
stage: 'work',
|
|
80
|
+
status: 'running',
|
|
81
|
+
attached: false,
|
|
82
|
+
pane_dead: false,
|
|
83
|
+
...overrides,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createTestQueryClient() {
|
|
88
|
+
return new QueryClient({
|
|
89
|
+
defaultOptions: {
|
|
90
|
+
queries: { retry: false, refetchInterval: false, refetchOnWindowFocus: false },
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderHome() {
|
|
96
|
+
return render(
|
|
97
|
+
<ThemeProvider>
|
|
98
|
+
<QueryClientProvider client={createTestQueryClient()}>
|
|
99
|
+
<MemoryRouter initialEntries={['/']}>
|
|
100
|
+
<Routes>
|
|
101
|
+
<Route path="/" element={<Home />} />
|
|
102
|
+
</Routes>
|
|
103
|
+
</MemoryRouter>
|
|
104
|
+
</QueryClientProvider>
|
|
105
|
+
</ThemeProvider>,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Live sessions section ─────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
describe('Home — live sessions section', () => {
|
|
112
|
+
it('renders live session cards above the pipeline list', async () => {
|
|
113
|
+
vi.mocked(fetchPipeline).mockResolvedValue([makeView()])
|
|
114
|
+
vi.mocked(fetchSessions).mockResolvedValue([makeSession()])
|
|
115
|
+
|
|
116
|
+
renderHome()
|
|
117
|
+
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.getByText('Live sessions')).toBeInTheDocument()
|
|
120
|
+
})
|
|
121
|
+
expect(screen.getByText('Live session issue')).toBeInTheDocument()
|
|
122
|
+
|
|
123
|
+
// The "Live sessions" heading appears before the pipeline card in DOM order.
|
|
124
|
+
const heading = screen.getByText('Live sessions')
|
|
125
|
+
const pipelineCard = screen.getByText('Fix the thing')
|
|
126
|
+
expect(
|
|
127
|
+
heading.compareDocumentPosition(pipelineCard) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
128
|
+
).toBeTruthy()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('renders no "Live sessions" heading when there are no live sessions', async () => {
|
|
132
|
+
vi.mocked(fetchPipeline).mockResolvedValue([makeView()])
|
|
133
|
+
vi.mocked(fetchSessions).mockResolvedValue([])
|
|
134
|
+
|
|
135
|
+
renderHome()
|
|
136
|
+
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(screen.getByText('Fix the thing')).toBeInTheDocument()
|
|
139
|
+
})
|
|
140
|
+
expect(screen.queryByText('Live sessions')).not.toBeInTheDocument()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('navigates to /terminal/:sessionId when a session card is tapped', async () => {
|
|
144
|
+
vi.mocked(fetchPipeline).mockResolvedValue([])
|
|
145
|
+
vi.mocked(fetchSessions).mockResolvedValue([makeSession()])
|
|
146
|
+
|
|
147
|
+
renderHome()
|
|
148
|
+
|
|
149
|
+
const card = await screen.findByText('Live session issue')
|
|
150
|
+
await userEvent.click(card)
|
|
151
|
+
|
|
152
|
+
expect(navigateSpy).toHaveBeenCalledWith('/terminal/work-2')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// ── Active tab: in-progress/done grouping (#1218) ───────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe('Home — Active tab grouping', () => {
|
|
159
|
+
it('renders in-progress items expanded, needs-me first then running', async () => {
|
|
160
|
+
// Incoming (API) order deliberately scrambled: running item first, then
|
|
161
|
+
// a needs-me (failed, offers retry) item — expect needs-me to sort first.
|
|
162
|
+
const running = makeView({
|
|
163
|
+
assignment_id: 'a-running',
|
|
164
|
+
issue_title: 'Running item',
|
|
165
|
+
current_stage: 'coding',
|
|
166
|
+
available_gates: [],
|
|
167
|
+
})
|
|
168
|
+
const needsMe = makeView({
|
|
169
|
+
assignment_id: 'a-needs-me',
|
|
170
|
+
issue_title: 'Failed item needing retry',
|
|
171
|
+
current_stage: 'failed',
|
|
172
|
+
available_gates: [{ action: 'retry', label: 'Retry', endpoint: '/api/pipeline/action' }],
|
|
173
|
+
})
|
|
174
|
+
vi.mocked(fetchPipeline).mockResolvedValue([running, needsMe])
|
|
175
|
+
vi.mocked(fetchSessions).mockResolvedValue([])
|
|
176
|
+
|
|
177
|
+
renderHome()
|
|
178
|
+
|
|
179
|
+
const needsMeCard = await screen.findByText('Failed item needing retry')
|
|
180
|
+
const runningCard = screen.getByText('Running item')
|
|
181
|
+
expect(
|
|
182
|
+
needsMeCard.compareDocumentPosition(runningCard) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
183
|
+
).toBeTruthy()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('collapses done-ish items into a "Work done (N)" section by default', async () => {
|
|
187
|
+
const running = makeView({
|
|
188
|
+
assignment_id: 'a-running',
|
|
189
|
+
issue_title: 'Running item',
|
|
190
|
+
current_stage: 'coding',
|
|
191
|
+
})
|
|
192
|
+
const done1 = makeView({
|
|
193
|
+
assignment_id: 'a-done-1',
|
|
194
|
+
issue_title: 'Finished thing one',
|
|
195
|
+
current_stage: 'done',
|
|
196
|
+
available_gates: [{ action: 'enqueue', label: 'Queue', endpoint: '/api/pipeline/action' }],
|
|
197
|
+
finished_at: 100,
|
|
198
|
+
})
|
|
199
|
+
const done2 = makeView({
|
|
200
|
+
assignment_id: 'a-done-2',
|
|
201
|
+
issue_title: 'Finished thing two',
|
|
202
|
+
current_stage: 'review_done',
|
|
203
|
+
available_gates: [{ action: 'enqueue', label: 'Queue', endpoint: '/api/pipeline/action' }],
|
|
204
|
+
finished_at: 200,
|
|
205
|
+
})
|
|
206
|
+
vi.mocked(fetchPipeline).mockResolvedValue([running, done1, done2])
|
|
207
|
+
vi.mocked(fetchSessions).mockResolvedValue([])
|
|
208
|
+
|
|
209
|
+
renderHome()
|
|
210
|
+
|
|
211
|
+
await waitFor(() => {
|
|
212
|
+
expect(screen.getByText('Running item')).toBeInTheDocument()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Collapsed by default: header with count shown, done cards not rendered.
|
|
216
|
+
expect(screen.getByText('Work done (2)')).toBeInTheDocument()
|
|
217
|
+
expect(screen.queryByText('Finished thing one')).not.toBeInTheDocument()
|
|
218
|
+
expect(screen.queryByText('Finished thing two')).not.toBeInTheDocument()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('expands the Work done section on tap, sorted by recency descending', async () => {
|
|
222
|
+
const older = makeView({
|
|
223
|
+
assignment_id: 'a-done-older',
|
|
224
|
+
issue_title: 'Older done item',
|
|
225
|
+
current_stage: 'done',
|
|
226
|
+
available_gates: [{ action: 'enqueue', label: 'Queue', endpoint: '/api/pipeline/action' }],
|
|
227
|
+
finished_at: 100,
|
|
228
|
+
})
|
|
229
|
+
const newer = makeView({
|
|
230
|
+
assignment_id: 'a-done-newer',
|
|
231
|
+
issue_title: 'Newer done item',
|
|
232
|
+
current_stage: 'smoke_passed',
|
|
233
|
+
available_gates: [{ action: 'enqueue', label: 'Queue', endpoint: '/api/pipeline/action' }],
|
|
234
|
+
finished_at: 200,
|
|
235
|
+
})
|
|
236
|
+
vi.mocked(fetchPipeline).mockResolvedValue([older, newer])
|
|
237
|
+
vi.mocked(fetchSessions).mockResolvedValue([])
|
|
238
|
+
|
|
239
|
+
renderHome()
|
|
240
|
+
|
|
241
|
+
const toggle = await screen.findByText('Work done (2)')
|
|
242
|
+
await userEvent.click(toggle)
|
|
243
|
+
|
|
244
|
+
const newerCard = await screen.findByText('Newer done item')
|
|
245
|
+
const olderCard = await screen.findByText('Older done item')
|
|
246
|
+
expect(
|
|
247
|
+
newerCard.compareDocumentPosition(olderCard) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
248
|
+
).toBeTruthy()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('keeps the "Needs me" tab as a flat, ungrouped list', async () => {
|
|
252
|
+
const done = makeView({
|
|
253
|
+
assignment_id: 'a-done',
|
|
254
|
+
issue_title: 'Finished needing merge',
|
|
255
|
+
current_stage: 'done',
|
|
256
|
+
available_gates: [{ action: 'enqueue', label: 'Queue', endpoint: '/api/pipeline/action' }],
|
|
257
|
+
finished_at: 100,
|
|
258
|
+
})
|
|
259
|
+
vi.mocked(fetchPipeline).mockResolvedValue([done])
|
|
260
|
+
vi.mocked(fetchSessions).mockResolvedValue([])
|
|
261
|
+
|
|
262
|
+
renderHome()
|
|
263
|
+
|
|
264
|
+
const tab = await screen.findByRole('tab', { name: /Needs me/i })
|
|
265
|
+
await userEvent.click(tab)
|
|
266
|
+
|
|
267
|
+
// Rendered directly — no collapsed "Work done" wrapper on this tab.
|
|
268
|
+
expect(await screen.findByText('Finished needing merge')).toBeInTheDocument()
|
|
269
|
+
expect(screen.queryByText(/Work done \(/)).not.toBeInTheDocument()
|
|
270
|
+
})
|
|
271
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for MobileKeyBar (#1070, #1299) -- the acceptance criterion
|
|
3
|
+
* is "each soft-key emits the correct escape / control bytes to the terminal
|
|
4
|
+
* write path (mocked)". `onSend` stands in for that write path here;
|
|
5
|
+
* `Terminal.test.tsx` covers the real (fake) WebSocket wiring end-to-end.
|
|
6
|
+
*
|
|
7
|
+
* Scroll-mode tests (#1299): assert the toggle, swapped key rows, and the
|
|
8
|
+
* line-submit-exits-scroll-first invariant.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
11
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
12
|
+
import userEvent from '@testing-library/user-event'
|
|
13
|
+
import MobileKeyBar, { KEY_BYTES } from '@/components/MobileKeyBar'
|
|
14
|
+
|
|
15
|
+
const noop = () => {}
|
|
16
|
+
|
|
17
|
+
describe('MobileKeyBar', () => {
|
|
18
|
+
it.each([
|
|
19
|
+
['Escape', KEY_BYTES.escape],
|
|
20
|
+
['Tab', KEY_BYTES.tab],
|
|
21
|
+
['Ctrl-C', KEY_BYTES.ctrlC],
|
|
22
|
+
['Slash', KEY_BYTES.slash],
|
|
23
|
+
['Left', KEY_BYTES.left],
|
|
24
|
+
['Up', KEY_BYTES.up],
|
|
25
|
+
['Down', KEY_BYTES.down],
|
|
26
|
+
['Right', KEY_BYTES.right],
|
|
27
|
+
['Enter', KEY_BYTES.enter],
|
|
28
|
+
])('%s sends %j', async (ariaLabel, expectedBytes) => {
|
|
29
|
+
const onSend = vi.fn()
|
|
30
|
+
render(<MobileKeyBar onSend={onSend} onControl={noop} />)
|
|
31
|
+
|
|
32
|
+
await userEvent.click(screen.getByRole('button', { name: ariaLabel }))
|
|
33
|
+
|
|
34
|
+
expect(onSend).toHaveBeenCalledOnce()
|
|
35
|
+
expect(onSend).toHaveBeenCalledWith(expectedBytes)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('sends the typed line followed by a carriage return on submit, then clears the field', async () => {
|
|
39
|
+
const onSend = vi.fn()
|
|
40
|
+
render(<MobileKeyBar onSend={onSend} onControl={noop} />)
|
|
41
|
+
|
|
42
|
+
const input = screen.getByRole('textbox', { name: 'Command line' })
|
|
43
|
+
await userEvent.type(input, '/status')
|
|
44
|
+
await userEvent.click(screen.getByRole('button', { name: 'Send line' }))
|
|
45
|
+
|
|
46
|
+
expect(onSend).toHaveBeenCalledOnce()
|
|
47
|
+
expect(onSend).toHaveBeenCalledWith('/status\r')
|
|
48
|
+
expect(input).toHaveValue('')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('does not send an empty line', async () => {
|
|
52
|
+
const onSend = vi.fn()
|
|
53
|
+
render(<MobileKeyBar onSend={onSend} onControl={noop} />)
|
|
54
|
+
|
|
55
|
+
await userEvent.click(screen.getByRole('button', { name: 'Send line' }))
|
|
56
|
+
|
|
57
|
+
expect(onSend).not.toHaveBeenCalled()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("submits the line on the field's own Enter key, same as tapping Send", async () => {
|
|
61
|
+
const onSend = vi.fn()
|
|
62
|
+
render(<MobileKeyBar onSend={onSend} onControl={noop} />)
|
|
63
|
+
|
|
64
|
+
const input = screen.getByRole('textbox', { name: 'Command line' })
|
|
65
|
+
await userEvent.type(input, 'ls{Enter}')
|
|
66
|
+
|
|
67
|
+
expect(onSend).toHaveBeenCalledOnce()
|
|
68
|
+
expect(onSend).toHaveBeenCalledWith('ls\r')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('does not steal focus from the line-input when a soft key is tapped', async () => {
|
|
72
|
+
const onSend = vi.fn()
|
|
73
|
+
render(<MobileKeyBar onSend={onSend} onControl={noop} />)
|
|
74
|
+
|
|
75
|
+
const input = screen.getByRole('textbox', { name: 'Command line' })
|
|
76
|
+
input.focus()
|
|
77
|
+
expect(input).toHaveFocus()
|
|
78
|
+
|
|
79
|
+
await userEvent.click(screen.getByRole('button', { name: 'Ctrl-C' }))
|
|
80
|
+
|
|
81
|
+
expect(input).toHaveFocus()
|
|
82
|
+
expect(onSend).toHaveBeenCalledOnce()
|
|
83
|
+
expect(onSend).toHaveBeenCalledWith(KEY_BYTES.ctrlC)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('MobileKeyBar — scroll mode (#1299)', () => {
|
|
88
|
+
it('tapping Scroll emits onControl("enter") and shows the SCROLL badge', async () => {
|
|
89
|
+
const onControl = vi.fn()
|
|
90
|
+
render(<MobileKeyBar onSend={noop} onControl={onControl} />)
|
|
91
|
+
|
|
92
|
+
// Scroll button exists in normal mode
|
|
93
|
+
await userEvent.click(screen.getByRole('button', { name: 'Scroll' }))
|
|
94
|
+
|
|
95
|
+
expect(onControl).toHaveBeenCalledOnce()
|
|
96
|
+
expect(onControl).toHaveBeenCalledWith('enter')
|
|
97
|
+
|
|
98
|
+
// SCROLL badge is now visible
|
|
99
|
+
expect(screen.getByRole('status', { name: 'Scroll mode active' })).toBeInTheDocument()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('tapping Exit Scroll emits onControl("exit") and hides the badge', async () => {
|
|
103
|
+
const onControl = vi.fn()
|
|
104
|
+
render(<MobileKeyBar onSend={noop} onControl={onControl} />)
|
|
105
|
+
|
|
106
|
+
// Enter scroll mode first
|
|
107
|
+
await userEvent.click(screen.getByRole('button', { name: 'Scroll' }))
|
|
108
|
+
onControl.mockClear()
|
|
109
|
+
|
|
110
|
+
// Exit scroll mode
|
|
111
|
+
await userEvent.click(screen.getByRole('button', { name: 'Exit Scroll' }))
|
|
112
|
+
|
|
113
|
+
expect(onControl).toHaveBeenCalledOnce()
|
|
114
|
+
expect(onControl).toHaveBeenCalledWith('exit')
|
|
115
|
+
|
|
116
|
+
// Badge is gone; normal keys are back
|
|
117
|
+
expect(screen.queryByRole('status', { name: 'Scroll mode active' })).not.toBeInTheDocument()
|
|
118
|
+
expect(screen.getByRole('button', { name: 'Scroll' })).toBeInTheDocument()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('scroll controls emit the correct actions while in scroll mode', async () => {
|
|
122
|
+
const onControl = vi.fn()
|
|
123
|
+
render(<MobileKeyBar onSend={noop} onControl={onControl} />)
|
|
124
|
+
|
|
125
|
+
await userEvent.click(screen.getByRole('button', { name: 'Scroll' }))
|
|
126
|
+
onControl.mockClear()
|
|
127
|
+
|
|
128
|
+
const cases: Array<[string, string]> = [
|
|
129
|
+
['Page Up', 'page-up'],
|
|
130
|
+
['Page Down', 'page-down'],
|
|
131
|
+
['Top', 'top'],
|
|
132
|
+
['Bottom', 'bottom'],
|
|
133
|
+
]
|
|
134
|
+
for (const [label, action] of cases) {
|
|
135
|
+
await userEvent.click(screen.getByRole('button', { name: label }))
|
|
136
|
+
expect(onControl).toHaveBeenLastCalledWith(action)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('normal key rows are absent while in scroll mode', async () => {
|
|
141
|
+
render(<MobileKeyBar onSend={noop} onControl={noop} />)
|
|
142
|
+
|
|
143
|
+
await userEvent.click(screen.getByRole('button', { name: 'Scroll' }))
|
|
144
|
+
|
|
145
|
+
// Normal-mode-only keys must be gone while in scroll mode
|
|
146
|
+
expect(screen.queryByRole('button', { name: 'Escape' })).not.toBeInTheDocument()
|
|
147
|
+
expect(screen.queryByRole('button', { name: 'Up' })).not.toBeInTheDocument()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('submitting the line-input in scroll mode sends exit before the text', async () => {
|
|
151
|
+
const onSend = vi.fn()
|
|
152
|
+
const onControl = vi.fn()
|
|
153
|
+
render(<MobileKeyBar onSend={onSend} onControl={onControl} />)
|
|
154
|
+
|
|
155
|
+
// Enter scroll mode
|
|
156
|
+
await userEvent.click(screen.getByRole('button', { name: 'Scroll' }))
|
|
157
|
+
onControl.mockClear()
|
|
158
|
+
|
|
159
|
+
// Type and submit text
|
|
160
|
+
const input = screen.getByRole('textbox', { name: 'Command line' })
|
|
161
|
+
await userEvent.type(input, 'hello')
|
|
162
|
+
await userEvent.click(screen.getByRole('button', { name: 'Send line' }))
|
|
163
|
+
|
|
164
|
+
// exit must have been called before onSend
|
|
165
|
+
expect(onControl).toHaveBeenCalledOnce()
|
|
166
|
+
expect(onControl).toHaveBeenCalledWith('exit')
|
|
167
|
+
|
|
168
|
+
expect(onSend).toHaveBeenCalledOnce()
|
|
169
|
+
expect(onSend).toHaveBeenCalledWith('hello\r')
|
|
170
|
+
|
|
171
|
+
// Scroll mode is now off
|
|
172
|
+
expect(screen.queryByRole('status', { name: 'Scroll mode active' })).not.toBeInTheDocument()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('scroll mode resets to off when connectionKey changes (reconnect)', async () => {
|
|
176
|
+
const onControl = vi.fn()
|
|
177
|
+
const { rerender } = render(
|
|
178
|
+
<MobileKeyBar onSend={noop} onControl={onControl} connectionKey={1} />,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Enter scroll mode
|
|
182
|
+
await userEvent.click(screen.getByRole('button', { name: 'Scroll' }))
|
|
183
|
+
expect(screen.getByRole('status', { name: 'Scroll mode active' })).toBeInTheDocument()
|
|
184
|
+
|
|
185
|
+
// Simulate reconnect: connectionKey increments
|
|
186
|
+
rerender(<MobileKeyBar onSend={noop} onControl={onControl} connectionKey={2} />)
|
|
187
|
+
|
|
188
|
+
// Badge must be gone
|
|
189
|
+
await waitFor(() =>
|
|
190
|
+
expect(
|
|
191
|
+
screen.queryByRole('status', { name: 'Scroll mode active' }),
|
|
192
|
+
).not.toBeInTheDocument(),
|
|
193
|
+
)
|
|
194
|
+
// Normal Scroll button is back
|
|
195
|
+
expect(screen.getByRole('button', { name: 'Scroll' })).toBeInTheDocument()
|
|
196
|
+
})
|
|
197
|
+
})
|