@marcomigozzi/trailhead 0.14.0 → 0.16.0
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/README.it.md +158 -34
- package/README.md +147 -33
- package/bin/auto-intro-docs.test.js +72 -0
- package/bin/auto-merge-docs.test.js +33 -0
- package/bin/auto-merge-ticket-docs.test.js +32 -0
- package/bin/auto-review-rounds-docs.test.js +45 -0
- package/bin/auto-run-docs.test.js +60 -0
- package/bin/auto-scope-docs.test.js +33 -0
- package/bin/auto-slots-docs.test.js +45 -0
- package/bin/auto-uat-docs.test.js +47 -0
- package/bin/base-preflight-docs.test.js +65 -0
- package/bin/execute-split-docs.test.js +146 -0
- package/bin/grilling-holistic-review-docs.test.js +90 -0
- package/bin/isolation-answer-docs.test.js +41 -0
- package/bin/isolation-copy-docs.test.js +28 -0
- package/bin/lib/codex-projection.js +6 -6
- package/bin/lib/codex-projection.test.js +43 -2
- package/bin/needs-you-docs.test.js +41 -0
- package/bin/pick-stress-test-docs.test.js +105 -0
- package/bin/report-status-docs.test.js +195 -0
- package/bin/resolve-helpers-docs.test.js +36 -0
- package/bin/scope-confirm-skip-docs.test.js +70 -0
- package/bin/trailhead.js +6 -6
- package/bin/trailhead.test.js +355 -16
- package/bin/uat-setup-docs.test.js +65 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
- package/plugins/trailhead/agents/trailhead-debug.md +20 -1
- package/plugins/trailhead/agents/trailhead-executor.md +29 -2
- package/plugins/trailhead/agents/trailhead-fix.md +29 -2
- package/plugins/trailhead/agents/trailhead-plan.md +20 -1
- package/plugins/trailhead/agents/trailhead-research.md +22 -3
- package/plugins/trailhead/agents/trailhead-verify.md +15 -2
- package/plugins/trailhead/commands/auto.md +2 -2
- package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
- package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
- package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
- package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
- package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
- package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
- package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
- package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
- package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
- package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
- package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
- package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
- package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
- package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
- package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
- package/plugins/trailhead/hooks/mod/README.md +209 -36
- package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
- package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
- package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
- package/plugins/trailhead/hooks/mod/register.tsx +312 -40
- package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
- package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
- package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
- package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
- package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
- package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
- package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
- package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
- package/plugins/trailhead/skills/_shared/choices.md +11 -4
- package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
- package/plugins/trailhead/skills/_shared/load-first.md +3 -3
- package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
- package/plugins/trailhead/skills/_shared/substrate.md +9 -4
- package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +12 -2
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
- package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
- package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
- package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
- package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
- package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
- package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
- package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
- package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
- package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
- package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
- package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
- package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
- package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
- package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
- package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
- package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
- package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
- package/plugins/trailhead/tests/session-registry.test.ts +50 -0
- package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
- package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
- package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
- package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
- package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
- package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
- package/plugins/trailhead/types/index.d.ts +62 -3
|
@@ -11,6 +11,17 @@ const FLOOR = 15_000
|
|
|
11
11
|
const TIMER = 180_000
|
|
12
12
|
const AMBER = '#e0af68'
|
|
13
13
|
const URL = 'https://github.com/o/r/issues'
|
|
14
|
+
const ROOT = '/repo'
|
|
15
|
+
const ROOT_OTHER = '/other'
|
|
16
|
+
// A linked worktree of ROOT: the repo's root is the main tree's, the checkout's own is this.
|
|
17
|
+
const ROOT_TREE = '/repo-t5'
|
|
18
|
+
// The checkout handed to a verb: base64url of the root, computed here by hand so the pane's own encoder is checked.
|
|
19
|
+
// The helper's decodeIn cannot be imported here (the CommonJS template does not load in this suite); the node suite
|
|
20
|
+
// checks decodeIn against Buffer's base64url, and these tests pin the same alphabet.
|
|
21
|
+
const IN = (root: string) => {
|
|
22
|
+
const b64 = btoa(Array.from(new TextEncoder().encode(root), byte => String.fromCharCode(byte)).join(''))
|
|
23
|
+
return `--in=${b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}`
|
|
24
|
+
}
|
|
14
25
|
|
|
15
26
|
const MAP = {
|
|
16
27
|
title: 'trailhead mod',
|
|
@@ -61,9 +72,57 @@ const MAPS = [
|
|
|
61
72
|
{ number: 198, title: 'trailhead mod', url: `${URL}/198` },
|
|
62
73
|
{ number: 14, title: 'older map', url: `${URL}/14` },
|
|
63
74
|
]
|
|
75
|
+
// The maps read: one GraphQL call that carries each map's sub-issue counts and the whiteboard's.
|
|
76
|
+
const MAPS_QUERY =
|
|
77
|
+
'query($owner:String!,$repo:String!){repository(owner:$owner,name:$repo){issues(first:100,states:OPEN,labels:["trailhead:map"]){nodes{number title url subIssuesSummary{total completed}}totalCount}whiteboardOpen:issues(states:OPEN,labels:["trailhead:whiteboard"]){totalCount}whiteboardAll:issues(labels:["trailhead:whiteboard"]){totalCount}}}'
|
|
78
|
+
const MAPS_CALL = ['gh', 'api', 'graphql', '-F', 'owner={owner}', '-F', 'repo={repo}', '-f', `query=${MAPS_QUERY}`]
|
|
79
|
+
const MAPS_LIST_CALL = ['gh', 'issue', 'list', '--label', 'trailhead:map', '--state', 'open', '--limit', '100', '--json', 'number,title,url']
|
|
80
|
+
// The pending-UAT read: every open `trailhead:uat` ticket with its body and labels, one GraphQL page per call;
|
|
81
|
+
// the scope (a map label, or the whiteboard) is filtered by the pane.
|
|
82
|
+
const UAT_QUERY =
|
|
83
|
+
'query($owner:String!,$repo:String!,$after:String){repository(owner:$owner,name:$repo){issues(first:100,after:$after,states:OPEN,labels:["trailhead:uat"],orderBy:{field:CREATED_AT,direction:DESC}){nodes{number body labels(first:50){nodes{name}}}pageInfo{hasNextPage endCursor}}}}'
|
|
84
|
+
const uatCall = (after?: string) => [
|
|
85
|
+
'gh', 'api', 'graphql', '-F', 'owner={owner}', '-F', 'repo={repo}', '-f', `query=${UAT_QUERY}`,
|
|
86
|
+
...(after === undefined ? [] : ['-f', `after=${after}`]),
|
|
87
|
+
]
|
|
88
|
+
const isUatRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.includes(`query=${UAT_QUERY}`)
|
|
89
|
+
// The retired search read of the UAT parents: the pane must never run it.
|
|
90
|
+
const isUatSearch = (argv: readonly string[]) => argv[2] === 'list' && argv.includes('trailhead:uat')
|
|
91
|
+
const UAT_MAX_PAGES = 10
|
|
92
|
+
// One open UAT ticket as the read returns it: its body and the labels it carries (the scope among them).
|
|
93
|
+
const uatRow = (number: number, body: string, scope = 'trailhead:map-198') => ({ number, body, labels: [{ name: 'trailhead:uat' }, { name: scope }] })
|
|
94
|
+
// The open-ticket read: one GraphQL page per call (`after=` carries the cursor of the next one).
|
|
95
|
+
const TICKETS_QUERY =
|
|
96
|
+
'query($owner:String!,$repo:String!,$label:String!,$after:String){repository(owner:$owner,name:$repo){issues(first:100,after:$after,states:OPEN,labels:[$label],orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url labels(first:50){nodes{name}}assignees(first:10){nodes{login}}}pageInfo{hasNextPage endCursor}}}}'
|
|
97
|
+
const ticketsCall = (label: string, after?: string) => [
|
|
98
|
+
'gh', 'api', 'graphql', '-F', 'owner={owner}', '-F', 'repo={repo}', '-f', `query=${TICKETS_QUERY}`, '-f', `label=${label}`,
|
|
99
|
+
...(after === undefined ? [] : ['-f', `after=${after}`]),
|
|
100
|
+
]
|
|
101
|
+
const ticketsListCall = (label: string) => ['gh', 'issue', 'list', '--label', label, '--state', 'open', '--limit', '200', '--json', 'number,title,labels,assignees,url']
|
|
102
|
+
const isTicketsRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.includes(`query=${TICKETS_QUERY}`)
|
|
103
|
+
// The maps read only: the ticket reads are GraphQL too, but answered and failed on their own.
|
|
104
|
+
const isGraphql = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && !isTicketsRead(argv) && !isUatRead(argv) && !isCountRead(argv)
|
|
105
|
+
// The open-ticket total: one aliased `totalCount` per open map (`m<n>`) plus the whiteboard (`wb`), in one call.
|
|
106
|
+
const countQuery = (maps: number[]) =>
|
|
107
|
+
`query($owner:String!,$repo:String!){repository(owner:$owner,name:$repo){${[...maps.map(n => `m${n}:issues(states:OPEN,labels:["trailhead:map-${n}"]){totalCount}`), 'wb:issues(states:OPEN,labels:["trailhead:whiteboard"]){totalCount}'].join('')}}}`
|
|
108
|
+
const countCall = (...maps: number[]) => ['gh', 'api', 'graphql', '-F', 'owner={owner}', '-F', 'repo={repo}', '-f', `query=${countQuery(maps)}`]
|
|
109
|
+
const isCountRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.some(a => a.startsWith('query=') && a.includes('wb:issues('))
|
|
110
|
+
const flatNode = (t: any) => ({ ...t, labels: { nodes: t.labels }, assignees: { nodes: t.assignees } })
|
|
111
|
+
const PAGE = 100
|
|
112
|
+
const isMapsList = (argv: readonly string[]) => argv[2] === 'list' && argv.includes('trailhead:map')
|
|
64
113
|
const OTHER_TICKETS = [issue(140, 'Old frontier ticket', ['trailhead:build']), issue(141, 'Old held ticket', ['trailhead:task'], ['carol'])]
|
|
65
114
|
const OTHER_HISTORY = OTHER_TICKETS.map(t => hrow(t.number, t.labels.map(l => l.name), null))
|
|
66
115
|
|
|
116
|
+
// The whiteboard (map-less tickets) and the pinned dashboard issue, as the dashboard view reads them.
|
|
117
|
+
const WB = ['trailhead:ticket', 'trailhead:whiteboard']
|
|
118
|
+
const WHITEBOARD = [
|
|
119
|
+
issue(301, 'Fix the README badge', [...WB, 'trailhead:bug']),
|
|
120
|
+
issue(302, 'Bump the CI node', [...WB, 'trailhead:task'], ['alice']),
|
|
121
|
+
issue(303, 'Waits on upstream', [...WB, 'trailhead:task', 'trailhead:blocked']),
|
|
122
|
+
issue(304, 'Quarantined loose one', [...WB, 'trailhead:task', 'trailhead:unverified']),
|
|
123
|
+
]
|
|
124
|
+
const DASHBOARD = [{ number: 5, url: `${URL}/5` }]
|
|
125
|
+
|
|
67
126
|
const stateText = (over: Record<string, unknown> = {}): string =>
|
|
68
127
|
JSON.stringify({
|
|
69
128
|
map: 198,
|
|
@@ -78,7 +137,7 @@ const stateText = (over: Record<string, unknown> = {}): string =>
|
|
|
78
137
|
})
|
|
79
138
|
|
|
80
139
|
// The plugin's atoms, which the engine resets on /clear.
|
|
81
|
-
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'pendingWork', 'paneAutoOpened']
|
|
140
|
+
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'isDashboard', 'dashboardPane', 'pendingWork', 'pendingPhase', 'paneAutoOpened', 'ownTicket', 'repoPin']
|
|
82
141
|
|
|
83
142
|
const PANE_PROPS = {
|
|
84
143
|
title: 'Map',
|
|
@@ -104,17 +163,60 @@ const rig = (on: On, engine?: any) => {
|
|
|
104
163
|
map: MAP as unknown,
|
|
105
164
|
// The open maps list, and the second map's own answers (view, open list, history).
|
|
106
165
|
maps: MAPS as unknown[],
|
|
166
|
+
// Sub-issue totals per map number, as the GraphQL read reports them (a map without one has no summary).
|
|
167
|
+
mapSummary: { 198: { total: 23, completed: 16 }, 14: { total: 5, completed: 2 } } as Record<number, unknown>,
|
|
168
|
+
// The whiteboard's open and total tickets as the same call reports them (null: the fields are absent).
|
|
169
|
+
whiteboardCounts: { open: 3, total: 9 } as { open: number; total: number } | null,
|
|
170
|
+
// The open tickets per label, as the total's count read reports them (a missing label counts 0); `countMode`
|
|
171
|
+
// makes that read fail (`fail`), answer `errors` (`errors`) or non-JSON (`junk`), `countTotal` the maps read's `totalCount`.
|
|
172
|
+
openCounts: { 'trailhead:map-198': 5, 'trailhead:map-14': 2, 'trailhead:whiteboard': 3 } as Record<string, number>,
|
|
173
|
+
countMode: 'ok' as 'ok' | 'fail' | 'errors' | 'junk',
|
|
174
|
+
countTotal: null as number | null,
|
|
107
175
|
otherMap: OTHER_MAP as unknown,
|
|
108
176
|
otherTickets: OTHER_TICKETS as unknown[],
|
|
109
177
|
otherHistory: OTHER_HISTORY as unknown[],
|
|
178
|
+
whiteboard: WHITEBOARD as unknown[],
|
|
179
|
+
// The UAT read's answer: one row per open UAT ticket, `{ number, body, labels }` (the scope label rides
|
|
180
|
+
// in `labels`); null makes the read fail. `uatErrors` answers a reply with `errors`, `uatBroken` non-JSON.
|
|
181
|
+
uat: [] as unknown[] | null,
|
|
182
|
+
uatErrors: false,
|
|
183
|
+
uatBroken: false,
|
|
184
|
+
dashboard: DASHBOARD as unknown[],
|
|
110
185
|
gh: null as Gh | null,
|
|
111
186
|
// What `gh` answers when no test replaced it; a test can wrap it.
|
|
112
187
|
defaultGh: null as unknown as Gh,
|
|
113
188
|
deny: null as string | null,
|
|
189
|
+
// The session's working directory, which a shell `cd` moves, and what `session.repo` finds there
|
|
190
|
+
// (null outside any repo). Relative reads and a `gh` without its own cwd resolve against `cwd`.
|
|
191
|
+
cwd: '/repo',
|
|
192
|
+
repo: { root: '/repo', remote: 'git@github.com:o/r.git', internal: false } as { root: string; remote: string | null; internal: boolean } | null,
|
|
193
|
+
// The active-map marker of the second checkout (ROOT_OTHER); null: it has none.
|
|
194
|
+
otherActive: null as string | null,
|
|
195
|
+
// The worktree checkout's marker, what `git rev-parse --show-toplevel` answers there (null: it fails), and the
|
|
196
|
+
// session's project root; every path read and every `git` run is recorded.
|
|
197
|
+
treeActive: null as string | null,
|
|
198
|
+
top: null as string | null,
|
|
199
|
+
sessionRoot: '/repo',
|
|
200
|
+
reads: [] as string[],
|
|
201
|
+
// The hosts `gh auth status --hostname` accepts besides github.com, and every host it was asked about.
|
|
202
|
+
authHosts: [] as string[],
|
|
203
|
+
authCalls: [] as string[],
|
|
204
|
+
gitCalls: [] as { argv: readonly string[]; cwd: string | undefined }[],
|
|
205
|
+
// The cwd every `gh` run carried (undefined: none given).
|
|
206
|
+
cwds: [] as (string | undefined)[],
|
|
114
207
|
calls: [] as (readonly string[])[],
|
|
115
208
|
panes: [] as { id: string; title: string; isShown: boolean; isFocused: boolean; isPlaced: boolean }[],
|
|
116
209
|
isPlaced: true,
|
|
117
210
|
logs: [] as string[],
|
|
211
|
+
toasts: [] as string[],
|
|
212
|
+
// What a Bash call prints on stdout (the created issue's URL for a `gh issue create`).
|
|
213
|
+
stdout: 'ok' as string,
|
|
214
|
+
// `session.repo` answers a denial (the checkout cannot be read).
|
|
215
|
+
repoFails: false,
|
|
216
|
+
// What `git remote get-url origin` answers (null: no origin), for a `session.repo` that answers null (#299).
|
|
217
|
+
gitRemote: null as string | null,
|
|
218
|
+
// Checkout roots that no longer exist: git answers 128 there (a removed worktree or clone).
|
|
219
|
+
gone: [] as string[],
|
|
118
220
|
opens: [] as { id: string; title?: string }[],
|
|
119
221
|
// `command.run` beneath the plugin: every run is recorded; `gate` holds a work run, `refuse` rejects it.
|
|
120
222
|
// Every file write beneath the plugin: the pane must never write the active-map marker.
|
|
@@ -128,7 +230,15 @@ const rig = (on: On, engine?: any) => {
|
|
|
128
230
|
// The clear answered by a `{ deny }` hook: it resolves the call and the session stays.
|
|
129
231
|
denyClear: false,
|
|
130
232
|
// Runs between the old session ending and the new one starting.
|
|
131
|
-
duringClear: null as (() => void) | null,
|
|
233
|
+
duringClear: null as (() => void | Promise<void>) | null,
|
|
234
|
+
// Runs once, in the next `session.id` read (a dispatch's own read before its clear).
|
|
235
|
+
beforeSessionId: null as (() => void | Promise<void>) | null,
|
|
236
|
+
// Every value written to the pendingPhase atom, in order.
|
|
237
|
+
phases: [] as unknown[],
|
|
238
|
+
// Atom values the engine reports whatever was written (a state a hot reload left); a write clears the key.
|
|
239
|
+
forced: {} as Record<string, unknown>,
|
|
240
|
+
// Runs once, before the next pendingPhase write lands (the row still reads what it read before).
|
|
241
|
+
beforePhaseWrite: null as (() => void | Promise<void>) | null,
|
|
132
242
|
session: SESSION,
|
|
133
243
|
clock: mock.clock(on, { now: T0 }),
|
|
134
244
|
// The person opening the pane: the engine's own record, the plugin is not told.
|
|
@@ -137,13 +247,57 @@ const rig = (on: On, engine?: any) => {
|
|
|
137
247
|
},
|
|
138
248
|
}
|
|
139
249
|
const defaultGh: Gh = argv => {
|
|
140
|
-
if (argv
|
|
250
|
+
if (isTicketsRead(argv)) {
|
|
251
|
+
const label = (argv.find(a => a.startsWith('label=')) ?? '').slice('label='.length)
|
|
252
|
+
const cursor = argv.find(a => a.startsWith('after='))
|
|
253
|
+
const all = (label === 'trailhead:whiteboard' ? r.whiteboard : label === 'trailhead:map-14' ? r.otherTickets : r.tickets) as unknown[]
|
|
254
|
+
const from = cursor === undefined ? 0 : Number(cursor.slice('after=c'.length))
|
|
255
|
+
const to = from + PAGE
|
|
256
|
+
const pageInfo = { hasNextPage: to < all.length, endCursor: `c${to}` }
|
|
257
|
+
return ok(JSON.stringify({ data: { repository: { issues: { nodes: all.slice(from, to).map(flatNode), pageInfo } } } }))
|
|
258
|
+
}
|
|
259
|
+
if (isUatRead(argv)) {
|
|
260
|
+
if (r.uat === null) return { exitCode: 1, stdout: '', stderr: 'boom' }
|
|
261
|
+
if (r.uatBroken) return ok('not json')
|
|
262
|
+
if (r.uatErrors) return ok(JSON.stringify({ data: { repository: { issues: { nodes: [] } } }, errors: [{ message: 'x' }] }))
|
|
263
|
+
const cursor = argv.find(a => a.startsWith('after='))
|
|
264
|
+
const from = cursor === undefined ? 0 : Number(cursor.slice('after=c'.length))
|
|
265
|
+
const to = from + PAGE
|
|
266
|
+
const pageInfo = { hasNextPage: to < r.uat.length, endCursor: `c${to}` }
|
|
267
|
+
const nodes = (r.uat as any[]).slice(from, to).map(u => ({ ...u, labels: { nodes: u.labels ?? [] } }))
|
|
268
|
+
return ok(JSON.stringify({ data: { repository: { issues: { nodes, pageInfo } } } }))
|
|
269
|
+
}
|
|
270
|
+
if (isCountRead(argv)) {
|
|
271
|
+
if (r.countMode === 'fail') return { exitCode: 1, stdout: '', stderr: 'boom' }
|
|
272
|
+
if (r.countMode === 'junk') return ok('not json')
|
|
273
|
+
const query = (argv.find(a => a.startsWith('query=')) ?? '').slice('query='.length)
|
|
274
|
+
const repo = Object.fromEntries([...query.matchAll(/(\w+):issues\(states:OPEN,labels:\["([^"]+)"\]\)\{totalCount\}/g)].map(m => [m[1], { totalCount: r.openCounts[m[2] as string] ?? 0 }]))
|
|
275
|
+
return ok(JSON.stringify({ data: { repository: repo }, ...(r.countMode === 'errors' ? { errors: [{ message: 'x' }] } : {}) }))
|
|
276
|
+
}
|
|
277
|
+
if (argv.includes('trailhead:whiteboard')) return ok(JSON.stringify(r.whiteboard))
|
|
278
|
+
if (argv.includes('trailhead:dashboard')) return ok(JSON.stringify(r.dashboard))
|
|
279
|
+
if (isGraphql(argv)) {
|
|
280
|
+
const nodes = (r.maps as { number: number }[]).map(m => ({ ...m, subIssuesSummary: r.mapSummary[m.number] }))
|
|
281
|
+
const wb = r.whiteboardCounts
|
|
282
|
+
const counts = wb === null ? {} : { whiteboardOpen: { totalCount: wb.open }, whiteboardAll: { totalCount: wb.total } }
|
|
283
|
+
const totalCount = r.countTotal ?? nodes.length
|
|
284
|
+
return ok(JSON.stringify({ data: { repository: { issues: { nodes, totalCount }, ...counts } } }))
|
|
285
|
+
}
|
|
286
|
+
if (isMapsList(argv)) return ok(JSON.stringify(r.maps))
|
|
141
287
|
const isOther = argv.includes('14') || argv.includes('trailhead:map-14')
|
|
142
288
|
if (argv[2] === 'view') return ok(JSON.stringify(isOther ? r.otherMap : r.map))
|
|
143
289
|
return ok(JSON.stringify(argv.includes('all') ? (isOther ? r.otherHistory : r.history) : isOther ? r.otherTickets : r.tickets))
|
|
144
290
|
}
|
|
145
291
|
on('fs.read', (_$, e) => {
|
|
146
|
-
|
|
292
|
+
// A relative path reaches the hook already resolved against the host's directory, not the repo's: put
|
|
293
|
+
// it under the session's working directory, which is what the engine resolves it against. A path under
|
|
294
|
+
// one of the repos is absolute (a pinned read) and stays.
|
|
295
|
+
const isAbsolute = [ROOT, ROOT_OTHER, ROOT_TREE].some(root => e.path.startsWith(`${root}/`))
|
|
296
|
+
const path = isAbsolute ? e.path : `${r.cwd}${e.path.slice(e.path.indexOf('/.trailhead/'))}`
|
|
297
|
+
r.reads.push(path)
|
|
298
|
+
const root = path.startsWith(`${ROOT_TREE}/`) ? ROOT_TREE : path.startsWith(`${ROOT_OTHER}/`) ? ROOT_OTHER : path.startsWith(`${ROOT}/`) ? ROOT : null
|
|
299
|
+
const marker = root === ROOT_TREE ? r.treeActive : root === ROOT_OTHER ? r.otherActive : r.active
|
|
300
|
+
const file = path.endsWith('/.trailhead/active-map') ? (root === null ? null : marker) : path.endsWith('/.trailhead/work-state.json') ? (root === null ? null : r.work) : undefined
|
|
147
301
|
if (file === undefined) return { deny: `unexpected read of ${e.path}` }
|
|
148
302
|
if (file === null) return { deny: `ENOENT: no such file or directory, open '${e.path}'` }
|
|
149
303
|
if (file instanceof Error) return { deny: file.message }
|
|
@@ -160,16 +314,24 @@ const rig = (on: On, engine?: any) => {
|
|
|
160
314
|
const emptied = new Set<string>()
|
|
161
315
|
let clears = 0
|
|
162
316
|
on('state.get', { plugin: 'trailhead' }, async (_$, e, next) => {
|
|
317
|
+
if (e.key in r.forced) return { value: { value: r.forced[e.key], version: 1 } }
|
|
163
318
|
const read = await next(e)
|
|
164
319
|
return emptied.has(e.key) ? { value: { value: undefined, version: read.value?.version ?? 0 } } : read
|
|
165
320
|
})
|
|
166
|
-
on('state.set', { plugin: 'trailhead' }, (_$, e, next) => {
|
|
321
|
+
on('state.set', { plugin: 'trailhead' }, async (_$, e, next) => {
|
|
322
|
+
if (e.key === 'pendingPhase') {
|
|
323
|
+
const hold = r.beforePhaseWrite
|
|
324
|
+
r.beforePhaseWrite = null
|
|
325
|
+
await hold?.()
|
|
326
|
+
r.phases.push(e.value)
|
|
327
|
+
}
|
|
328
|
+
delete r.forced[e.key]
|
|
167
329
|
emptied.delete(e.key)
|
|
168
330
|
return next(e)
|
|
169
331
|
})
|
|
170
332
|
const emulateClear = async () => {
|
|
171
333
|
await engine.session.end({ reason: 'clear', sessionId: r.session, resume: {} })
|
|
172
|
-
r.duringClear?.()
|
|
334
|
+
await r.duringClear?.()
|
|
173
335
|
ATOM_KEYS.forEach(key => emptied.add(key))
|
|
174
336
|
r.session = `${NEXT_SESSION}-${(clears += 1)}`
|
|
175
337
|
await engine.classic.SessionStart({ source: 'clear' })
|
|
@@ -188,14 +350,52 @@ const rig = (on: On, engine?: any) => {
|
|
|
188
350
|
if (r.refuse !== null) throw new Error(r.refuse)
|
|
189
351
|
return { text: '' }
|
|
190
352
|
})
|
|
191
|
-
on('session.id', () =>
|
|
353
|
+
on('session.id', async () => {
|
|
354
|
+
const hold = r.beforeSessionId
|
|
355
|
+
r.beforeSessionId = null
|
|
356
|
+
await hold?.()
|
|
357
|
+
return { value: r.session }
|
|
358
|
+
})
|
|
192
359
|
on('ui.log', (_$, e) => {
|
|
193
360
|
r.logs.push(e.text)
|
|
194
361
|
return { value: undefined }
|
|
195
362
|
})
|
|
196
363
|
on('ui.status', () => ({ value: undefined }))
|
|
364
|
+
on('ui.toast', (_$, e) => {
|
|
365
|
+
r.toasts.push(e.text)
|
|
366
|
+
return { value: undefined }
|
|
367
|
+
})
|
|
368
|
+
on('session.repo', () => (r.repoFails ? { deny: 'repo read failed' } : { value: r.repo }))
|
|
369
|
+
on('session.root', () => ({ value: r.sessionRoot }))
|
|
370
|
+
on('session.cwd', () => ({ value: r.cwd }))
|
|
197
371
|
on('process.run', async (_$, e) => {
|
|
372
|
+
// The pin's own `git rev-parse --show-toplevel`: answered apart, never counted as a `gh` call.
|
|
373
|
+
if (e.argv[0] === 'git') {
|
|
374
|
+
r.gitCalls.push({ argv: [...e.argv], cwd: e.init?.cwd })
|
|
375
|
+
// Answered per directory, like git: the worktree gives `top`, the other checkouts their own root.
|
|
376
|
+
const where = e.init?.cwd
|
|
377
|
+
if (e.argv[1] === 'remote') {
|
|
378
|
+
const remote = r.gitRemote === null ? { exitCode: 2, stdout: '', stderr: '' } : { exitCode: 0, stdout: `${r.gitRemote}\n`, stderr: '' }
|
|
379
|
+
return { value: { ...remote, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
380
|
+
}
|
|
381
|
+
const top = where !== undefined && r.gone.includes(where) ? null : where === ROOT_TREE ? r.top : where === ROOT || where === ROOT_OTHER ? where : null
|
|
382
|
+
const out = top === null ? { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository' } : { exitCode: 0, stdout: `${top}\n`, stderr: '' }
|
|
383
|
+
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
384
|
+
}
|
|
385
|
+
// The pin's host check: answered apart, never counted as a read of the pane.
|
|
386
|
+
if (e.argv[1] === 'auth') {
|
|
387
|
+
const host = e.argv[4] ?? ''
|
|
388
|
+
r.authCalls.push(host)
|
|
389
|
+
const out = r.authHosts.includes(host) ? { exitCode: 0, stdout: '', stderr: '' } : { exitCode: 1, stdout: '', stderr: `You are not logged into any accounts on ${host}` }
|
|
390
|
+
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
391
|
+
}
|
|
198
392
|
r.calls.push([...e.argv])
|
|
393
|
+
r.cwds.push(e.init?.cwd)
|
|
394
|
+
// `gh` needs a repo around it: without its own cwd it uses the session's, wherever a `cd` left it.
|
|
395
|
+
const where = e.init?.cwd ?? r.cwd
|
|
396
|
+
if (where !== ROOT && where !== ROOT_OTHER && where !== ROOT_TREE) {
|
|
397
|
+
return { value: { exitCode: 1, stdout: '', stderr: 'failed to run git: fatal: not a git repository', isStdoutTruncated: false, isStderrTruncated: false } }
|
|
398
|
+
}
|
|
199
399
|
if (r.deny !== null) return { deny: r.deny }
|
|
200
400
|
const out = await (r.gh ?? defaultGh)(e.argv)
|
|
201
401
|
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
@@ -213,7 +413,7 @@ const rig = (on: On, engine?: any) => {
|
|
|
213
413
|
return { value: undefined }
|
|
214
414
|
})
|
|
215
415
|
on('ui.panes', () => ({ value: r.panes }))
|
|
216
|
-
on('tool.call', { tool: 'Bash' }, () => ({ result: '
|
|
416
|
+
on('tool.call', { tool: 'Bash' }, () => ({ result: { stdout: r.stdout, stderr: '' } }) as never)
|
|
217
417
|
on('session.start', (_$, e) => ({ cwd: e.cwd }))
|
|
218
418
|
on('classic.SessionStart', () => ({}))
|
|
219
419
|
on('turn.start', (_$, e) => ({ turnId: e.turnId }))
|
|
@@ -243,11 +443,11 @@ const settle = async (r: Rig) => {
|
|
|
243
443
|
for (let i = 0; i < 10; i += 1) await r.clock.settle()
|
|
244
444
|
}
|
|
245
445
|
// The open-list reads; the history read rides along with each and is not counted.
|
|
246
|
-
const fetches = (r: Rig) => r.calls.filter(argv => argv
|
|
446
|
+
const fetches = (r: Rig) => r.calls.filter(argv => isTicketsRead(argv) && !argv.some(a => a.startsWith('after='))).length
|
|
247
447
|
// A gh that answers as the default does, except for the history read.
|
|
248
448
|
const withHistory = (r: Rig, answer: GhOut): Gh => argv => {
|
|
249
449
|
if (argv.includes('all')) return answer
|
|
250
|
-
if (argv
|
|
450
|
+
if (isGraphql(argv) || isCountRead(argv) || isMapsList(argv)) return r.defaultGh(argv)
|
|
251
451
|
return argv[2] === 'view' ? ok(JSON.stringify(r.map)) : ok(JSON.stringify(r.tickets))
|
|
252
452
|
}
|
|
253
453
|
const hasKey = async (ui: any, key: string): Promise<boolean> => (await ui.find({ key })) !== undefined
|
|
@@ -273,9 +473,11 @@ for (const surface of SURFACES) {
|
|
|
273
473
|
|
|
274
474
|
expect(r.calls).toEqual([
|
|
275
475
|
['gh', 'issue', 'view', '198', '--json', 'title,body,url'],
|
|
276
|
-
|
|
476
|
+
ticketsCall('trailhead:map-198'),
|
|
277
477
|
['gh', 'issue', 'list', '--label', 'trailhead:map-198', '--state', 'all', '--limit', '5000', '--json', 'number,labels,createdAt,closedAt'],
|
|
278
|
-
|
|
478
|
+
MAPS_CALL,
|
|
479
|
+
uatCall(),
|
|
480
|
+
countCall(14, 198),
|
|
279
481
|
])
|
|
280
482
|
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
281
483
|
expect((await ui.find({ type: 'Link' }))?.props).toMatchObject({ href: `${URL}/198` })
|
|
@@ -297,8 +499,10 @@ for (const surface of SURFACES) {
|
|
|
297
499
|
expect(links).toContain(`${URL}/209`)
|
|
298
500
|
|
|
299
501
|
// holders on every assigned row; the session's own ticket is marked and in the accent
|
|
300
|
-
expect(await textOf(ui, '
|
|
301
|
-
expect((await ui.find({ key: '
|
|
502
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
503
|
+
expect((await ui.find({ key: 'self-208' }))?.children[0]).toMatchObject({ props: { color: ACCENT } })
|
|
504
|
+
expect((await ui.find({ key: 'self-208' }))?.children[0].props).not.toHaveProperty('bold')
|
|
505
|
+
expect(await ui.find({ key: 'holder-208' })).toBeUndefined()
|
|
302
506
|
expect(await textOf(ui, 'mark-208')).toBe('▸ ')
|
|
303
507
|
expect((await ui.find({ key: 'mark-208' }))?.children[0]).toMatchObject({ props: { bold: true, color: ACCENT } })
|
|
304
508
|
expect((await ui.find({ key: 'ticket-208' }))?.children[0]).toMatchObject({ props: { bold: true, color: ACCENT } })
|
|
@@ -319,6 +523,229 @@ for (const surface of SURFACES) {
|
|
|
319
523
|
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
320
524
|
})
|
|
321
525
|
|
|
526
|
+
test(`a UAT ticket sits under UAT between In progress and Paused, with its parent line and its work button [${surface}]`, async ($, on) => {
|
|
527
|
+
const r = rig(on, $)
|
|
528
|
+
r.work = stateText()
|
|
529
|
+
r.tickets = [
|
|
530
|
+
...TICKETS,
|
|
531
|
+
issue(240, 'UAT of the burn-down', ['trailhead:task', 'trailhead:uat']),
|
|
532
|
+
issue(241, 'Device UAT seed', ['trailhead:seed', 'trailhead:blocked', 'trailhead:uat']),
|
|
533
|
+
issue(242, 'Held UAT', ['trailhead:task', 'trailhead:uat'], ['alice']),
|
|
534
|
+
issue(243, 'Paused UAT', ['trailhead:task', 'trailhead:uat', 'trailhead:paused']),
|
|
535
|
+
]
|
|
536
|
+
r.uat = [
|
|
537
|
+
uatRow(240, `Run it\n\nUAT of: [Burn-down view of the map](${URL}/209)\n`),
|
|
538
|
+
uatRow(241, `UAT of: #212`),
|
|
539
|
+
uatRow(243, `UAT of: [Paused parent](${URL}/150)`),
|
|
540
|
+
]
|
|
541
|
+
await $.session.start(START)
|
|
542
|
+
const ui = await openAndMount($, r)
|
|
543
|
+
|
|
544
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 3')
|
|
545
|
+
expect((await ui.find({ key: 'head-uat' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
546
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-frontier', 'group-in-progress', 'group-uat', 'group-paused', 'group-blocked'])
|
|
547
|
+
// each ticket once: a UAT row is in no other group, a paused UAT stays under Paused
|
|
548
|
+
expect(await keysOf(ui, 'row-')).toEqual([
|
|
549
|
+
'row-209', 'row-210', 'row-214', 'row-208', 'row-212', 'row-240', 'row-241', 'row-242', 'row-243', 'row-211', 'row-213',
|
|
550
|
+
])
|
|
551
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
552
|
+
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
553
|
+
// the parent line, dim, under the row; none without a parent
|
|
554
|
+
expect(await textOf(ui, 'uat-of-240')).toBe('tests Burn-down view of the map #209')
|
|
555
|
+
expect((await ui.find({ key: 'uat-of-240' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
556
|
+
expect(await textOf(ui, 'uat-of-241')).toBe('tests #212')
|
|
557
|
+
expect(await hasKey(ui, 'uat-of-242')).toBe(false)
|
|
558
|
+
expect(await textOf(ui, 'uat-of-243')).toBe('tests Paused parent #150')
|
|
559
|
+
// the parents come through one GraphQL read, never through the search index
|
|
560
|
+
expect(r.calls.filter(isUatRead)).toEqual([uatCall()])
|
|
561
|
+
expect(r.calls.filter(isUatSearch)).toEqual([])
|
|
562
|
+
// a marker, not a state: a frontier UAT task keeps its work button, a held one its holder, a seed stays dim
|
|
563
|
+
expect(await hasKey(ui, 'work-240')).toBe(true)
|
|
564
|
+
expect(await textOf(ui, 'holder-242')).toBe('@alice')
|
|
565
|
+
expect(await hasKey(ui, 'work-242')).toBe(false)
|
|
566
|
+
expect(await hasKey(ui, 'work-241')).toBe(false)
|
|
567
|
+
expect((await ui.find({ key: 'ticket-241' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
test(`no UAT ticket draws no UAT group [${surface}]`, async ($, on) => {
|
|
571
|
+
const none = rig(on, $)
|
|
572
|
+
await $.session.start(START)
|
|
573
|
+
const ui0 = await openAndMount($, none)
|
|
574
|
+
expect(await hasKey(ui0, 'group-uat')).toBe(false)
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
test(`a failed UAT read draws the UAT rows without the parent line, no error [${surface}]`, async ($, on) => {
|
|
578
|
+
const r = rig(on, $)
|
|
579
|
+
r.tickets = [...TICKETS, issue(240, 'UAT of the burn-down', ['trailhead:task', 'trailhead:uat'])]
|
|
580
|
+
r.uat = null
|
|
581
|
+
await $.session.start(START)
|
|
582
|
+
const ui = await openAndMount($, r)
|
|
583
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
584
|
+
expect(await hasKey(ui, 'uat-of-240')).toBe(false)
|
|
585
|
+
expect(await hasKey(ui, 'error')).toBe(false)
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
test(`a UAT row of another scope does not set the parent of a row with the same number [${surface}]`, async ($, on) => {
|
|
589
|
+
const r = rig(on, $)
|
|
590
|
+
r.tickets = [...TICKETS, issue(240, 'UAT of the burn-down', ['trailhead:task', 'trailhead:uat'])]
|
|
591
|
+
r.uat = [uatRow(240, `UAT of: [Elsewhere](${URL}/14)`, 'trailhead:map-14'), uatRow(241, `UAT of: #5`)]
|
|
592
|
+
await $.session.start(START)
|
|
593
|
+
const ui = await openAndMount($, r)
|
|
594
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
595
|
+
expect(await hasKey(ui, 'uat-of-240')).toBe(false)
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
test(`more than one page of follow-ups is read, the second call carrying the cursor [${surface}]`, async ($, on) => {
|
|
599
|
+
const r = rig(on, $)
|
|
600
|
+
r.tickets = [...TICKETS, issue(500, 'Deep UAT', ['trailhead:task', 'trailhead:uat'])]
|
|
601
|
+
r.uat = [...Array.from({ length: 149 }, (_, i) => uatRow(1000 + i, 'no parent line')), uatRow(500, `UAT of: [Deep parent](${URL}/77)`)]
|
|
602
|
+
await $.session.start(START)
|
|
603
|
+
const ui = await openAndMount($, r)
|
|
604
|
+
expect(r.calls.filter(isUatRead)).toEqual([uatCall(), uatCall('c100')])
|
|
605
|
+
expect(await textOf(ui, 'uat-of-500')).toBe('tests Deep parent #77')
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
const failClosed: [string, (r: Rig) => void][] = [
|
|
609
|
+
['a failed second page', r => { r.gh = argv => (isUatRead(argv) && argv.some(a => a.startsWith('after=')) ? { exitCode: 1, stdout: '', stderr: 'boom' } : r.defaultGh(argv)) }],
|
|
610
|
+
['a reply with errors', r => { r.uatErrors = true }],
|
|
611
|
+
['a reply that is not JSON', r => { r.uatBroken = true }],
|
|
612
|
+
]
|
|
613
|
+
for (const [name, arm] of failClosed) {
|
|
614
|
+
test(`${name} draws the UAT rows with no parent line, no fallback, no partial map [${surface}]`, async ($, on) => {
|
|
615
|
+
const r = rig(on, $)
|
|
616
|
+
r.tickets = [...TICKETS, issue(240, 'UAT of the burn-down', ['trailhead:task', 'trailhead:uat'])]
|
|
617
|
+
// the parent sits on page 1; a second page exists so the failed-page case has something to lose
|
|
618
|
+
r.uat = [uatRow(240, `UAT of: [Burn-down](${URL}/209)`), ...Array.from({ length: 120 }, (_, i) => uatRow(1000 + i, 'x'))]
|
|
619
|
+
arm(r)
|
|
620
|
+
await $.session.start(START)
|
|
621
|
+
const ui = await openAndMount($, r)
|
|
622
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
623
|
+
expect(await hasKey(ui, 'uat-of-240')).toBe(false)
|
|
624
|
+
expect(await hasKey(ui, 'error')).toBe(false)
|
|
625
|
+
expect(r.calls.filter(isUatSearch)).toEqual([])
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
test(`a read still paging at the cap draws no parent line and stops at the cap [${surface}]`, async ($, on) => {
|
|
630
|
+
const r = rig(on, $)
|
|
631
|
+
r.tickets = [...TICKETS, issue(240, 'UAT of the burn-down', ['trailhead:task', 'trailhead:uat'])]
|
|
632
|
+
r.uat = [uatRow(240, `UAT of: [Burn-down](${URL}/209)`), ...Array.from({ length: UAT_MAX_PAGES * PAGE + 5 }, (_, i) => uatRow(2000 + i, 'x'))]
|
|
633
|
+
await $.session.start(START)
|
|
634
|
+
const ui = await openAndMount($, r)
|
|
635
|
+
expect(r.calls.filter(isUatRead)).toHaveLength(UAT_MAX_PAGES)
|
|
636
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
637
|
+
expect(await hasKey(ui, 'uat-of-240')).toBe(false)
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
test(`the dashboard view groups UAT whiteboard tickets with their parent, reading the whiteboard scope [${surface}]`, async ($, on) => {
|
|
641
|
+
const r = rig(on, $)
|
|
642
|
+
r.whiteboard = [...WHITEBOARD, issue(306, 'Loose UAT', [...WB, 'trailhead:task', 'trailhead:uat'])]
|
|
643
|
+
r.uat = [uatRow(306, `UAT of: [Loose parent](${URL}/290)`, 'trailhead:whiteboard'), uatRow(306, `UAT of: #1`, 'trailhead:map-14')]
|
|
644
|
+
await $.session.start(START)
|
|
645
|
+
const ui = await openAndMount($, r)
|
|
646
|
+
await pressDashboard(ui, r)
|
|
647
|
+
expect(r.calls.filter(isUatRead)).toContainEqual(uatCall())
|
|
648
|
+
expect(r.calls.filter(isUatSearch)).toEqual([])
|
|
649
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
650
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-306', 'row-303'])
|
|
651
|
+
expect(await textOf(ui, 'uat-of-306')).toBe('tests Loose parent #290')
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test(`a paused ticket sits under Paused between In progress and Blocked, with a resume button [${surface}]`, async ($, on) => {
|
|
655
|
+
const r = rig(on, $)
|
|
656
|
+
r.work = stateText()
|
|
657
|
+
r.tickets = [
|
|
658
|
+
...TICKETS,
|
|
659
|
+
issue(230, 'Paused with a holder', ['trailhead:build', 'trailhead:paused'], ['ToRvaLDz']),
|
|
660
|
+
issue(231, 'Paused and released', ['trailhead:build', 'trailhead:paused']),
|
|
661
|
+
issue(232, 'Paused and blocked', ['trailhead:build', 'trailhead:paused', 'trailhead:blocked'], ['bob']),
|
|
662
|
+
]
|
|
663
|
+
await $.session.start(START)
|
|
664
|
+
const ui = await openAndMount($, r)
|
|
665
|
+
|
|
666
|
+
expect(await textOf(ui, 'head-paused')).toBe('Paused · 3')
|
|
667
|
+
expect((await ui.find({ key: 'head-paused' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
668
|
+
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 2')
|
|
669
|
+
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 2')
|
|
670
|
+
// a paused ticket appears in one group only: Frontier, In progress, Paused, Blocked in order
|
|
671
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-209', 'row-210', 'row-214', 'row-208', 'row-212', 'row-230', 'row-231', 'row-232', 'row-211', 'row-213'])
|
|
672
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-frontier', 'group-in-progress', 'group-paused', 'group-blocked'])
|
|
673
|
+
for (const n of [230, 231, 232]) {
|
|
674
|
+
expect(await hasKey(ui, `resume-${n}`)).toBe(true)
|
|
675
|
+
expect(await hasKey(ui, `work-${n}`)).toBe(false)
|
|
676
|
+
expect(await hasKey(ui, `holder-${n}`)).toBe(false)
|
|
677
|
+
}
|
|
678
|
+
expect((await ui.find({ key: 'ticket-231' }))?.children[0].props).not.toHaveProperty('dimColor')
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test(`the dashboard view groups paused whiteboard tickets the same way [${surface}]`, async ($, on) => {
|
|
682
|
+
const r = rig(on, $)
|
|
683
|
+
r.whiteboard = [...WHITEBOARD, issue(305, 'Paused loose one', [...WB, 'trailhead:task', 'trailhead:paused'], ['alice'])]
|
|
684
|
+
await $.session.start(START)
|
|
685
|
+
const ui = await openAndMount($, r)
|
|
686
|
+
await pressDashboard(ui, r)
|
|
687
|
+
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
688
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-305', 'row-303'])
|
|
689
|
+
expect(await hasKey(ui, 'work-305')).toBe(false)
|
|
690
|
+
expect(await hasKey(ui, 'resume-305')).toBe(true)
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
test(`needs-you tickets sit in their own group first, once each, with a work button unless held [${surface}]`, async ($, on) => {
|
|
694
|
+
const r = rig(on, $)
|
|
695
|
+
r.tickets = [
|
|
696
|
+
...TICKETS,
|
|
697
|
+
issue(250, 'Needs you plain', ['trailhead:build', 'trailhead:needs-you']),
|
|
698
|
+
issue(251, 'Needs you paused', ['trailhead:build', 'trailhead:needs-you', 'trailhead:paused']),
|
|
699
|
+
issue(252, 'Needs you paused and held', ['trailhead:build', 'trailhead:needs-you', 'trailhead:paused'], ['bob']),
|
|
700
|
+
issue(253, 'Needs you UAT', ['trailhead:task', 'trailhead:needs-you', 'trailhead:uat']),
|
|
701
|
+
issue(254, 'Needs you held', ['trailhead:build', 'trailhead:needs-you'], ['alice']),
|
|
702
|
+
issue(255, 'Needs you blocked', ['trailhead:build', 'trailhead:needs-you', 'trailhead:blocked']),
|
|
703
|
+
]
|
|
704
|
+
await $.session.start(START)
|
|
705
|
+
const ui = await openAndMount($, r)
|
|
706
|
+
|
|
707
|
+
expect(await textOf(ui, 'head-needs-you')).toBe('Needs you · 6')
|
|
708
|
+
expect((await ui.find({ key: 'head-needs-you' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
709
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-needs-you', 'group-frontier', 'group-in-progress', 'group-blocked'])
|
|
710
|
+
// each ticket once, ahead of the frontier rows; the other counts leave them out
|
|
711
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-250', 'row-251', 'row-252', 'row-253', 'row-254', 'row-255', 'row-209', 'row-210', 'row-214', 'row-208', 'row-212', 'row-211', 'row-213'])
|
|
712
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
713
|
+
expect(await hasKey(ui, 'head-paused')).toBe(false)
|
|
714
|
+
expect(await hasKey(ui, 'head-uat')).toBe(false)
|
|
715
|
+
for (const n of [250, 251, 253]) {
|
|
716
|
+
expect(await hasKey(ui, `work-${n}`)).toBe(true)
|
|
717
|
+
expect(await hasKey(ui, `resume-${n}`)).toBe(false)
|
|
718
|
+
}
|
|
719
|
+
for (const n of [252, 254]) {
|
|
720
|
+
expect(await textOf(ui, `holder-${n}`)).toBe(n === 252 ? '@bob' : '@alice')
|
|
721
|
+
expect(await hasKey(ui, `work-${n}`)).toBe(false)
|
|
722
|
+
expect(await hasKey(ui, `resume-${n}`)).toBe(false)
|
|
723
|
+
}
|
|
724
|
+
expect(await hasKey(ui, 'work-255')).toBe(false)
|
|
725
|
+
expect(await hasKey(ui, 'resume-255')).toBe(false)
|
|
726
|
+
expect(await hasKey(ui, 'holder-255')).toBe(false)
|
|
727
|
+
expect((await ui.find({ key: 'ticket-255' }))?.children[0].props).toMatchObject({ dimColor: true })
|
|
728
|
+
|
|
729
|
+
await ui.press({ key: 'work-251' })
|
|
730
|
+
await settle(r)
|
|
731
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '251' }])
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
test(`the dashboard view puts a needs-you whiteboard ticket first in its own group [${surface}]`, async ($, on) => {
|
|
735
|
+
const r = rig(on, $)
|
|
736
|
+
r.whiteboard = [...WHITEBOARD, issue(307, 'Loose needs you', [...WB, 'trailhead:task', 'trailhead:needs-you', 'trailhead:paused'])]
|
|
737
|
+
await $.session.start(START)
|
|
738
|
+
const ui = await openAndMount($, r)
|
|
739
|
+
await pressDashboard(ui, r)
|
|
740
|
+
expect(await textOf(ui, 'head-needs-you')).toBe('Needs you · 1')
|
|
741
|
+
// first ticket group, right after the maps group
|
|
742
|
+
expect((await keysOf(ui, 'group-')).slice(0, 2)).toEqual(['group-maps', 'group-needs-you'])
|
|
743
|
+
expect((await keysOf(ui, 'row-'))[0]).toBe('row-307')
|
|
744
|
+
expect(await hasKey(ui, 'head-paused')).toBe(false)
|
|
745
|
+
expect(await hasKey(ui, 'work-307')).toBe(true)
|
|
746
|
+
expect(await hasKey(ui, 'resume-307')).toBe(false)
|
|
747
|
+
})
|
|
748
|
+
|
|
322
749
|
test(`ticket rows are spaced apart on the desktop only [${surface}]`, async ($, on) => {
|
|
323
750
|
const r = rig(on)
|
|
324
751
|
await $.session.start(START)
|
|
@@ -489,6 +916,70 @@ for (const surface of SURFACES) {
|
|
|
489
916
|
expect(await ui.find({ key: 'mark-212' })).toBeUndefined()
|
|
490
917
|
})
|
|
491
918
|
|
|
919
|
+
test(`another session's work state does not move this session's mark [${surface}]`, async ($, on) => {
|
|
920
|
+
const r = rig(on, $)
|
|
921
|
+
r.work = stateText()
|
|
922
|
+
await $.session.start(START)
|
|
923
|
+
const ui = await openAndMount($, r)
|
|
924
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
925
|
+
|
|
926
|
+
// a second session on the same map takes over the shared file
|
|
927
|
+
r.work = stateText({ ticket: 212, title: 'Native gh-backed tools', session: NEXT_SESSION })
|
|
928
|
+
await r.clock.advance(FLOOR)
|
|
929
|
+
await completeTurn($)
|
|
930
|
+
await settle(r)
|
|
931
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
932
|
+
expect(await textOf(ui, 'mark-208')).toBe('▸ ')
|
|
933
|
+
expect(await ui.find({ key: 'self-212' })).toBeUndefined()
|
|
934
|
+
expect(await textOf(ui, 'holder-212')).toBe('@alice')
|
|
935
|
+
|
|
936
|
+
// and then removes it: the last state read was not ours, so the mark stays
|
|
937
|
+
r.work = null
|
|
938
|
+
await r.clock.advance(FLOOR)
|
|
939
|
+
await completeTurn($)
|
|
940
|
+
await settle(r)
|
|
941
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
test(`this session's own Resolve, which removes the file, clears the mark [${surface}]`, async ($, on) => {
|
|
945
|
+
const r = rig(on, $)
|
|
946
|
+
r.work = stateText()
|
|
947
|
+
await $.session.start(START)
|
|
948
|
+
const ui = await openAndMount($, r)
|
|
949
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
950
|
+
r.work = null
|
|
951
|
+
await r.clock.advance(FLOOR)
|
|
952
|
+
await completeTurn($)
|
|
953
|
+
await settle(r)
|
|
954
|
+
expect(await ui.find({ key: 'self-208' })).toBeUndefined()
|
|
955
|
+
expect(await ui.find({ key: 'mark-208' })).toBeUndefined()
|
|
956
|
+
expect(await textOf(ui, 'holder-208')).toBe('@ToRvaLDz')
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
test(`each session marks its own ticket on the same map [${surface}]`, async ($, on) => {
|
|
960
|
+
const r = rig(on, $)
|
|
961
|
+
r.session = NEXT_SESSION
|
|
962
|
+
r.work = stateText({ ticket: 212, title: 'Native gh-backed tools', session: NEXT_SESSION })
|
|
963
|
+
await $.session.start(START)
|
|
964
|
+
const ui = await openAndMount($, r)
|
|
965
|
+
expect(await textOf(ui, 'self-212')).toBe('this session')
|
|
966
|
+
expect(await textOf(ui, 'mark-212')).toBe('▸ ')
|
|
967
|
+
expect(await textOf(ui, 'holder-208')).toBe('@ToRvaLDz')
|
|
968
|
+
expect(await ui.find({ key: 'self-208' })).toBeUndefined()
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
test(`a /clear starts the new session with no ticket of its own [${surface}]`, async ($, on) => {
|
|
972
|
+
const r = rig(on, $)
|
|
973
|
+
r.work = stateText()
|
|
974
|
+
await $.session.start(START)
|
|
975
|
+
const ui = await openAndMount($, r)
|
|
976
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
977
|
+
await $.command.run({ command: 'clear', args: '' })
|
|
978
|
+
await settle(r)
|
|
979
|
+
expect(await ui.find({ key: 'self-208' })).toBeUndefined()
|
|
980
|
+
expect(await ui.find({ key: 'mark-208' })).toBeUndefined()
|
|
981
|
+
})
|
|
982
|
+
|
|
492
983
|
test(`unverified tickets are counted in the plural [${surface}]`, async ($, on) => {
|
|
493
984
|
const r = rig(on, $)
|
|
494
985
|
r.tickets = [issue(1, 'a', ['trailhead:unverified']), issue(2, 'b', ['trailhead:unverified']), issue(3, 'c', ['trailhead:build'])]
|
|
@@ -522,7 +1013,7 @@ for (const surface of SURFACES) {
|
|
|
522
1013
|
await completeTurn($)
|
|
523
1014
|
await settle(r)
|
|
524
1015
|
// the open maps are still read, so one can be picked; no map is
|
|
525
|
-
expect(r.calls).toEqual([
|
|
1016
|
+
expect(r.calls).toEqual([MAPS_CALL, countCall(14, 198)])
|
|
526
1017
|
})
|
|
527
1018
|
|
|
528
1019
|
test(`first fetch in flight: the map number and Loading tickets [${surface}]`, async ($, on) => {
|
|
@@ -647,6 +1138,199 @@ for (const surface of SURFACES) {
|
|
|
647
1138
|
expect(fetches(r)).toBe(2)
|
|
648
1139
|
})
|
|
649
1140
|
|
|
1141
|
+
// The host drops a refused timer silently (a closed ticket stayed on the pane for good): the pane must not
|
|
1142
|
+
// wait on a dead handle.
|
|
1143
|
+
test(`a slow timer the host dropped is started again by the next trigger [${surface}]`, async ($, on) => {
|
|
1144
|
+
// Above the mock clock, which answers the wait without passing it on.
|
|
1145
|
+
let isDropping = false
|
|
1146
|
+
on('clock.every', { ms: TIMER }, async ($$, e, next) => (isDropping ? { deny: 'dropped' } : next(e)))
|
|
1147
|
+
const r = rig(on, $)
|
|
1148
|
+
await $.session.start(START)
|
|
1149
|
+
// the first period is refused: the interval ends unseen
|
|
1150
|
+
isDropping = true
|
|
1151
|
+
await openAndMount($, r)
|
|
1152
|
+
expect(fetches(r)).toBe(1)
|
|
1153
|
+
await r.clock.advance(TIMER)
|
|
1154
|
+
await settle(r)
|
|
1155
|
+
isDropping = false
|
|
1156
|
+
await r.clock.advance(TIMER)
|
|
1157
|
+
await settle(r)
|
|
1158
|
+
expect(fetches(r)).toBe(1)
|
|
1159
|
+
|
|
1160
|
+
// one trigger brings the timer back
|
|
1161
|
+
await completeTurn($)
|
|
1162
|
+
await settle(r)
|
|
1163
|
+
expect(fetches(r)).toBe(2)
|
|
1164
|
+
await r.clock.advance(TIMER)
|
|
1165
|
+
await settle(r)
|
|
1166
|
+
expect(fetches(r)).toBe(3)
|
|
1167
|
+
})
|
|
1168
|
+
|
|
1169
|
+
// The open list is a GraphQL read, never the search-backed `gh issue list --label`, which lags a write.
|
|
1170
|
+
const listReads = (r: Rig) => r.calls.filter(argv => argv[2] === 'list' && argv.includes('--label') && argv.includes('trailhead:map-198') && argv.includes('open') && !argv.includes('trailhead:uat'))
|
|
1171
|
+
|
|
1172
|
+
test(`a gh issue create shows the new ticket's row without a manual refresh [${surface}]`, async ($, on) => {
|
|
1173
|
+
const r = rig(on, $)
|
|
1174
|
+
await $.session.start(START)
|
|
1175
|
+
const ui = await openAndMount($, r)
|
|
1176
|
+
expect(await hasKey(ui, 'row-240')).toBe(false)
|
|
1177
|
+
|
|
1178
|
+
r.tickets = [...TICKETS, issue(240, 'Just created', ['trailhead:build'])]
|
|
1179
|
+
await r.clock.advance(FLOOR)
|
|
1180
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue create -t t' } as never)
|
|
1181
|
+
await settle(r)
|
|
1182
|
+
expect(await hasKey(ui, 'row-240')).toBe(true)
|
|
1183
|
+
expect(listReads(r)).toEqual([])
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
test(`a gh issue close drops the closed ticket's row without a manual refresh [${surface}]`, async ($, on) => {
|
|
1187
|
+
const r = rig(on, $)
|
|
1188
|
+
await $.session.start(START)
|
|
1189
|
+
const ui = await openAndMount($, r)
|
|
1190
|
+
expect(await hasKey(ui, 'row-209')).toBe(true)
|
|
1191
|
+
|
|
1192
|
+
r.tickets = TICKETS.filter(t => t.number !== 209)
|
|
1193
|
+
await r.clock.advance(FLOOR)
|
|
1194
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue close 209' } as never)
|
|
1195
|
+
await settle(r)
|
|
1196
|
+
expect(await hasKey(ui, 'row-209')).toBe(false)
|
|
1197
|
+
expect(listReads(r)).toEqual([])
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
// The reads asked while the pane was undrawn (the person on another session) are dropped: drawn again with a
|
|
1201
|
+
// list older than one slow-timer period, the pane reads at once instead of showing a closed ticket.
|
|
1202
|
+
test(`a pane drawn again with an overdue list reads at once [${surface}]`, async ($, on) => {
|
|
1203
|
+
const r = rig(on, $)
|
|
1204
|
+
await $.session.start(START)
|
|
1205
|
+
const first = await openAndMount($, r)
|
|
1206
|
+
expect(fetches(r)).toBe(1)
|
|
1207
|
+
|
|
1208
|
+
await first.unmount()
|
|
1209
|
+
r.panes = r.panes.map(p => ({ ...p, isPlaced: false }))
|
|
1210
|
+
r.tickets = TICKETS.filter(t => t.number !== 209)
|
|
1211
|
+
await r.clock.advance(FLOOR)
|
|
1212
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue close 209' } as never)
|
|
1213
|
+
await completeTurn($)
|
|
1214
|
+
await r.clock.advance(TIMER + 30_001)
|
|
1215
|
+
await settle(r)
|
|
1216
|
+
expect(fetches(r)).toBe(1)
|
|
1217
|
+
|
|
1218
|
+
r.panes = r.panes.map(p => ({ ...p, isPlaced: true }))
|
|
1219
|
+
const ui = await mountPane($)
|
|
1220
|
+
await settle(r)
|
|
1221
|
+
expect(fetches(r)).toBe(2)
|
|
1222
|
+
expect(await hasKey(ui, 'row-209')).toBe(false)
|
|
1223
|
+
})
|
|
1224
|
+
|
|
1225
|
+
test(`a create inside the floor shows at the floor's end [${surface}]`, async ($, on) => {
|
|
1226
|
+
const r = rig(on, $)
|
|
1227
|
+
await $.session.start(START)
|
|
1228
|
+
const ui = await openAndMount($, r)
|
|
1229
|
+
await r.clock.advance(2_000)
|
|
1230
|
+
r.tickets = [...TICKETS, issue(240, 'Just created', ['trailhead:build'])]
|
|
1231
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue create -t t' } as never)
|
|
1232
|
+
await settle(r)
|
|
1233
|
+
expect(await hasKey(ui, 'row-240')).toBe(false)
|
|
1234
|
+
await r.clock.advance(FLOOR - 2_000)
|
|
1235
|
+
await settle(r)
|
|
1236
|
+
expect(await hasKey(ui, 'row-240')).toBe(true)
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
test(`a close inside the floor drops the row at the floor's end [${surface}]`, async ($, on) => {
|
|
1240
|
+
const r = rig(on, $)
|
|
1241
|
+
await $.session.start(START)
|
|
1242
|
+
const ui = await openAndMount($, r)
|
|
1243
|
+
await r.clock.advance(2_000)
|
|
1244
|
+
r.tickets = TICKETS.filter(t => t.number !== 209)
|
|
1245
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue close 209' } as never)
|
|
1246
|
+
await settle(r)
|
|
1247
|
+
expect(await hasKey(ui, 'row-209')).toBe(true)
|
|
1248
|
+
await r.clock.advance(FLOOR - 2_000)
|
|
1249
|
+
await settle(r)
|
|
1250
|
+
expect(await hasKey(ui, 'row-209')).toBe(false)
|
|
1251
|
+
})
|
|
1252
|
+
|
|
1253
|
+
test(`a list of 150 tickets is read in two pages [${surface}]`, async ($, on) => {
|
|
1254
|
+
const r = rig(on, $)
|
|
1255
|
+
r.tickets = Array.from({ length: 150 }, (_, i) => issue(i + 1, `t${i + 1}`, ['trailhead:build']))
|
|
1256
|
+
await $.session.start(START)
|
|
1257
|
+
const ui = await openAndMount($, r)
|
|
1258
|
+
expect(r.calls.filter(isTicketsRead)).toEqual([ticketsCall('trailhead:map-198'), ticketsCall('trailhead:map-198', 'c100')])
|
|
1259
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 150')
|
|
1260
|
+
expect(await hasKey(ui, 'capped')).toBe(false)
|
|
1261
|
+
expect(listReads(r)).toEqual([])
|
|
1262
|
+
})
|
|
1263
|
+
|
|
1264
|
+
test(`a failure on the second page discards the read and falls back to gh issue list once [${surface}]`, async ($, on) => {
|
|
1265
|
+
const r = rig(on, $)
|
|
1266
|
+
r.tickets = Array.from({ length: 150 }, (_, i) => issue(i + 1, `t${i + 1}`, ['trailhead:build']))
|
|
1267
|
+
r.gh = argv => (isTicketsRead(argv) && argv.some(a => a.startsWith('after=')) ? { exitCode: 1, stdout: '', stderr: 'boom' } : r.defaultGh(argv))
|
|
1268
|
+
await $.session.start(START)
|
|
1269
|
+
const ui = await openAndMount($, r)
|
|
1270
|
+
expect(listReads(r)).toEqual([ticketsListCall('trailhead:map-198')])
|
|
1271
|
+
// the fallback list is whole (the rig's list answer), never the 100 rows of page one
|
|
1272
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 150')
|
|
1273
|
+
expect(r.calls.filter(isTicketsRead)).toHaveLength(2)
|
|
1274
|
+
})
|
|
1275
|
+
|
|
1276
|
+
test(`an unreadable first page falls back the same way [${surface}]`, async ($, on) => {
|
|
1277
|
+
const r = rig(on, $)
|
|
1278
|
+
r.gh = argv => (isTicketsRead(argv) ? ok('not json') : argv[2] === 'list' && argv.includes('trailhead:map-198') && argv.includes('open') ? ok(JSON.stringify([issue(300, 'from the list', ['trailhead:build'])])) : r.defaultGh(argv))
|
|
1279
|
+
await $.session.start(START)
|
|
1280
|
+
const ui = await openAndMount($, r)
|
|
1281
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-300'])
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
test(`the whiteboard read keeps only the rows that are also tickets [${surface}]`, async ($, on) => {
|
|
1285
|
+
const r = rig(on, $)
|
|
1286
|
+
r.whiteboard = [...WHITEBOARD, issue(310, 'Whiteboard label only', ['trailhead:whiteboard', 'trailhead:task'])]
|
|
1287
|
+
await $.session.start(START)
|
|
1288
|
+
const ui = await openAndMount($, r)
|
|
1289
|
+
await pressDashboard(ui, r)
|
|
1290
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-303'])
|
|
1291
|
+
})
|
|
1292
|
+
|
|
1293
|
+
test(`a write that reads at once is followed by one more read at the floor's end [${surface}]`, async ($, on) => {
|
|
1294
|
+
const r = rig(on, $)
|
|
1295
|
+
await $.session.start(START)
|
|
1296
|
+
await openAndMount($, r)
|
|
1297
|
+
await r.clock.advance(FLOOR)
|
|
1298
|
+
const before = fetches(r)
|
|
1299
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue close 5' } as never)
|
|
1300
|
+
await settle(r)
|
|
1301
|
+
expect(fetches(r)).toBe(before + 1)
|
|
1302
|
+
await r.clock.advance(FLOOR - 1)
|
|
1303
|
+
await settle(r)
|
|
1304
|
+
expect(fetches(r)).toBe(before + 1)
|
|
1305
|
+
await r.clock.advance(1)
|
|
1306
|
+
await settle(r)
|
|
1307
|
+
expect(fetches(r)).toBe(before + 2)
|
|
1308
|
+
// the follow-up is spent
|
|
1309
|
+
await r.clock.advance(FLOOR)
|
|
1310
|
+
await settle(r)
|
|
1311
|
+
expect(fetches(r)).toBe(before + 2)
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1314
|
+
test(`a burst of writes costs one read per floor plus one trailing [${surface}]`, async ($, on) => {
|
|
1315
|
+
const r = rig(on, $)
|
|
1316
|
+
await $.session.start(START)
|
|
1317
|
+
await openAndMount($, r)
|
|
1318
|
+
await r.clock.advance(FLOOR)
|
|
1319
|
+
const before = fetches(r)
|
|
1320
|
+
for (const command of ['gh issue close 5', 'gh issue create -t a', 'gh issue comment 5 -b x', 'gh issue edit 5 --add-label y', 'gh issue close 6']) {
|
|
1321
|
+
await $.tool.call({ tool: 'Bash', command } as never)
|
|
1322
|
+
await r.clock.advance(1_000)
|
|
1323
|
+
}
|
|
1324
|
+
await settle(r)
|
|
1325
|
+
expect(fetches(r)).toBe(before + 1)
|
|
1326
|
+
await r.clock.advance(FLOOR)
|
|
1327
|
+
await settle(r)
|
|
1328
|
+
expect(fetches(r)).toBe(before + 2)
|
|
1329
|
+
await r.clock.advance(FLOOR)
|
|
1330
|
+
await settle(r)
|
|
1331
|
+
expect(fetches(r)).toBe(before + 2)
|
|
1332
|
+
})
|
|
1333
|
+
|
|
650
1334
|
test(`session start, turn complete and the slow timer refresh an open pane [${surface}]`, async ($, on) => {
|
|
651
1335
|
const r = rig(on, $)
|
|
652
1336
|
await $.session.start(START)
|
|
@@ -673,15 +1357,21 @@ for (const surface of SURFACES) {
|
|
|
673
1357
|
await $.session.start(START)
|
|
674
1358
|
await openAndMount($, r)
|
|
675
1359
|
const base = fetches(r)
|
|
676
|
-
for (const command of ['gh issue list --state open', 'ls -la', 'gh pr close 4', 'gh issue view 5']) {
|
|
1360
|
+
for (const command of ['gh issue list --state open', 'ls -la', 'gh pr close 4', 'gh issue view 5', 'gh api repos/o/r/issues/5', 'gh api graphql -f query=x']) {
|
|
677
1361
|
await r.clock.advance(FLOOR)
|
|
678
1362
|
await $.tool.call({ tool: 'Bash', command } as never)
|
|
679
1363
|
await settle(r)
|
|
680
1364
|
}
|
|
681
1365
|
expect(fetches(r)).toBe(base)
|
|
682
1366
|
|
|
683
|
-
for (const command of ['gh issue close 5', 'gh --repo o/r issue close 5', 'gh issue comment 5 -b hi', 'gh issue edit 5 --add-label x', 'gh issue create -t t', 'gh issue reopen 5']) {
|
|
684
|
-
|
|
1367
|
+
for (const command of ['gh issue close 5', 'gh --repo o/r issue close 5', 'gh issue comment 5 -b hi', 'gh issue edit 5 --add-label x', 'gh issue create -t t', 'gh issue reopen 5', 'gh issue delete 5', 'gh api repos/o/r/issues/198/sub_issues -F sub_issue_id=1', 'node /t/trailhead-issue-body.js append --issue 198 --section x --line y']) {
|
|
1368
|
+
// until a whole floor passes with no read (the previous write's follow-up, the slow timer), so this write starts outside one
|
|
1369
|
+
let seen = -1
|
|
1370
|
+
while (seen !== fetches(r)) {
|
|
1371
|
+
seen = fetches(r)
|
|
1372
|
+
await r.clock.advance(FLOOR)
|
|
1373
|
+
await settle(r)
|
|
1374
|
+
}
|
|
685
1375
|
const before = fetches(r)
|
|
686
1376
|
await $.tool.call({ tool: 'Bash', command } as never)
|
|
687
1377
|
await settle(r)
|
|
@@ -754,15 +1444,15 @@ for (const surface of SURFACES) {
|
|
|
754
1444
|
await $.session.start(START)
|
|
755
1445
|
const ui = await openAndMount($, r)
|
|
756
1446
|
const buttons = (await ui.findAll({ type: 'Button' })).map((b: any) => b.key)
|
|
757
|
-
expect(buttons).toEqual(['map-14', 'work-209', 'work-210', 'work-214', 'refresh', 'close'])
|
|
1447
|
+
expect(buttons).toEqual(['open-whiteboard', 'map-14', 'work-209', 'work-210', 'work-214', 'refresh', 'close'])
|
|
758
1448
|
expect((await ui.find({ key: 'work-209' }))?.props).toMatchObject({ label: 'work', variant: 'primary' })
|
|
759
1449
|
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
760
1450
|
|
|
761
1451
|
const first = ui.press({ key: 'work-209' })
|
|
762
1452
|
await settle(r)
|
|
763
1453
|
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
764
|
-
expect(await textOf(ui, '
|
|
765
|
-
expect((await ui.find({ key: '
|
|
1454
|
+
expect(await textOf(ui, 'starting-209')).toBe('starting')
|
|
1455
|
+
expect((await ui.find({ key: 'starting-209' }))?.children[0]).toMatchObject({ props: { color: AMBER } })
|
|
766
1456
|
expect(await ui.find({ key: 'work-209' })).toBeUndefined()
|
|
767
1457
|
expect(await ui.find({ key: 'work-210' })).toBeDefined()
|
|
768
1458
|
|
|
@@ -770,12 +1460,12 @@ for (const surface of SURFACES) {
|
|
|
770
1460
|
await ui.press({ key: 'work-210' })
|
|
771
1461
|
await settle(r)
|
|
772
1462
|
expect(r.runs).toHaveLength(2)
|
|
773
|
-
expect(await ui.find({ key: '
|
|
1463
|
+
expect(await ui.find({ key: 'starting-210' })).toBeUndefined()
|
|
774
1464
|
|
|
775
1465
|
release()
|
|
776
1466
|
await first
|
|
777
1467
|
await settle(r)
|
|
778
|
-
expect(await ui.find({ key: '
|
|
1468
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
779
1469
|
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
780
1470
|
|
|
781
1471
|
// free again: the next press goes through
|
|
@@ -789,7 +1479,43 @@ for (const surface of SURFACES) {
|
|
|
789
1479
|
])
|
|
790
1480
|
})
|
|
791
1481
|
|
|
792
|
-
test(`a
|
|
1482
|
+
test(`a resume button on a paused row runs /trailhead:resume, shares the one-at-a-time guard and the refusal [${surface}]`, async ($, on) => {
|
|
1483
|
+
const r = rig(on, $)
|
|
1484
|
+
let release: () => void = () => undefined
|
|
1485
|
+
r.gate = new Promise<void>(resolve => (release = resolve))
|
|
1486
|
+
r.tickets = [...TICKETS, issue(230, 'Paused one', ['trailhead:build', 'trailhead:paused'], ['alice'])]
|
|
1487
|
+
await $.session.start(START)
|
|
1488
|
+
const ui = await openAndMount($, r)
|
|
1489
|
+
expect((await ui.find({ key: 'resume-230' }))?.props).toMatchObject({ label: 'resume', variant: 'primary' })
|
|
1490
|
+
|
|
1491
|
+
const first = ui.press({ key: 'resume-230' })
|
|
1492
|
+
await settle(r)
|
|
1493
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:resume', args: '230' }])
|
|
1494
|
+
expect(await textOf(ui, 'starting-230')).toBe('starting')
|
|
1495
|
+
expect(await ui.find({ key: 'resume-230' })).toBeUndefined()
|
|
1496
|
+
|
|
1497
|
+
// a work press meanwhile is ignored: one in flight at a time
|
|
1498
|
+
await ui.press({ key: 'work-210' })
|
|
1499
|
+
await settle(r)
|
|
1500
|
+
expect(r.runs).toHaveLength(2)
|
|
1501
|
+
|
|
1502
|
+
release()
|
|
1503
|
+
await first
|
|
1504
|
+
await settle(r)
|
|
1505
|
+
expect(await ui.find({ key: 'starting-230' })).toBeUndefined()
|
|
1506
|
+
expect(await ui.find({ key: 'resume-230' })).toBeDefined()
|
|
1507
|
+
|
|
1508
|
+
// outside the pinned repo it goes through, handing the pinned checkout to the verb
|
|
1509
|
+
const runs = r.runs.length
|
|
1510
|
+
r.cwd = ROOT_OTHER
|
|
1511
|
+
r.repo = { root: ROOT_OTHER, remote: 'https://github.com/o/other.git', internal: false }
|
|
1512
|
+
await ui.press({ key: 'resume-230' })
|
|
1513
|
+
await settle(r)
|
|
1514
|
+
expect(r.runs.slice(runs)).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:resume', args: `230 ${IN(ROOT)}` }])
|
|
1515
|
+
expect(r.toasts).toEqual([])
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
test(`a work command that is refused clears the queued mark [${surface}]`, async ($, on) => {
|
|
793
1519
|
const r = rig(on, $)
|
|
794
1520
|
r.refuse = 'cannot run now'
|
|
795
1521
|
await $.session.start(START)
|
|
@@ -801,7 +1527,109 @@ for (const surface of SURFACES) {
|
|
|
801
1527
|
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
802
1528
|
})
|
|
803
1529
|
|
|
804
|
-
test(`a work press
|
|
1530
|
+
test(`a work press with the session outside any repo goes through with the pinned checkout handed over [${surface}]`, async ($, on) => {
|
|
1531
|
+
const r = rig(on, $)
|
|
1532
|
+
await $.session.start(START)
|
|
1533
|
+
const ui = await openAndMount($, r)
|
|
1534
|
+
r.cwd = '/tmp'
|
|
1535
|
+
r.repo = null
|
|
1536
|
+
await ui.press({ key: 'work-209' })
|
|
1537
|
+
await settle(r)
|
|
1538
|
+
expect(r.toasts).toEqual([])
|
|
1539
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: `209 ${IN(ROOT)}` }])
|
|
1540
|
+
// The token the verb receives decodes (plain base64url, the helper's alphabet) to the pinned root; the suite has no Buffer.
|
|
1541
|
+
const [number, word] = r.runs[1].args.split(' ')
|
|
1542
|
+
expect(number).toBe('209')
|
|
1543
|
+
expect(word).toMatch(/^--in=[A-Za-z0-9_-]+$/)
|
|
1544
|
+
expect(word).toBe(IN(ROOT))
|
|
1545
|
+
expect(atob(word.slice('--in='.length).replace(/-/g, '+').replace(/_/g, '/'))).toBe(ROOT)
|
|
1546
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1547
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1548
|
+
})
|
|
1549
|
+
|
|
1550
|
+
test(`a work press with the session in another repo goes through with the pinned checkout handed over [${surface}]`, async ($, on) => {
|
|
1551
|
+
const r = rig(on, $)
|
|
1552
|
+
await $.session.start(START)
|
|
1553
|
+
const ui = await openAndMount($, r)
|
|
1554
|
+
r.cwd = ROOT_OTHER
|
|
1555
|
+
r.repo = { root: ROOT_OTHER, remote: 'https://github.com/o/other.git', internal: false }
|
|
1556
|
+
await ui.press({ key: 'work-209' })
|
|
1557
|
+
await settle(r)
|
|
1558
|
+
expect(r.toasts).toEqual([])
|
|
1559
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: `209 ${IN(ROOT)}` }])
|
|
1560
|
+
})
|
|
1561
|
+
|
|
1562
|
+
test(`a pinned worktree checkout is handed over by its own root [${surface}]`, async ($, on) => {
|
|
1563
|
+
const r = rig(on, $)
|
|
1564
|
+
r.top = ROOT_TREE
|
|
1565
|
+
r.cwd = ROOT_TREE
|
|
1566
|
+
r.sessionRoot = ROOT_TREE
|
|
1567
|
+
r.active = '14\n'
|
|
1568
|
+
r.treeActive = '198\n'
|
|
1569
|
+
await $.session.start({ ...START, cwd: ROOT_TREE })
|
|
1570
|
+
const ui = await openAndMount($, r)
|
|
1571
|
+
// From inside it nothing is handed over.
|
|
1572
|
+
await ui.press({ key: 'work-209' })
|
|
1573
|
+
await settle(r)
|
|
1574
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1575
|
+
// A shell cd out of it: the token names the worktree root, not the main tree's.
|
|
1576
|
+
r.cwd = ROOT_OTHER
|
|
1577
|
+
r.repo = { root: ROOT_OTHER, remote: 'https://github.com/o/other.git', internal: false }
|
|
1578
|
+
r.runs.length = 0
|
|
1579
|
+
await ui.press({ key: 'work-209' })
|
|
1580
|
+
await settle(r)
|
|
1581
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: `209 ${IN(ROOT_TREE)}` }])
|
|
1582
|
+
})
|
|
1583
|
+
|
|
1584
|
+
test(`a work press whose pinned checkout is gone while the session is out is refused before the clear [${surface}]`, async ($, on) => {
|
|
1585
|
+
const r = rig(on, $)
|
|
1586
|
+
await $.session.start(START)
|
|
1587
|
+
const ui = await openAndMount($, r)
|
|
1588
|
+
r.gone = [ROOT]
|
|
1589
|
+
r.cwd = '/tmp'
|
|
1590
|
+
r.repo = null
|
|
1591
|
+
await ui.press({ key: 'work-209' })
|
|
1592
|
+
await settle(r)
|
|
1593
|
+
expect(r.runs).toEqual([])
|
|
1594
|
+
expect(r.toasts).toHaveLength(1)
|
|
1595
|
+
expect(r.toasts[0]).toContain('#209')
|
|
1596
|
+
expect(r.toasts[0]).toContain('unreadable')
|
|
1597
|
+
expect(r.toasts[0].length).toBeLessThanOrEqual(40)
|
|
1598
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1599
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1600
|
+
})
|
|
1601
|
+
|
|
1602
|
+
test(`a work press in the pinned repo goes through although session.repo answers null [${surface}]`, async ($, on) => {
|
|
1603
|
+
const r = rig(on, $)
|
|
1604
|
+
await $.session.start(START)
|
|
1605
|
+
const ui = await openAndMount($, r)
|
|
1606
|
+
r.repo = null
|
|
1607
|
+
r.gitRemote = 'git@github.com:o/r.git'
|
|
1608
|
+
await ui.press({ key: 'work-209' })
|
|
1609
|
+
await settle(r)
|
|
1610
|
+
expect(r.toasts).toEqual([])
|
|
1611
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1612
|
+
})
|
|
1613
|
+
|
|
1614
|
+
test(`a work press whose checkout cannot be read is refused, and the next one from the repo goes through [${surface}]`, async ($, on) => {
|
|
1615
|
+
const r = rig(on, $)
|
|
1616
|
+
await $.session.start(START)
|
|
1617
|
+
const ui = await openAndMount($, r)
|
|
1618
|
+
r.repoFails = true
|
|
1619
|
+
await ui.press({ key: 'work-209' })
|
|
1620
|
+
await settle(r)
|
|
1621
|
+
expect(r.runs).toEqual([])
|
|
1622
|
+
expect(r.toasts).toHaveLength(1)
|
|
1623
|
+
expect(r.toasts[0]).toContain('unreadable')
|
|
1624
|
+
expect(r.toasts[0]).not.toContain('cd into')
|
|
1625
|
+
expect(r.toasts[0].length).toBeLessThanOrEqual(40)
|
|
1626
|
+
r.repoFails = false
|
|
1627
|
+
await ui.press({ key: 'work-209' })
|
|
1628
|
+
await settle(r)
|
|
1629
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1630
|
+
})
|
|
1631
|
+
|
|
1632
|
+
test(`a work press clears the conversation first, the row staying starting across the reset [${surface}]`, async ($, on) => {
|
|
805
1633
|
const r = rig(on, $)
|
|
806
1634
|
let releaseClear: () => void = () => undefined
|
|
807
1635
|
let releaseWork: () => void = () => undefined
|
|
@@ -813,7 +1641,7 @@ for (const surface of SURFACES) {
|
|
|
813
1641
|
const first = ui.press({ key: 'work-209' })
|
|
814
1642
|
await settle(r)
|
|
815
1643
|
expect(r.runs).toEqual([{ command: 'clear', args: '' }])
|
|
816
|
-
expect(await textOf(ui, '
|
|
1644
|
+
expect(await textOf(ui, 'starting-209')).toBe('starting')
|
|
817
1645
|
|
|
818
1646
|
// a second press while the clear is pending is ignored
|
|
819
1647
|
await ui.press({ key: 'work-210' })
|
|
@@ -826,549 +1654,1858 @@ for (const surface of SURFACES) {
|
|
|
826
1654
|
expect(r.session).toBe(`${NEXT_SESSION}-1`)
|
|
827
1655
|
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
828
1656
|
await ui.redraw()
|
|
829
|
-
expect(await textOf(ui, '
|
|
1657
|
+
expect(await textOf(ui, 'starting-209')).toBe('starting')
|
|
830
1658
|
await ui.press({ key: 'work-210' })
|
|
831
1659
|
expect(r.runs).toHaveLength(2)
|
|
832
1660
|
|
|
833
1661
|
releaseWork()
|
|
834
1662
|
await first
|
|
835
1663
|
await settle(r)
|
|
836
|
-
expect(await ui.find({ key: '
|
|
1664
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
837
1665
|
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
838
1666
|
})
|
|
839
1667
|
|
|
840
|
-
|
|
1668
|
+
// The main loop's turn, as the host raises it: a press during it is held by the panel until it ends.
|
|
1669
|
+
const startMainTurn = ($: any, id = 'held-turn') => $.turn.start({ text: 'go', turnId: id })
|
|
1670
|
+
const abortTurn = ($: any) =>
|
|
1671
|
+
$.turn.complete({ turnId: `t${(turns += 1)}`, answer: '', durationMs: 1, isAborted: true, reason: 'aborted' })
|
|
1672
|
+
|
|
1673
|
+
test(`a press during a running turn is held with a cancel button and runs when the turn ends [${surface}]`, async ($, on) => {
|
|
841
1674
|
const r = rig(on, $)
|
|
842
|
-
r.refuseClear = 'cannot clear now'
|
|
843
1675
|
await $.session.start(START)
|
|
844
1676
|
const ui = await openAndMount($, r)
|
|
1677
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeUndefined()
|
|
1678
|
+
|
|
1679
|
+
await startMainTurn($)
|
|
845
1680
|
await ui.press({ key: 'work-209' })
|
|
846
1681
|
await settle(r)
|
|
847
|
-
|
|
848
|
-
expect(r.
|
|
1682
|
+
// Nothing reaches the host while the turn runs.
|
|
1683
|
+
expect(r.runs).toEqual([])
|
|
1684
|
+
expect(await textOf(ui, 'queued-209')).toBe('queued')
|
|
1685
|
+
expect((await ui.find({ key: 'queued-209' }))?.children[0]).toMatchObject({ props: { color: AMBER } })
|
|
1686
|
+
expect((await ui.find({ key: 'cancel-209' }))?.props).toMatchObject({ label: 'cancel' })
|
|
1687
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
1688
|
+
expect(await ui.find({ key: 'work-209' })).toBeUndefined()
|
|
1689
|
+
|
|
1690
|
+
// A second press while one is held is ignored.
|
|
1691
|
+
await ui.press({ key: 'work-210' })
|
|
1692
|
+
await settle(r)
|
|
1693
|
+
expect(r.runs).toEqual([])
|
|
1694
|
+
expect(await ui.find({ key: 'queued-210' })).toBeUndefined()
|
|
1695
|
+
|
|
1696
|
+
await completeTurn($)
|
|
1697
|
+
await settle(r)
|
|
1698
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1699
|
+
expect(r.session).toBe(`${NEXT_SESSION}-1`)
|
|
849
1700
|
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
850
1701
|
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
851
1702
|
})
|
|
852
1703
|
|
|
853
|
-
test(`a
|
|
1704
|
+
test(`a press held in the repo while the session cd's out is sent with the pinned checkout handed over [${surface}]`, async ($, on) => {
|
|
854
1705
|
const r = rig(on, $)
|
|
855
|
-
r.denyClear = true
|
|
856
1706
|
await $.session.start(START)
|
|
857
1707
|
const ui = await openAndMount($, r)
|
|
1708
|
+
await startMainTurn($)
|
|
858
1709
|
await ui.press({ key: 'work-209' })
|
|
859
1710
|
await settle(r)
|
|
860
|
-
expect(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1711
|
+
expect(await textOf(ui, 'queued-209')).toBe('queued')
|
|
1712
|
+
// The turn moves the session out of any repo (a GitHub repo would move the pin along with it).
|
|
1713
|
+
r.cwd = '/tmp'
|
|
1714
|
+
r.repo = null
|
|
1715
|
+
await completeTurn($)
|
|
1716
|
+
await settle(r)
|
|
1717
|
+
expect(r.toasts).toEqual([])
|
|
1718
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: `209 ${IN(ROOT)}` }])
|
|
864
1719
|
})
|
|
865
1720
|
|
|
866
|
-
test(`
|
|
1721
|
+
test(`a held press whose checkout cannot be read at release is refused before the clear [${surface}]`, async ($, on) => {
|
|
867
1722
|
const r = rig(on, $)
|
|
868
1723
|
await $.session.start(START)
|
|
869
1724
|
const ui = await openAndMount($, r)
|
|
870
|
-
await
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
r.duringClear = () => (r.active = '14\n')
|
|
874
|
-
await $.command.run({ command: 'clear', args: '' } as never)
|
|
875
|
-
await ui.redraw()
|
|
1725
|
+
await startMainTurn($)
|
|
1726
|
+
await ui.press({ key: 'work-209' })
|
|
876
1727
|
await settle(r)
|
|
877
|
-
expect(await textOf(ui, '
|
|
878
|
-
|
|
879
|
-
|
|
1728
|
+
expect(await textOf(ui, 'queued-209')).toBe('queued')
|
|
1729
|
+
r.repoFails = true
|
|
1730
|
+
await completeTurn($)
|
|
1731
|
+
await settle(r)
|
|
1732
|
+
expect(r.runs).toEqual([])
|
|
1733
|
+
expect(r.toasts).toHaveLength(1)
|
|
1734
|
+
expect(r.toasts[0]).toContain('unreadable')
|
|
1735
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1736
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
880
1737
|
})
|
|
881
1738
|
|
|
882
|
-
test(`a
|
|
1739
|
+
test(`a cancel before the turn ends sends nothing, keeps the conversation and puts the work button back [${surface}]`, async ($, on) => {
|
|
883
1740
|
const r = rig(on, $)
|
|
884
1741
|
await $.session.start(START)
|
|
885
1742
|
const ui = await openAndMount($, r)
|
|
886
|
-
|
|
887
|
-
|
|
1743
|
+
await startMainTurn($)
|
|
1744
|
+
await ui.press({ key: 'work-209' })
|
|
1745
|
+
await settle(r)
|
|
1746
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeDefined()
|
|
888
1747
|
|
|
889
|
-
await
|
|
890
|
-
await ui.redraw()
|
|
1748
|
+
await ui.press({ key: 'cancel-209' })
|
|
891
1749
|
await settle(r)
|
|
892
|
-
expect(
|
|
893
|
-
expect(
|
|
894
|
-
expect(await
|
|
895
|
-
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
896
|
-
})
|
|
1750
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1751
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeUndefined()
|
|
1752
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
897
1753
|
|
|
898
|
-
|
|
899
|
-
const r = rig(on, $)
|
|
900
|
-
await $.session.start(START)
|
|
901
|
-
await mountBand($, FULLSCREEN)
|
|
1754
|
+
await completeTurn($)
|
|
902
1755
|
await settle(r)
|
|
903
|
-
expect(r.
|
|
1756
|
+
expect(r.runs).toEqual([])
|
|
1757
|
+
expect(r.session).toBe(SESSION)
|
|
1758
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
904
1759
|
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
await
|
|
1760
|
+
// The slot is free again: another row can be pressed and is held.
|
|
1761
|
+
await startMainTurn($, 'second')
|
|
1762
|
+
await ui.press({ key: 'work-210' })
|
|
1763
|
+
await settle(r)
|
|
1764
|
+
expect(await textOf(ui, 'queued-210')).toBe('queued')
|
|
1765
|
+
expect(r.runs).toEqual([])
|
|
908
1766
|
await completeTurn($)
|
|
909
|
-
await mountBand($, FULLSCREEN)
|
|
910
1767
|
await settle(r)
|
|
911
|
-
expect(r.
|
|
1768
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '210' }])
|
|
912
1769
|
})
|
|
913
1770
|
|
|
914
|
-
test(`a
|
|
1771
|
+
test(`a cancel of a held resume press sends nothing [${surface}]`, async ($, on) => {
|
|
915
1772
|
const r = rig(on, $)
|
|
1773
|
+
r.tickets = [...TICKETS, issue(230, 'Paused one', ['trailhead:build', 'trailhead:paused'], ['alice'])]
|
|
916
1774
|
await $.session.start(START)
|
|
917
1775
|
const ui = await openAndMount($, r)
|
|
918
|
-
await
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
await $.command.run({ command: 'clear', args: '' } as never)
|
|
922
|
-
await ui.redraw()
|
|
1776
|
+
await startMainTurn($)
|
|
1777
|
+
await ui.press({ key: 'resume-230' })
|
|
923
1778
|
await settle(r)
|
|
924
|
-
expect(await textOf(ui, '
|
|
925
|
-
|
|
926
|
-
|
|
1779
|
+
expect(await textOf(ui, 'queued-230')).toBe('queued')
|
|
1780
|
+
await ui.press({ key: 'cancel-230' })
|
|
1781
|
+
await completeTurn($)
|
|
1782
|
+
await settle(r)
|
|
1783
|
+
expect(r.runs).toEqual([])
|
|
1784
|
+
expect(await ui.find({ key: 'resume-230' })).toBeDefined()
|
|
927
1785
|
})
|
|
928
1786
|
|
|
929
|
-
test(`
|
|
1787
|
+
test(`a press with no turn running starts at once and reads starting, with no cancel button [${surface}]`, async ($, on) => {
|
|
930
1788
|
const r = rig(on, $)
|
|
1789
|
+
let release: () => void = () => undefined
|
|
1790
|
+
r.gate = new Promise<void>(resolve => (release = resolve))
|
|
931
1791
|
await $.session.start(START)
|
|
932
1792
|
const ui = await openAndMount($, r)
|
|
933
|
-
|
|
934
|
-
expect((await ui.find({ key: 'refresh' }))?.props).not.toHaveProperty('variant')
|
|
935
|
-
expect(fetches(r)).toBe(1)
|
|
936
|
-
|
|
937
|
-
// inside the floor: no fetch now, one when it ends
|
|
938
|
-
await r.clock.advance(5_000)
|
|
939
|
-
await ui.press({ key: 'refresh' })
|
|
940
|
-
await ui.press({ key: 'refresh' })
|
|
941
|
-
await settle(r)
|
|
942
|
-
expect(fetches(r)).toBe(1)
|
|
943
|
-
await r.clock.advance(FLOOR - 5_000)
|
|
1793
|
+
const first = ui.press({ key: 'work-209' })
|
|
944
1794
|
await settle(r)
|
|
945
|
-
expect(
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
await
|
|
949
|
-
await ui.
|
|
1795
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1796
|
+
expect(await textOf(ui, 'starting-209')).toBe('starting')
|
|
1797
|
+
expect((await ui.find({ key: 'starting-209' }))?.children[0]).toMatchObject({ props: { color: AMBER } })
|
|
1798
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1799
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeUndefined()
|
|
1800
|
+
release()
|
|
1801
|
+
await first
|
|
950
1802
|
await settle(r)
|
|
951
|
-
expect(
|
|
1803
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
1804
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
952
1805
|
})
|
|
953
1806
|
|
|
954
|
-
|
|
955
|
-
const mountBand = ($: any, viewport: Record<string, unknown> | undefined) =>
|
|
956
|
-
$.ui.mount({ plugin: 'trailhead', surface, component: 'AbovePrompt', props: BAND_PROPS, viewport })
|
|
957
|
-
|
|
958
|
-
test(`/trailhead:panel opens and closes the pane, answering without running a command [${surface}]`, async ($, on) => {
|
|
1807
|
+
test(`an interrupted turn releases a held press too [${surface}]`, async ($, on) => {
|
|
959
1808
|
const r = rig(on, $)
|
|
960
1809
|
await $.session.start(START)
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
965
|
-
await settle(r)
|
|
966
|
-
expect(fetches(r)).toBe(1)
|
|
967
|
-
|
|
968
|
-
const closed = await panel($)
|
|
969
|
-
expect(closed.text).toBe('Map panel closed.')
|
|
970
|
-
expect(r.panes).toEqual([])
|
|
971
|
-
await r.clock.advance(TIMER * 2)
|
|
972
|
-
await completeTurn($)
|
|
1810
|
+
const ui = await openAndMount($, r)
|
|
1811
|
+
await startMainTurn($)
|
|
1812
|
+
await ui.press({ key: 'work-209' })
|
|
973
1813
|
await settle(r)
|
|
974
|
-
expect(fetches(r)).toBe(1)
|
|
975
|
-
|
|
976
|
-
// the model never sees it; other commands still reach the engine
|
|
977
1814
|
expect(r.runs).toEqual([])
|
|
978
|
-
await
|
|
979
|
-
|
|
1815
|
+
await abortTurn($)
|
|
1816
|
+
await settle(r)
|
|
1817
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
980
1818
|
})
|
|
981
1819
|
|
|
982
|
-
test(`
|
|
1820
|
+
test(`a subagent's turn does not hold a press, nor does its end release one [${surface}]`, async ($, on) => {
|
|
983
1821
|
const r = rig(on, $)
|
|
984
1822
|
await $.session.start(START)
|
|
985
1823
|
const ui = await openAndMount($, r)
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1824
|
+
await $.turn.start({ text: 'sub', turnId: 'sub-1', agentId: 'agent-1' })
|
|
1825
|
+
await ui.press({ key: 'work-209' })
|
|
1826
|
+
await settle(r)
|
|
1827
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1828
|
+
|
|
1829
|
+
await startMainTurn($, 'main-1')
|
|
1830
|
+
await ui.press({ key: 'work-210' })
|
|
1831
|
+
await settle(r)
|
|
1832
|
+
expect(r.runs).toHaveLength(2)
|
|
1833
|
+
await $.turn.complete({ turnId: 'sub-1', answer: '', durationMs: 1, isAborted: false, reason: 'answer', agentId: 'agent-1' })
|
|
1834
|
+
await settle(r)
|
|
1835
|
+
expect(r.runs).toHaveLength(2)
|
|
1836
|
+
expect(await textOf(ui, 'queued-210')).toBe('queued')
|
|
1837
|
+
await completeTurn($)
|
|
1838
|
+
await settle(r)
|
|
1839
|
+
expect(r.runs).toHaveLength(4)
|
|
989
1840
|
})
|
|
990
1841
|
|
|
991
|
-
test(`a
|
|
1842
|
+
test(`a turn that starts while the dispatch is still reading re-holds the press, cancel offered again [${surface}]`, async ($, on) => {
|
|
992
1843
|
const r = rig(on, $)
|
|
993
1844
|
await $.session.start(START)
|
|
994
1845
|
const ui = await openAndMount($, r)
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
await ui.press({ key: 'close' })
|
|
999
|
-
await settle(r)
|
|
1000
|
-
expect(r.panes).toEqual([])
|
|
1001
|
-
await r.clock.advance(TIMER * 2)
|
|
1846
|
+
await startMainTurn($)
|
|
1847
|
+
await ui.press({ key: 'work-209' })
|
|
1002
1848
|
await completeTurn($)
|
|
1849
|
+
// The next session id read is the dispatch's own, before its clear.
|
|
1850
|
+
r.beforeSessionId = async () => void (await startMainTurn($, 'late'))
|
|
1003
1851
|
await settle(r)
|
|
1004
|
-
expect(
|
|
1852
|
+
expect(r.runs).toEqual([])
|
|
1853
|
+
await ui.redraw()
|
|
1854
|
+
expect(await textOf(ui, 'queued-209')).toBe('queued')
|
|
1855
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeDefined()
|
|
1856
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
1005
1857
|
|
|
1006
|
-
|
|
1007
|
-
await mountBand($, FULLSCREEN)
|
|
1858
|
+
await completeTurn($)
|
|
1008
1859
|
await settle(r)
|
|
1009
|
-
expect(r.
|
|
1010
|
-
expect(
|
|
1860
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1861
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1862
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1011
1863
|
})
|
|
1012
1864
|
|
|
1013
|
-
test(
|
|
1865
|
+
test(`a turn that starts between the end of one and the release keeps the press held until it ends [${surface}]`, async ($, on) => {
|
|
1014
1866
|
const r = rig(on, $)
|
|
1015
|
-
r.isPlaced = false
|
|
1016
1867
|
await $.session.start(START)
|
|
1017
|
-
|
|
1868
|
+
const ui = await openAndMount($, r)
|
|
1869
|
+
await startMainTurn($)
|
|
1870
|
+
await ui.press({ key: 'work-209' })
|
|
1871
|
+
await completeTurn($)
|
|
1872
|
+
// The release timer has not run yet: a queued prompt of the person's starts a turn.
|
|
1873
|
+
await startMainTurn($, 'next')
|
|
1018
1874
|
await settle(r)
|
|
1019
|
-
expect(
|
|
1020
|
-
|
|
1021
|
-
expect(
|
|
1875
|
+
expect(r.runs).toEqual([])
|
|
1876
|
+
expect(await textOf(ui, 'queued-209')).toBe('queued')
|
|
1877
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeDefined()
|
|
1878
|
+
|
|
1879
|
+
await completeTurn($)
|
|
1880
|
+
await settle(r)
|
|
1881
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
1022
1882
|
})
|
|
1023
1883
|
|
|
1024
|
-
for (const
|
|
1025
|
-
test(`
|
|
1884
|
+
for (const reason of ['prompt_input_exit', 'logout', 'other'] as const) {
|
|
1885
|
+
test(`a session that ends (${reason}) while a press is held drops it and resets the turn flag [${surface}]`, async ($, on) => {
|
|
1026
1886
|
const r = rig(on, $)
|
|
1027
|
-
r.work = withBand ? stateText() : null
|
|
1028
1887
|
await $.session.start(START)
|
|
1029
|
-
|
|
1030
|
-
|
|
1888
|
+
const ui = await openAndMount($, r)
|
|
1889
|
+
await startMainTurn($)
|
|
1890
|
+
await ui.press({ key: 'work-209' })
|
|
1031
1891
|
await settle(r)
|
|
1032
|
-
expect(
|
|
1033
|
-
expect(fetches(r)).toBe(1)
|
|
1034
|
-
// the band keeps its own drawing: it draws with a state, yields without one
|
|
1035
|
-
expect(await ui.find({ key: withBand ? 'ticket' : 'engine' })).toBeDefined()
|
|
1892
|
+
expect(await textOf(ui, 'queued-209')).toBe('queued')
|
|
1036
1893
|
|
|
1037
|
-
|
|
1038
|
-
r
|
|
1894
|
+
await $.session.end({ reason, sessionId: r.session, resume: {} })
|
|
1895
|
+
await settle(r)
|
|
1896
|
+
expect(r.runs).toEqual([])
|
|
1039
1897
|
await ui.redraw()
|
|
1040
|
-
await
|
|
1898
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1899
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1900
|
+
|
|
1901
|
+
// No turn is running in the next session: a press goes straight through.
|
|
1902
|
+
await ui.press({ key: 'work-210' })
|
|
1041
1903
|
await settle(r)
|
|
1042
|
-
expect(r.
|
|
1904
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '210' }])
|
|
1043
1905
|
})
|
|
1044
1906
|
}
|
|
1045
1907
|
|
|
1046
|
-
test(`
|
|
1908
|
+
test(`a cancel pressed between the release and the clear sends nothing and puts the work button back [${surface}]`, async ($, on) => {
|
|
1047
1909
|
const r = rig(on, $)
|
|
1048
1910
|
await $.session.start(START)
|
|
1049
|
-
await
|
|
1050
|
-
await
|
|
1051
|
-
await
|
|
1911
|
+
const ui = await openAndMount($, r)
|
|
1912
|
+
await startMainTurn($)
|
|
1913
|
+
await ui.press({ key: 'work-209' })
|
|
1052
1914
|
await settle(r)
|
|
1053
|
-
|
|
1915
|
+
await completeTurn($)
|
|
1916
|
+
// The row on screen still reads queued with its cancel button when the press lands, before the clear.
|
|
1917
|
+
let isPressed = false
|
|
1918
|
+
r.beforePhaseWrite = async () => {
|
|
1919
|
+
try {
|
|
1920
|
+
await ui.press({ key: 'cancel-209' })
|
|
1921
|
+
isPressed = true
|
|
1922
|
+
} catch (err) {
|
|
1923
|
+
r.logs.push(`seam press failed: ${String(err)}`)
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
await settle(r)
|
|
1927
|
+
expect(r.logs.filter(l => l.startsWith('seam'))).toEqual([])
|
|
1928
|
+
expect(isPressed).toBe(true)
|
|
1929
|
+
expect(r.runs).toEqual([])
|
|
1930
|
+
expect(r.session).toBe(SESSION)
|
|
1931
|
+
await ui.redraw()
|
|
1932
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
1933
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1934
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1935
|
+
// The slot is free again.
|
|
1936
|
+
await ui.press({ key: 'work-210' })
|
|
1937
|
+
await settle(r)
|
|
1938
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '210' }])
|
|
1939
|
+
})
|
|
1054
1940
|
|
|
1055
|
-
|
|
1941
|
+
// A cancel from a screen a frame behind: the row is drawn queued (the engine reports that phase) for the press.
|
|
1942
|
+
const staleCancel = (ui: any, r: Rig, number: number) => async () => {
|
|
1943
|
+
r.forced = { pendingPhase: 'queued' }
|
|
1944
|
+
await ui.redraw()
|
|
1945
|
+
await ui.press({ key: `cancel-${number}` })
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
test(`a cancel landing while a re-hold writes queued drops the press instead of holding it [${surface}]`, async ($, on) => {
|
|
1949
|
+
const r = rig(on, $)
|
|
1056
1950
|
await $.session.start(START)
|
|
1057
|
-
await
|
|
1951
|
+
const ui = await openAndMount($, r)
|
|
1952
|
+
await startMainTurn($)
|
|
1953
|
+
await ui.press({ key: 'work-209' })
|
|
1058
1954
|
await settle(r)
|
|
1059
|
-
|
|
1060
|
-
|
|
1955
|
+
await completeTurn($)
|
|
1956
|
+
// A turn starts during the dispatch's reads: it re-holds, and the cancel lands on that queued write.
|
|
1957
|
+
r.beforeSessionId = async () => {
|
|
1958
|
+
await startMainTurn($, 'late')
|
|
1959
|
+
r.beforePhaseWrite = staleCancel(ui, r, 209)
|
|
1960
|
+
}
|
|
1961
|
+
await settle(r)
|
|
1962
|
+
await completeTurn($)
|
|
1963
|
+
await settle(r)
|
|
1964
|
+
expect(r.runs).toEqual([])
|
|
1965
|
+
expect(r.session).toBe(SESSION)
|
|
1966
|
+
await ui.redraw()
|
|
1967
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1968
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
1969
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1061
1970
|
})
|
|
1062
1971
|
|
|
1063
|
-
test(`a
|
|
1972
|
+
test(`a cancel landing between the queued write and the hold drops the press [${surface}]`, async ($, on) => {
|
|
1064
1973
|
const r = rig(on, $)
|
|
1065
1974
|
await $.session.start(START)
|
|
1066
|
-
await
|
|
1067
|
-
await
|
|
1068
|
-
|
|
1975
|
+
const ui = await openAndMount($, r)
|
|
1976
|
+
await startMainTurn($)
|
|
1977
|
+
await ui.press({ key: 'work-209' })
|
|
1978
|
+
await settle(r)
|
|
1979
|
+
await ui.press({ key: 'cancel-209' })
|
|
1980
|
+
await settle(r)
|
|
1981
|
+
|
|
1982
|
+
r.beforePhaseWrite = staleCancel(ui, r, 209)
|
|
1983
|
+
await ui.press({ key: 'work-209' })
|
|
1984
|
+
await settle(r)
|
|
1985
|
+
await completeTurn($)
|
|
1986
|
+
await settle(r)
|
|
1987
|
+
expect(r.runs).toEqual([])
|
|
1988
|
+
await ui.redraw()
|
|
1989
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1990
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1991
|
+
})
|
|
1992
|
+
|
|
1993
|
+
test(`a press made right after a cancel keeps its queued mark and claim and runs when the turn ends [${surface}]`, async ($, on) => {
|
|
1994
|
+
const r = rig(on, $)
|
|
1995
|
+
await $.session.start(START)
|
|
1996
|
+
const ui = await openAndMount($, r)
|
|
1997
|
+
await startMainTurn($)
|
|
1998
|
+
await ui.press({ key: 'work-209' })
|
|
1999
|
+
await settle(r)
|
|
2000
|
+
// No settling in between: the cancel's own redraw must not let a stale-claim sweep erase the next press.
|
|
2001
|
+
await ui.press({ key: 'cancel-209' })
|
|
2002
|
+
await ui.press({ key: 'work-210' })
|
|
2003
|
+
await settle(r)
|
|
2004
|
+
await ui.redraw()
|
|
2005
|
+
await settle(r)
|
|
2006
|
+
expect(r.runs).toEqual([])
|
|
2007
|
+
expect(await textOf(ui, 'queued-210')).toBe('queued')
|
|
2008
|
+
expect(await ui.find({ key: 'cancel-210' })).toBeDefined()
|
|
2009
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
2010
|
+
|
|
2011
|
+
await completeTurn($)
|
|
2012
|
+
await settle(r)
|
|
2013
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '210' }])
|
|
2014
|
+
})
|
|
2015
|
+
|
|
2016
|
+
for (const phase of [null, 'starting'] as const) {
|
|
2017
|
+
test(`a pending claim with phase ${phase} and no press behind it is cleared when the pane draws [${surface}]`, async ($, on) => {
|
|
2018
|
+
const r = rig(on, $)
|
|
2019
|
+
await $.session.start(START)
|
|
2020
|
+
const ui = await openAndMount($, r)
|
|
2021
|
+
// What a hot reload leaves: the atoms say pending, the module state is empty.
|
|
2022
|
+
r.forced = { pendingWork: 209, pendingPhase: phase }
|
|
2023
|
+
await ui.redraw()
|
|
2024
|
+
await settle(r)
|
|
2025
|
+
await ui.redraw()
|
|
2026
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
2027
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
2028
|
+
await ui.press({ key: 'work-210' })
|
|
2029
|
+
await settle(r)
|
|
2030
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '210' }])
|
|
2031
|
+
})
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
test(`a press with no turn running never shows queued or a cancel button [${surface}]`, async ($, on) => {
|
|
2035
|
+
const r = rig(on, $)
|
|
2036
|
+
let release: () => void = () => undefined
|
|
2037
|
+
r.clearGate = new Promise<void>(resolve => (release = resolve))
|
|
2038
|
+
await $.session.start(START)
|
|
2039
|
+
const ui = await openAndMount($, r)
|
|
2040
|
+
const first = ui.press({ key: 'work-209' })
|
|
2041
|
+
await settle(r)
|
|
2042
|
+
// The phase went straight to starting: queued was never written, so no cancel was ever offered.
|
|
2043
|
+
expect(r.phases[0]).toBe('starting')
|
|
2044
|
+
expect(r.phases).not.toContain('queued')
|
|
2045
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeUndefined()
|
|
2046
|
+
release()
|
|
2047
|
+
await first
|
|
2048
|
+
await settle(r)
|
|
2049
|
+
})
|
|
2050
|
+
|
|
2051
|
+
for (const reason of ['clear', 'resume'] as const) {
|
|
2052
|
+
test(`a session that ends (${reason}) while a press is held drops it [${surface}]`, async ($, on) => {
|
|
2053
|
+
const r = rig(on, $)
|
|
2054
|
+
await $.session.start(START)
|
|
2055
|
+
const ui = await openAndMount($, r)
|
|
2056
|
+
await startMainTurn($)
|
|
2057
|
+
await ui.press({ key: 'work-209' })
|
|
2058
|
+
await settle(r)
|
|
2059
|
+
expect(await textOf(ui, 'queued-209')).toBe('queued')
|
|
2060
|
+
|
|
2061
|
+
await $.session.end({ reason, sessionId: r.session, resume: {} })
|
|
2062
|
+
await settle(r)
|
|
2063
|
+
expect(r.runs).toEqual([])
|
|
2064
|
+
await ui.redraw()
|
|
2065
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
2066
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
2067
|
+
// The turn flag was reset too: a press goes straight through.
|
|
2068
|
+
await ui.press({ key: 'work-210' })
|
|
2069
|
+
await settle(r)
|
|
2070
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '210' }])
|
|
2071
|
+
})
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
test(`a cancel on a stuck queued row (nothing held, nothing sending) clears it [${surface}]`, async ($, on) => {
|
|
2075
|
+
const r = rig(on, $)
|
|
2076
|
+
await $.session.start(START)
|
|
2077
|
+
const ui = await openAndMount($, r)
|
|
2078
|
+
// What a hot reload leaves: the atoms say queued, the module state is empty.
|
|
2079
|
+
r.forced = { pendingWork: 209, pendingPhase: 'queued' }
|
|
2080
|
+
await ui.redraw()
|
|
2081
|
+
expect(await ui.find({ key: 'cancel-209' })).toBeDefined()
|
|
2082
|
+
await ui.press({ key: 'cancel-209' })
|
|
2083
|
+
await settle(r)
|
|
2084
|
+
await ui.redraw()
|
|
2085
|
+
expect(r.runs).toEqual([])
|
|
2086
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
2087
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
2088
|
+
})
|
|
2089
|
+
|
|
2090
|
+
test(`the mod's own clear leaves the row starting across the reset until the verb settles [${surface}]`, async ($, on) => {
|
|
2091
|
+
const r = rig(on, $)
|
|
2092
|
+
let release: () => void = () => undefined
|
|
2093
|
+
r.gate = new Promise<void>(resolve => (release = resolve))
|
|
2094
|
+
await $.session.start(START)
|
|
2095
|
+
const ui = await openAndMount($, r)
|
|
2096
|
+
let during: string | undefined
|
|
2097
|
+
r.duringClear = async () => {
|
|
2098
|
+
await ui.redraw()
|
|
2099
|
+
during = await textOf(ui, 'starting-209')
|
|
2100
|
+
}
|
|
2101
|
+
const first = ui.press({ key: 'work-209' })
|
|
2102
|
+
await settle(r)
|
|
2103
|
+
expect(during).toBe('starting')
|
|
2104
|
+
await ui.redraw()
|
|
2105
|
+
expect(await textOf(ui, 'starting-209')).toBe('starting')
|
|
2106
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '209' }])
|
|
2107
|
+
release()
|
|
2108
|
+
await first
|
|
2109
|
+
await settle(r)
|
|
2110
|
+
expect(await ui.find({ key: 'starting-209' })).toBeUndefined()
|
|
2111
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
2112
|
+
})
|
|
2113
|
+
|
|
2114
|
+
test(`a clear that is refused runs no work command and clears the queued mark [${surface}]`, async ($, on) => {
|
|
2115
|
+
const r = rig(on, $)
|
|
2116
|
+
r.refuseClear = 'cannot clear now'
|
|
2117
|
+
await $.session.start(START)
|
|
2118
|
+
const ui = await openAndMount($, r)
|
|
2119
|
+
await ui.press({ key: 'work-209' })
|
|
2120
|
+
await settle(r)
|
|
2121
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }])
|
|
2122
|
+
expect(r.session).toBe(SESSION)
|
|
2123
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
2124
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
2125
|
+
})
|
|
2126
|
+
|
|
2127
|
+
test(`a clear that resolves without switching the session runs no work command [${surface}]`, async ($, on) => {
|
|
2128
|
+
const r = rig(on, $)
|
|
2129
|
+
r.denyClear = true
|
|
2130
|
+
await $.session.start(START)
|
|
2131
|
+
const ui = await openAndMount($, r)
|
|
2132
|
+
await ui.press({ key: 'work-209' })
|
|
2133
|
+
await settle(r)
|
|
2134
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }])
|
|
2135
|
+
expect(r.session).toBe(SESSION)
|
|
2136
|
+
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
2137
|
+
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
2138
|
+
})
|
|
2139
|
+
|
|
2140
|
+
test(`an active map that moved during /clear drops the carried view [${surface}]`, async ($, on) => {
|
|
2141
|
+
const r = rig(on, $)
|
|
2142
|
+
await $.session.start(START)
|
|
2143
|
+
const ui = await openAndMount($, r)
|
|
2144
|
+
await pressMap(ui, r, 14)
|
|
2145
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
2146
|
+
|
|
2147
|
+
r.duringClear = () => (r.active = '14\n')
|
|
2148
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
2149
|
+
await ui.redraw()
|
|
2150
|
+
await settle(r)
|
|
2151
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2152
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
2153
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5 · active')
|
|
2154
|
+
})
|
|
2155
|
+
|
|
2156
|
+
test(`a manual /clear keeps the pane filled, with no turn and inside the floor [${surface}]`, async ($, on) => {
|
|
2157
|
+
const r = rig(on, $)
|
|
2158
|
+
await $.session.start(START)
|
|
2159
|
+
const ui = await openAndMount($, r)
|
|
2160
|
+
expect(fetches(r)).toBe(1)
|
|
2161
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
2162
|
+
|
|
2163
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
2164
|
+
await ui.redraw()
|
|
2165
|
+
await settle(r)
|
|
2166
|
+
expect(r.session).toBe(`${NEXT_SESSION}-1`)
|
|
2167
|
+
expect(fetches(r)).toBe(2)
|
|
2168
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
2169
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
2170
|
+
})
|
|
2171
|
+
|
|
2172
|
+
test(`a pane the person closed is not opened again by itself after /clear [${surface}]`, async ($, on) => {
|
|
2173
|
+
const r = rig(on, $)
|
|
2174
|
+
await $.session.start(START)
|
|
2175
|
+
await mountBand($, FULLSCREEN)
|
|
2176
|
+
await settle(r)
|
|
2177
|
+
expect(r.opens).toHaveLength(1)
|
|
2178
|
+
|
|
2179
|
+
// closed by the person, then cleared
|
|
2180
|
+
r.panes = []
|
|
2181
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
2182
|
+
await completeTurn($)
|
|
1069
2183
|
await mountBand($, FULLSCREEN)
|
|
1070
2184
|
await settle(r)
|
|
1071
|
-
expect(r.opens).toHaveLength(1)
|
|
2185
|
+
expect(r.opens).toHaveLength(1)
|
|
2186
|
+
})
|
|
2187
|
+
|
|
2188
|
+
test(`a viewed map stays viewed through /clear [${surface}]`, async ($, on) => {
|
|
2189
|
+
const r = rig(on, $)
|
|
2190
|
+
await $.session.start(START)
|
|
2191
|
+
const ui = await openAndMount($, r)
|
|
2192
|
+
await pressMap(ui, r, 14)
|
|
2193
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
2194
|
+
|
|
2195
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
2196
|
+
await ui.redraw()
|
|
2197
|
+
await settle(r)
|
|
2198
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2199
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
2200
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
2201
|
+
})
|
|
2202
|
+
|
|
2203
|
+
test(`the refresh button obeys the same floor [${surface}]`, async ($, on) => {
|
|
2204
|
+
const r = rig(on, $)
|
|
2205
|
+
await $.session.start(START)
|
|
2206
|
+
const ui = await openAndMount($, r)
|
|
2207
|
+
expect((await ui.find({ key: 'refresh' }))?.props).toMatchObject({ label: 'refresh' })
|
|
2208
|
+
expect((await ui.find({ key: 'refresh' }))?.props).not.toHaveProperty('variant')
|
|
2209
|
+
expect(fetches(r)).toBe(1)
|
|
2210
|
+
|
|
2211
|
+
// inside the floor: no fetch now, one when it ends
|
|
2212
|
+
await r.clock.advance(5_000)
|
|
2213
|
+
await ui.press({ key: 'refresh' })
|
|
2214
|
+
await ui.press({ key: 'refresh' })
|
|
2215
|
+
await settle(r)
|
|
2216
|
+
expect(fetches(r)).toBe(1)
|
|
2217
|
+
await r.clock.advance(FLOOR - 5_000)
|
|
2218
|
+
await settle(r)
|
|
2219
|
+
expect(fetches(r)).toBe(2)
|
|
2220
|
+
|
|
2221
|
+
// outside the floor: at once
|
|
2222
|
+
await r.clock.advance(FLOOR)
|
|
2223
|
+
await ui.press({ key: 'refresh' })
|
|
2224
|
+
await settle(r)
|
|
2225
|
+
expect(fetches(r)).toBe(3)
|
|
2226
|
+
})
|
|
2227
|
+
|
|
2228
|
+
const panel = ($: any) => $.command.run({ command: 'trailhead:panel' })
|
|
2229
|
+
const mountBand = ($: any, viewport: Record<string, unknown> | undefined) =>
|
|
2230
|
+
$.ui.mount({ plugin: 'trailhead', surface, component: 'AbovePrompt', props: BAND_PROPS, viewport })
|
|
2231
|
+
|
|
2232
|
+
test(`/trailhead:panel opens and closes the pane, answering without running a command [${surface}]`, async ($, on) => {
|
|
2233
|
+
const r = rig(on, $)
|
|
2234
|
+
await $.session.start(START)
|
|
2235
|
+
const opened = await panel($)
|
|
2236
|
+
expect(opened.text).toBe('Map panel opened.')
|
|
2237
|
+
expect(r.opens).toEqual([{ id: PANE, title: 'Map' }])
|
|
2238
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2239
|
+
await settle(r)
|
|
2240
|
+
expect(fetches(r)).toBe(1)
|
|
2241
|
+
|
|
2242
|
+
const closed = await panel($)
|
|
2243
|
+
expect(closed.text).toBe('Map panel closed.')
|
|
2244
|
+
expect(r.panes).toEqual([])
|
|
2245
|
+
await r.clock.advance(TIMER * 2)
|
|
2246
|
+
await completeTurn($)
|
|
2247
|
+
await settle(r)
|
|
2248
|
+
expect(fetches(r)).toBe(1)
|
|
2249
|
+
|
|
2250
|
+
// the model never sees it; other commands still reach the engine
|
|
2251
|
+
expect(r.runs).toEqual([])
|
|
2252
|
+
await $.command.run({ command: 'trailhead:work', args: '5' } as never)
|
|
2253
|
+
expect(r.runs).toEqual([{ command: 'trailhead:work', args: '5' }])
|
|
2254
|
+
})
|
|
2255
|
+
|
|
2256
|
+
test(`the footer row spreads refresh to the left and close to the right [${surface}]`, async ($, on) => {
|
|
2257
|
+
const r = rig(on, $)
|
|
2258
|
+
await $.session.start(START)
|
|
2259
|
+
const ui = await openAndMount($, r)
|
|
2260
|
+
expect((await ui.find({ key: 'footer-row' }))?.props).toMatchObject({ width: 60, justifyContent: 'space-between' })
|
|
2261
|
+
expect((await ui.findAll({ type: 'Button' })).map((b: any) => b.key).slice(-2)).toEqual(['refresh', 'close'])
|
|
2262
|
+
expect((await ui.find({ key: 'close' }))?.props).toMatchObject({ label: 'close' })
|
|
2263
|
+
})
|
|
2264
|
+
|
|
2265
|
+
test(`a press on close removes the pane for good [${surface}]`, async ($, on) => {
|
|
2266
|
+
const r = rig(on, $)
|
|
2267
|
+
await $.session.start(START)
|
|
2268
|
+
const ui = await openAndMount($, r)
|
|
2269
|
+
expect(fetches(r)).toBe(1)
|
|
2270
|
+
const opened = r.opens.length
|
|
2271
|
+
|
|
2272
|
+
await ui.press({ key: 'close' })
|
|
2273
|
+
await settle(r)
|
|
2274
|
+
expect(r.panes).toEqual([])
|
|
2275
|
+
await r.clock.advance(TIMER * 2)
|
|
2276
|
+
await completeTurn($)
|
|
2277
|
+
await settle(r)
|
|
2278
|
+
expect(fetches(r)).toBe(1)
|
|
2279
|
+
|
|
2280
|
+
// the press counts as the person closing it: fullscreen never opens it again
|
|
2281
|
+
await mountBand($, FULLSCREEN)
|
|
2282
|
+
await settle(r)
|
|
2283
|
+
expect(r.opens).toHaveLength(opened)
|
|
2284
|
+
expect(r.logs).toEqual([])
|
|
2285
|
+
})
|
|
2286
|
+
|
|
2287
|
+
test(`/trailhead:panel says why a pane cannot be placed [${surface}]`, async ($, on) => {
|
|
2288
|
+
const r = rig(on, $)
|
|
2289
|
+
r.isPlaced = false
|
|
2290
|
+
await $.session.start(START)
|
|
2291
|
+
expect((await panel($)).text).toBe('Map panel not shown: terminal too narrow')
|
|
2292
|
+
await settle(r)
|
|
2293
|
+
expect(fetches(r)).toBe(0)
|
|
2294
|
+
// a pane waiting undrawn can still be closed
|
|
2295
|
+
expect((await panel($)).text).toBe('Map panel closed.')
|
|
2296
|
+
})
|
|
2297
|
+
|
|
2298
|
+
test(`/trailhead:panel shows a pane the auto-open left undrawn instead of closing it [${surface}]`, async ($, on) => {
|
|
2299
|
+
const r = rig(on, $)
|
|
2300
|
+
// sessione vuota, terminale sotto la soglia dei pannelli non richiesti: l'auto-open resta non disegnato
|
|
2301
|
+
r.isPlaced = false
|
|
2302
|
+
await $.session.start(START)
|
|
2303
|
+
await mountBand($, FULLSCREEN)
|
|
2304
|
+
await settle(r)
|
|
2305
|
+
expect(r.panes.map(p => [p.id, p.isPlaced])).toEqual([[PANE, false]])
|
|
2306
|
+
|
|
2307
|
+
// la richiesta della persona lo disegna a qualsiasi larghezza
|
|
2308
|
+
r.isPlaced = true
|
|
2309
|
+
expect((await panel($)).text).toBe('Map panel opened.')
|
|
2310
|
+
expect(r.panes.map(p => [p.id, p.isPlaced])).toEqual([[PANE, true]])
|
|
2311
|
+
await settle(r)
|
|
2312
|
+
expect(fetches(r)).toBe(1)
|
|
2313
|
+
|
|
2314
|
+
// disegnato, il comando torna a chiuderlo
|
|
2315
|
+
expect((await panel($)).text).toBe('Map panel closed.')
|
|
2316
|
+
expect(r.panes).toEqual([])
|
|
2317
|
+
})
|
|
2318
|
+
|
|
2319
|
+
for (const withBand of [false, true]) {
|
|
2320
|
+
test(`the pane opens by itself once in fullscreen, the band ${withBand ? 'drawing' : 'yielding'} [${surface}]`, async ($, on) => {
|
|
2321
|
+
const r = rig(on, $)
|
|
2322
|
+
r.work = withBand ? stateText() : null
|
|
2323
|
+
await $.session.start(START)
|
|
2324
|
+
expect(r.opens).toEqual([])
|
|
2325
|
+
const ui = await mountBand($, FULLSCREEN)
|
|
2326
|
+
await settle(r)
|
|
2327
|
+
expect([r.logs, r.opens]).toEqual([[], [{ id: PANE, title: 'Map' }]])
|
|
2328
|
+
expect(fetches(r)).toBe(1)
|
|
2329
|
+
// the band keeps its own drawing: it draws with a state, yields without one
|
|
2330
|
+
expect(await ui.find({ key: withBand ? 'ticket' : 'engine' })).toBeDefined()
|
|
2331
|
+
|
|
2332
|
+
// closed by the person: never again
|
|
2333
|
+
r.panes = []
|
|
2334
|
+
await ui.redraw()
|
|
2335
|
+
await mountBand($, FULLSCREEN)
|
|
2336
|
+
await settle(r)
|
|
2337
|
+
expect(r.opens).toHaveLength(1)
|
|
2338
|
+
})
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
test(`the pane never opens by itself on the main screen, unknown layout or without a map [${surface}]`, async ($, on) => {
|
|
2342
|
+
const r = rig(on, $)
|
|
2343
|
+
await $.session.start(START)
|
|
2344
|
+
await mountBand($, { columns: 200, rows: 50, isFullscreen: false })
|
|
2345
|
+
await mountBand($, { columns: 200, rows: 50 })
|
|
2346
|
+
await mountBand($, undefined)
|
|
2347
|
+
await settle(r)
|
|
2348
|
+
expect(r.opens).toEqual([])
|
|
2349
|
+
|
|
2350
|
+
r.active = null
|
|
2351
|
+
await $.session.start(START)
|
|
2352
|
+
await mountBand($, FULLSCREEN)
|
|
2353
|
+
await settle(r)
|
|
2354
|
+
expect(r.opens).toEqual([])
|
|
2355
|
+
expect(r.calls).toEqual([])
|
|
2356
|
+
})
|
|
2357
|
+
|
|
2358
|
+
test(`a pane the person closed with the command is not opened again by itself [${surface}]`, async ($, on) => {
|
|
2359
|
+
const r = rig(on, $)
|
|
2360
|
+
await $.session.start(START)
|
|
2361
|
+
await panel($)
|
|
2362
|
+
await panel($)
|
|
2363
|
+
expect(r.panes).toEqual([])
|
|
2364
|
+
await mountBand($, FULLSCREEN)
|
|
2365
|
+
await settle(r)
|
|
2366
|
+
expect(r.opens).toHaveLength(1)
|
|
2367
|
+
})
|
|
2368
|
+
|
|
2369
|
+
test(`a list that reached the gh limit says so [${surface}]`, async ($, on) => {
|
|
2370
|
+
const r = rig(on, $)
|
|
2371
|
+
r.tickets = Array.from({ length: 200 }, (_, i) => issue(i + 1, `t${i + 1}`, ['trailhead:build']))
|
|
2372
|
+
await $.session.start(START)
|
|
2373
|
+
const ui = await openAndMount($, r)
|
|
2374
|
+
expect(await textOf(ui, 'capped')).toBe('list capped at 200')
|
|
2375
|
+
expect((await ui.find({ key: 'capped' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
2376
|
+
r.tickets = TICKETS
|
|
2377
|
+
await r.clock.advance(FLOOR)
|
|
2378
|
+
await completeTurn($)
|
|
2379
|
+
await settle(r)
|
|
2380
|
+
expect(await ui.find({ key: 'capped' })).toBeUndefined()
|
|
2381
|
+
})
|
|
2382
|
+
|
|
2383
|
+
test(`a pane found already up at auto-open ends the auto-open for good [${surface}]`, async ($, on) => {
|
|
2384
|
+
const r = rig(on, $)
|
|
2385
|
+
await $.session.start(START)
|
|
2386
|
+
r.open()
|
|
2387
|
+
const ui = await mountBand($, FULLSCREEN)
|
|
2388
|
+
await settle(r)
|
|
2389
|
+
expect(r.opens).toEqual([])
|
|
2390
|
+
r.panes = []
|
|
2391
|
+
await ui.redraw()
|
|
2392
|
+
await mountBand($, FULLSCREEN)
|
|
2393
|
+
await settle(r)
|
|
2394
|
+
expect(r.opens).toEqual([])
|
|
2395
|
+
})
|
|
2396
|
+
|
|
2397
|
+
test(`a pane opened by hand after a map switch fills within the floor, not at the slow timer [${surface}]`, async ($, on) => {
|
|
2398
|
+
const r = rig(on, $)
|
|
2399
|
+
await $.session.start(START)
|
|
2400
|
+
r.open()
|
|
2401
|
+
const first = await mountPane($)
|
|
2402
|
+
await settle(r)
|
|
2403
|
+
expect(await textOf(first, 'map')).toBe('#198 trailhead mod')
|
|
2404
|
+
r.panes = []
|
|
2405
|
+
await first.unmount()
|
|
2406
|
+
|
|
2407
|
+
// the map moves on while the pane is closed
|
|
2408
|
+
r.active = '199\n'
|
|
2409
|
+
r.map = { ...MAP, title: 'second map', url: `${URL}/199` }
|
|
2410
|
+
r.tickets = [issue(300, 'only one', ['trailhead:build'])]
|
|
2411
|
+
await r.clock.advance(3_000)
|
|
2412
|
+
await completeTurn($)
|
|
2413
|
+
await settle(r)
|
|
2414
|
+
expect(fetches(r)).toBe(1)
|
|
2415
|
+
|
|
2416
|
+
r.open()
|
|
2417
|
+
const ui = await mountPane($)
|
|
2418
|
+
await settle(r)
|
|
2419
|
+
expect(await textOf(ui, 'map')).toBe('map #199')
|
|
2420
|
+
await r.clock.advance(FLOOR - 3_000)
|
|
2421
|
+
await settle(r)
|
|
2422
|
+
expect(await textOf(ui, 'map')).toBe('#199 second map')
|
|
2423
|
+
expect(fetches(r)).toBe(2)
|
|
2424
|
+
})
|
|
2425
|
+
|
|
2426
|
+
// ---- viewing another open map (#229) ----
|
|
2427
|
+
const pressMap = async (ui: any, r: Rig, n: number) => {
|
|
2428
|
+
await ui.press({ key: `map-${n}` })
|
|
2429
|
+
await settle(r)
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
test(`two open maps are listed, the active one marked [${surface}]`, async ($, on) => {
|
|
2433
|
+
const r = rig(on, $)
|
|
2434
|
+
await $.session.start(START)
|
|
2435
|
+
const ui = await openAndMount($, r)
|
|
2436
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2437
|
+
expect(await keysOf(ui, 'map-row-')).toEqual(['map-row-14', 'map-row-198'])
|
|
2438
|
+
expect(await textOf(ui, 'map-mark-198')).toBe('● ')
|
|
2439
|
+
expect((await ui.find({ key: 'map-mark-198' }))?.children[0]).toMatchObject({ props: { color: ACCENT } })
|
|
2440
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
2441
|
+
expect(await textOf(ui, 'map-label-198')).toBe('#198 trailhead mod')
|
|
2442
|
+
expect((await ui.find({ key: 'map-label-198' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
2443
|
+
expect(await textOf(ui, 'map-mark-14')).toBe(' ')
|
|
2444
|
+
expect((await ui.find({ key: 'map-14' }))?.props).toMatchObject({ label: '#14 older map' })
|
|
2445
|
+
expect(await ui.find({ key: 'map-198' })).toBeUndefined()
|
|
2446
|
+
expect(await ui.find({ key: 'viewing' })).toBeUndefined()
|
|
2447
|
+
// under the header, before the tickets
|
|
2448
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
2449
|
+
expect(keys.indexOf('group-maps')).toBeGreaterThan(keys.indexOf('header'))
|
|
2450
|
+
expect(keys.indexOf('group-maps')).toBeLessThan(keys.indexOf('group-frontier'))
|
|
2451
|
+
})
|
|
2452
|
+
|
|
2453
|
+
test(`one open map draws no maps group [${surface}]`, async ($, on) => {
|
|
2454
|
+
const r = rig(on, $)
|
|
2455
|
+
r.maps = [MAPS[0]]
|
|
2456
|
+
await $.session.start(START)
|
|
2457
|
+
const ui = await openAndMount($, r)
|
|
2458
|
+
expect(await hasKey(ui, 'head-maps')).toBe(false)
|
|
2459
|
+
expect(await hasKey(ui, 'group-maps')).toBe(false)
|
|
2460
|
+
expect((await ui.findAll({ type: 'Button' })).map((b: any) => b.key)).toEqual(['open-whiteboard', 'work-209', 'work-210', 'work-214', 'refresh', 'close'])
|
|
2461
|
+
})
|
|
2462
|
+
|
|
2463
|
+
test(`a press on another map shows its tickets, the active-map marker is never written [${surface}]`, async ($, on) => {
|
|
2464
|
+
const r = rig(on, $)
|
|
2465
|
+
r.work = stateText()
|
|
2466
|
+
await $.session.start(START)
|
|
2467
|
+
const ui = await openAndMount($, r)
|
|
2468
|
+
await pressMap(ui, r, 14)
|
|
2469
|
+
|
|
2470
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2471
|
+
expect(await textOf(ui, 'destination')).toBe('An older destination.')
|
|
2472
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
2473
|
+
expect(await textOf(ui, 'holder-141')).toBe('@carol')
|
|
2474
|
+
expect(await textOf(ui, 'burndown-counts')).toBe('open 2 · done 0 · removed 0 · peak 2')
|
|
2475
|
+
// the ticket in play belongs to the other map: nothing here is marked
|
|
2476
|
+
expect(await hasKey(ui, 'mark-208')).toBe(false)
|
|
2477
|
+
expect(r.calls.slice(-6).map(argv => argv.slice(0, 4))).toEqual([
|
|
2478
|
+
['gh', 'issue', 'view', '14'],
|
|
2479
|
+
['gh', 'api', 'graphql', '-F'],
|
|
2480
|
+
['gh', 'issue', 'list', '--label'],
|
|
2481
|
+
['gh', 'api', 'graphql', '-F'],
|
|
2482
|
+
['gh', 'api', 'graphql', '-F'],
|
|
2483
|
+
['gh', 'api', 'graphql', '-F'],
|
|
2484
|
+
])
|
|
2485
|
+
expect(r.calls.some(argv => argv.includes('trailhead:map-14'))).toBe(true)
|
|
2486
|
+
|
|
2487
|
+
// the viewed map is bold, the active one a button; one line says where we are
|
|
2488
|
+
expect(await textOf(ui, 'map-label-14')).toBe('#14 older map')
|
|
2489
|
+
expect((await ui.find({ key: 'map-198' }))?.props).toMatchObject({ label: '#198 trailhead mod' })
|
|
2490
|
+
expect(await hasKey(ui, 'map-14')).toBe(false)
|
|
2491
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
2492
|
+
expect((await ui.find({ key: 'viewing-text' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
2493
|
+
expect((await ui.find({ key: 'back' }))?.props).toMatchObject({ label: 'back to active' })
|
|
2494
|
+
// the work verb owns the marker: the pane never wrote it
|
|
2495
|
+
expect(r.writes).toEqual([])
|
|
2496
|
+
expect(r.active).toBe('198\n')
|
|
2497
|
+
})
|
|
2498
|
+
|
|
2499
|
+
test(`back to active and a press on the active map return to it [${surface}]`, async ($, on) => {
|
|
2500
|
+
const r = rig(on, $)
|
|
2501
|
+
await $.session.start(START)
|
|
2502
|
+
const ui = await openAndMount($, r)
|
|
2503
|
+
await pressMap(ui, r, 14)
|
|
2504
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2505
|
+
|
|
2506
|
+
await ui.press({ key: 'back' })
|
|
2507
|
+
await settle(r)
|
|
2508
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
2509
|
+
expect(await keysOf(ui, 'row-')).toContain('row-208')
|
|
2510
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
2511
|
+
expect(await textOf(ui, 'map-label-198')).toBe('#198 trailhead mod')
|
|
2512
|
+
|
|
2513
|
+
await pressMap(ui, r, 14)
|
|
2514
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2515
|
+
await pressMap(ui, r, 198)
|
|
2516
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
2517
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
2518
|
+
expect(r.writes).toEqual([])
|
|
2519
|
+
})
|
|
2520
|
+
|
|
2521
|
+
test(`a change of the active map resets the view [${surface}]`, async ($, on) => {
|
|
2522
|
+
const r = rig(on, $)
|
|
2523
|
+
await $.session.start(START)
|
|
2524
|
+
const ui = await openAndMount($, r)
|
|
2525
|
+
await pressMap(ui, r, 14)
|
|
2526
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2527
|
+
|
|
2528
|
+
// the work verb switched the active map to the viewed one at the end of a turn
|
|
2529
|
+
r.active = '14\n'
|
|
2530
|
+
await r.clock.advance(FLOOR)
|
|
2531
|
+
await completeTurn($)
|
|
2532
|
+
await settle(r)
|
|
2533
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2534
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
2535
|
+
expect(await textOf(ui, 'map-label-14')).toBe('#14 older map')
|
|
2536
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5 · active')
|
|
2537
|
+
expect((await ui.find({ key: 'map-198' }))?.props).toMatchObject({ label: '#198 trailhead mod' })
|
|
2538
|
+
})
|
|
2539
|
+
|
|
2540
|
+
test(`a work press on a viewed map runs the work verb, then the view follows the new active map [${surface}]`, async ($, on) => {
|
|
2541
|
+
const r = rig(on, $)
|
|
2542
|
+
await $.session.start(START)
|
|
2543
|
+
const ui = await openAndMount($, r)
|
|
2544
|
+
await pressMap(ui, r, 14)
|
|
2545
|
+
await ui.press({ key: 'work-140' })
|
|
2546
|
+
await settle(r)
|
|
2547
|
+
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '140' }])
|
|
2548
|
+
expect(r.writes).toEqual([])
|
|
2549
|
+
// still viewing until the verb has changed the marker
|
|
2550
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
2551
|
+
|
|
2552
|
+
r.active = '14\n'
|
|
2553
|
+
await r.clock.advance(FLOOR)
|
|
2554
|
+
await completeTurn($)
|
|
2555
|
+
await settle(r)
|
|
2556
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2557
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
2558
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5 · active')
|
|
2559
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23')
|
|
2560
|
+
})
|
|
2561
|
+
|
|
2562
|
+
test(`both maps reads failing keep the last list and set no footer error [${surface}]`, async ($, on) => {
|
|
2563
|
+
const r = rig(on, $)
|
|
2564
|
+
await $.session.start(START)
|
|
2565
|
+
const ui = await openAndMount($, r)
|
|
2566
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2567
|
+
|
|
2568
|
+
const base: Gh = argv => withHistory(r, ok(JSON.stringify(r.history)))(argv)
|
|
2569
|
+
r.gh = argv => (isGraphql(argv) || isMapsList(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
2570
|
+
r.tickets = [issue(300, 'fresh one', ['trailhead:build'])]
|
|
2571
|
+
await r.clock.advance(FLOOR)
|
|
2572
|
+
await completeTurn($)
|
|
2573
|
+
await settle(r)
|
|
2574
|
+
expect(await textOf(ui, 'ticket-300')).toBe('#300 fresh one')
|
|
2575
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2576
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2577
|
+
|
|
2578
|
+
// unreadable output is the same
|
|
2579
|
+
r.gh = argv => (isGraphql(argv) || isMapsList(argv) ? ok('not json') : base(argv))
|
|
2580
|
+
await r.clock.advance(FLOOR)
|
|
2581
|
+
await completeTurn($)
|
|
2582
|
+
await settle(r)
|
|
2583
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2584
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2585
|
+
})
|
|
2586
|
+
|
|
2587
|
+
test(`a viewed map that closed falls back to the active map [${surface}]`, async ($, on) => {
|
|
2588
|
+
const r = rig(on, $)
|
|
2589
|
+
await $.session.start(START)
|
|
2590
|
+
const ui = await openAndMount($, r)
|
|
2591
|
+
await pressMap(ui, r, 14)
|
|
2592
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2593
|
+
|
|
2594
|
+
r.maps = [MAPS[0]]
|
|
2595
|
+
await r.clock.advance(FLOOR)
|
|
2596
|
+
await completeTurn($)
|
|
2597
|
+
await settle(r)
|
|
2598
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
2599
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
2600
|
+
expect(await hasKey(ui, 'head-maps')).toBe(false)
|
|
2601
|
+
})
|
|
2602
|
+
|
|
2603
|
+
test(`a refresh in flight when the view changes never commits the old map's tickets [${surface}]`, async ($, on) => {
|
|
2604
|
+
const r = rig(on, $)
|
|
2605
|
+
// every value the pane stores, to look at what the old answer wrote
|
|
2606
|
+
const paneWrites: unknown[] = []
|
|
2607
|
+
on('state.set', { plugin: 'trailhead', key: 'mapPane' }, ($$, e, next) => {
|
|
2608
|
+
paneWrites.push(e.value)
|
|
2609
|
+
return next(e)
|
|
2610
|
+
})
|
|
2611
|
+
await $.session.start(START)
|
|
2612
|
+
const ui = await openAndMount($, r)
|
|
2613
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
2614
|
+
|
|
2615
|
+
let release: () => void = () => undefined
|
|
2616
|
+
let releaseOther: () => void = () => undefined
|
|
2617
|
+
const held = new Promise<void>(resolve => (release = resolve))
|
|
2618
|
+
const heldOther = new Promise<void>(resolve => (releaseOther = resolve))
|
|
2619
|
+
r.gh = async argv => {
|
|
2620
|
+
// the active map's open-list read is held, and the viewed map's reads too (to look at the screen between)
|
|
2621
|
+
if (argv.includes('trailhead:map-198') && argv.includes('open')) await held
|
|
2622
|
+
if (argv.includes('trailhead:map-14') || argv.includes('14')) await heldOther
|
|
2623
|
+
return r.defaultGh(argv)
|
|
2624
|
+
}
|
|
2625
|
+
r.tickets = [issue(300, 'fresh of 198', ['trailhead:build'])]
|
|
2626
|
+
await r.clock.advance(FLOOR)
|
|
2627
|
+
const refreshing = completeTurn($)
|
|
2628
|
+
await settle(r)
|
|
2629
|
+
|
|
2630
|
+
await ui.press({ key: 'map-14' })
|
|
2631
|
+
await settle(r)
|
|
2632
|
+
const before = paneWrites.length
|
|
2633
|
+
release()
|
|
2634
|
+
await refreshing
|
|
2635
|
+
await settle(r)
|
|
2636
|
+
// the old answer is in, the new one is not: nothing of 198 is on screen, only the wait
|
|
2637
|
+
expect(await textOf(ui, 'map')).toBe('map #14')
|
|
2638
|
+
expect(await textOf(ui, 'loading')).toBe('Loading tickets…')
|
|
2639
|
+
expect(await hasKey(ui, 'ticket-300')).toBe(false)
|
|
2640
|
+
// and the store itself: the stale answer was dropped, not committed behind the render's check
|
|
2641
|
+
const landed = paneWrites.slice(before).map(v => JSON.stringify(v))
|
|
2642
|
+
expect(landed.some(v => v.includes('fresh of 198'))).toBe(false)
|
|
2643
|
+
releaseOther()
|
|
2644
|
+
await settle(r)
|
|
2645
|
+
expect(await hasKey(ui, 'ticket-300')).toBe(false)
|
|
2646
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2647
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
2648
|
+
})
|
|
2649
|
+
|
|
2650
|
+
test(`with no active map the open maps are listed and one can be viewed [${surface}]`, async ($, on) => {
|
|
2651
|
+
const r = rig(on, $)
|
|
2652
|
+
r.active = null
|
|
2653
|
+
await $.session.start(START)
|
|
2654
|
+
const ui = await openAndMount($, r)
|
|
2655
|
+
await completeTurn($)
|
|
2656
|
+
await settle(r)
|
|
2657
|
+
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
2658
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2659
|
+
expect(await hasKey(ui, 'map-198')).toBe(true)
|
|
2660
|
+
expect(await hasKey(ui, 'map-14')).toBe(true)
|
|
2661
|
+
|
|
2662
|
+
await pressMap(ui, r, 14)
|
|
2663
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2664
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14')
|
|
2665
|
+
expect((await ui.find({ key: 'back' }))?.props).toMatchObject({ label: 'back' })
|
|
2666
|
+
await ui.press({ key: 'back' })
|
|
2667
|
+
await settle(r)
|
|
2668
|
+
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
2669
|
+
expect(r.writes).toEqual([])
|
|
2670
|
+
})
|
|
2671
|
+
|
|
2672
|
+
const pressDashboard = async (ui: any, r: Rig) => {
|
|
2673
|
+
await ui.press({ key: 'open-whiteboard' })
|
|
2674
|
+
await settle(r)
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
test(`the Whiteboard row shows the maps index and the whiteboard tickets [${surface}]`, async ($, on) => {
|
|
2678
|
+
const r = rig(on, $)
|
|
2679
|
+
await $.session.start(START)
|
|
2680
|
+
const ui = await openAndMount($, r)
|
|
2681
|
+
expect((await ui.find({ key: 'open-whiteboard' }))?.props).toMatchObject({ label: 'Whiteboard' })
|
|
2682
|
+
const before = r.calls.length
|
|
2683
|
+
await pressDashboard(ui, r)
|
|
2684
|
+
|
|
2685
|
+
expect(r.calls.slice(before)).toEqual([
|
|
2686
|
+
ticketsCall('trailhead:whiteboard'),
|
|
2687
|
+
['gh', 'issue', 'list', '--label', 'trailhead:dashboard', '--state', 'open', '--limit', '10', '--json', 'number,url'],
|
|
2688
|
+
MAPS_CALL,
|
|
2689
|
+
uatCall(),
|
|
2690
|
+
countCall(14, 198),
|
|
2691
|
+
])
|
|
2692
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
2693
|
+
expect((await ui.find({ key: 'dashboard-title' }))?.children[0]).toMatchObject({ props: { bold: true, color: ACCENT } })
|
|
2694
|
+
expect((await ui.findAll({ type: 'Link' })).map((l: any) => l.props.href)).toContain(`${URL}/5`)
|
|
2695
|
+
expect((await ui.find({ key: 'to-map' }))?.props).toMatchObject({ label: 'map' })
|
|
2696
|
+
expect(await hasKey(ui, 'dashboard')).toBe(false)
|
|
2697
|
+
// the view is the whiteboard: its row is not drawn again
|
|
2698
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(false)
|
|
2699
|
+
|
|
2700
|
+
// every open map is a button, the active one marked
|
|
2701
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2702
|
+
expect((await ui.find({ key: 'map-198' }))?.props).toMatchObject({ label: '#198 trailhead mod' })
|
|
2703
|
+
expect(await hasKey(ui, 'map-14')).toBe(true)
|
|
2704
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
2705
|
+
|
|
2706
|
+
// the whiteboard by state, the quarantined one only counted, no map tickets
|
|
2707
|
+
expect(await hasKey(ui, 'whiteboard')).toBe(true)
|
|
2708
|
+
expect(await textOf(ui, 'head-whiteboard')).toBe('Whiteboard · 3 open · 1 assigned')
|
|
2709
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 1')
|
|
2710
|
+
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 1')
|
|
2711
|
+
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 1')
|
|
2712
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-303'])
|
|
2713
|
+
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
2714
|
+
expect(await hasKey(ui, 'work-301')).toBe(true)
|
|
2715
|
+
expect(await textOf(ui, 'unverified')).toBe('1 unverified ticket hidden (quarantined)')
|
|
2716
|
+
expect(await hasKey(ui, 'burndown-section')).toBe(false)
|
|
2717
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2718
|
+
expect(r.writes).toEqual([])
|
|
2719
|
+
})
|
|
2720
|
+
|
|
2721
|
+
test(`a Whiteboard row above the maps list carries the counts, the header has no dashboard button [${surface}]`, async ($, on) => {
|
|
2722
|
+
const r = rig(on, $)
|
|
2723
|
+
r.maps = [MAPS[0]]
|
|
2724
|
+
await $.session.start(START)
|
|
2725
|
+
const ui = await openAndMount($, r)
|
|
2726
|
+
// one map open: no maps group, the row is still there, without a per-ticket query
|
|
2727
|
+
expect(await hasKey(ui, 'group-maps')).toBe(false)
|
|
2728
|
+
expect((await ui.find({ key: 'open-whiteboard' }))?.props).toMatchObject({ label: 'Whiteboard' })
|
|
2729
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
2730
|
+
expect((await ui.find({ key: 'whiteboard-aside' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
2731
|
+
expect(await hasKey(ui, 'dashboard')).toBe(false)
|
|
2732
|
+
expect(r.calls.filter(argv => argv.includes('trailhead:whiteboard'))).toEqual([])
|
|
2733
|
+
expect(r.calls.filter(isGraphql)).toEqual([MAPS_CALL])
|
|
2734
|
+
// above the maps list and the tickets, below the heading
|
|
2735
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
2736
|
+
expect(keys.indexOf('whiteboard-row')).toBeGreaterThan(keys.indexOf('header'))
|
|
2737
|
+
expect(keys.indexOf('whiteboard-row')).toBeLessThan(keys.indexOf('group-frontier'))
|
|
2738
|
+
})
|
|
2739
|
+
|
|
2740
|
+
test(`the Whiteboard row sits above the Maps list [${surface}]`, async ($, on) => {
|
|
2741
|
+
const r = rig(on, $)
|
|
2742
|
+
await $.session.start(START)
|
|
2743
|
+
const ui = await openAndMount($, r)
|
|
2744
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
2745
|
+
expect(keys.indexOf('whiteboard-row')).toBeLessThan(keys.indexOf('group-maps'))
|
|
2746
|
+
expect(keys.indexOf('whiteboard-row')).toBeGreaterThan(keys.indexOf('header'))
|
|
2747
|
+
})
|
|
2748
|
+
|
|
2749
|
+
test(`a press on the Whiteboard row shows the whiteboard, the map button goes back [${surface}]`, async ($, on) => {
|
|
2750
|
+
const r = rig(on, $)
|
|
2751
|
+
r.maps = [MAPS[0]]
|
|
2752
|
+
await $.session.start(START)
|
|
2753
|
+
const ui = await openAndMount($, r)
|
|
2754
|
+
await pressDashboard(ui, r)
|
|
2755
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
2756
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(false)
|
|
2757
|
+
await ui.press({ key: 'to-map' })
|
|
2758
|
+
await settle(r)
|
|
2759
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
2760
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
2761
|
+
})
|
|
2762
|
+
|
|
2763
|
+
test(`the whiteboard counts are drawn as read, zero included, and follow the next read [${surface}]`, async ($, on) => {
|
|
2764
|
+
const r = rig(on, $)
|
|
2765
|
+
r.whiteboardCounts = { open: 0, total: 0 }
|
|
2766
|
+
await $.session.start(START)
|
|
2767
|
+
const ui = await openAndMount($, r)
|
|
2768
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 0 open / 0')
|
|
2769
|
+
r.whiteboardCounts = { open: 4, total: 11 }
|
|
2770
|
+
await r.clock.advance(FLOOR)
|
|
2771
|
+
await completeTurn($)
|
|
2772
|
+
await settle(r)
|
|
2773
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 4 open / 11')
|
|
2774
|
+
})
|
|
2775
|
+
|
|
2776
|
+
test(`a failed counts read shows the row name-only, never an error [${surface}]`, async ($, on) => {
|
|
2777
|
+
const r = rig(on, $)
|
|
2778
|
+
const base = r.defaultGh
|
|
2779
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
2780
|
+
await $.session.start(START)
|
|
2781
|
+
const ui = await openAndMount($, r)
|
|
2782
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
2783
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
2784
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2785
|
+
|
|
2786
|
+
// counts that were read once are not kept through a failed read
|
|
2787
|
+
r.gh = null
|
|
2788
|
+
await r.clock.advance(FLOOR)
|
|
2789
|
+
await completeTurn($)
|
|
2790
|
+
await settle(r)
|
|
2791
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
2792
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
2793
|
+
await r.clock.advance(FLOOR)
|
|
2794
|
+
await completeTurn($)
|
|
2795
|
+
await settle(r)
|
|
2796
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
2797
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2798
|
+
})
|
|
2799
|
+
|
|
2800
|
+
test(`both maps reads failing keep the maps list but draw the row name-only [${surface}]`, async ($, on) => {
|
|
2801
|
+
const r = rig(on, $)
|
|
2802
|
+
await $.session.start(START)
|
|
2803
|
+
const ui = await openAndMount($, r)
|
|
2804
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
2805
|
+
const base: Gh = argv => withHistory(r, ok(JSON.stringify(r.history)))(argv)
|
|
2806
|
+
r.gh = argv => (isGraphql(argv) || isMapsList(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
2807
|
+
await r.clock.advance(FLOOR)
|
|
2808
|
+
await completeTurn($)
|
|
2809
|
+
await settle(r)
|
|
2810
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2811
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
2812
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
2813
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2814
|
+
})
|
|
2815
|
+
|
|
2816
|
+
test(`unreadable whiteboard fields leave the maps with counts and the row name-only [${surface}]`, async ($, on) => {
|
|
2817
|
+
const r = rig(on, $)
|
|
2818
|
+
r.whiteboardCounts = null
|
|
2819
|
+
await $.session.start(START)
|
|
2820
|
+
const ui = await openAndMount($, r)
|
|
2821
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
2822
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
2823
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
2824
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2825
|
+
})
|
|
2826
|
+
|
|
2827
|
+
test(`with no active map the Whiteboard row sits above the maps [${surface}]`, async ($, on) => {
|
|
2828
|
+
const none = rig(on, $)
|
|
2829
|
+
none.active = null
|
|
2830
|
+
await $.session.start(START)
|
|
2831
|
+
const ui = await openAndMount($, none)
|
|
2832
|
+
await completeTurn($)
|
|
2833
|
+
await settle(none)
|
|
2834
|
+
expect(await hasKey(ui, 'dashboard')).toBe(false)
|
|
2835
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
2836
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
2837
|
+
expect(keys.indexOf('whiteboard-row')).toBeLessThan(keys.indexOf('group-maps'))
|
|
2838
|
+
})
|
|
2839
|
+
|
|
2840
|
+
test(`the Whiteboard row stays in the loading view of a map [${surface}]`, async ($, on) => {
|
|
2841
|
+
const r = rig(on, $)
|
|
2842
|
+
let release: () => void = () => undefined
|
|
2843
|
+
const held = new Promise<void>(resolve => (release = resolve))
|
|
2844
|
+
const base = r.defaultGh
|
|
2845
|
+
r.gh = async argv => {
|
|
2846
|
+
if (argv[2] === 'view') await held
|
|
2847
|
+
return base(argv)
|
|
2848
|
+
}
|
|
2849
|
+
await $.session.start(START)
|
|
2850
|
+
r.open()
|
|
2851
|
+
const ui = await mountPane($)
|
|
2852
|
+
await settle(r)
|
|
2853
|
+
expect(await textOf(ui, 'loading')).toBe('Loading tickets…')
|
|
2854
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
2855
|
+
release()
|
|
2856
|
+
await settle(r)
|
|
2857
|
+
})
|
|
2858
|
+
|
|
2859
|
+
test(`the maps list shows each map's open and total from one call [${surface}]`, async ($, on) => {
|
|
2860
|
+
const r = rig(on, $)
|
|
2861
|
+
await $.session.start(START)
|
|
2862
|
+
const ui = await openAndMount($, r)
|
|
2863
|
+
expect(r.calls.filter(argv => argv.includes('trailhead:map') || isGraphql(argv))).toEqual([MAPS_CALL])
|
|
2864
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
2865
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5')
|
|
2866
|
+
// a map the read gave no summary for is drawn name-only
|
|
2867
|
+
r.mapSummary = { 198: { total: 23, completed: 16 } }
|
|
2868
|
+
await r.clock.advance(FLOOR)
|
|
2869
|
+
await completeTurn($)
|
|
2870
|
+
await settle(r)
|
|
2871
|
+
expect(await hasKey(ui, 'map-aside-14')).toBe(false)
|
|
2872
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
2873
|
+
})
|
|
2874
|
+
|
|
2875
|
+
test(`a failing counts read falls back to the name-only list with no footer error [${surface}]`, async ($, on) => {
|
|
2876
|
+
const r = rig(on, $)
|
|
2877
|
+
const base = r.defaultGh
|
|
2878
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
2879
|
+
await $.session.start(START)
|
|
2880
|
+
const ui = await openAndMount($, r)
|
|
2881
|
+
expect(r.calls.filter(argv => argv.includes('trailhead:map') || isGraphql(argv))).toEqual([MAPS_CALL, MAPS_LIST_CALL])
|
|
2882
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2883
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' active')
|
|
2884
|
+
expect(await hasKey(ui, 'map-aside-14')).toBe(false)
|
|
2885
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2886
|
+
|
|
2887
|
+
// a reply with errors is a failed read too
|
|
2888
|
+
r.gh = argv => (isGraphql(argv) ? ok(JSON.stringify({ data: { repository: { issues: { nodes: [] } } }, errors: [{ message: 'x' }] })) : base(argv))
|
|
2889
|
+
await r.clock.advance(FLOOR)
|
|
2890
|
+
await completeTurn($)
|
|
2891
|
+
await settle(r)
|
|
2892
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2893
|
+
expect(await hasKey(ui, 'map-aside-14')).toBe(false)
|
|
2894
|
+
})
|
|
2895
|
+
|
|
2896
|
+
// The open-ticket total across every open map plus the whiteboard (5 + 2 + 3 with the default counts).
|
|
2897
|
+
const TOTAL_TEXT = '10 open across all maps + whiteboard'
|
|
2898
|
+
const totalLines = async (ui: any): Promise<number> => (await ui.findAll({ key: 'open-total' })).length
|
|
2899
|
+
const refreshOnce = async ($: any, r: Rig) => {
|
|
2900
|
+
await r.clock.advance(FLOOR)
|
|
2901
|
+
await completeTurn($)
|
|
2902
|
+
await settle(r)
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
test(`the total line shows exactly once in the map view, under the Whiteboard row [${surface}]`, async ($, on) => {
|
|
2906
|
+
const r = rig(on, $)
|
|
2907
|
+
await $.session.start(START)
|
|
2908
|
+
const ui = await openAndMount($, r)
|
|
2909
|
+
expect(await totalLines(ui)).toBe(1)
|
|
2910
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
2911
|
+
expect((await ui.find({ key: 'open-total' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
2912
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
2913
|
+
expect(keys.indexOf('open-total')).toBeGreaterThan(keys.indexOf('whiteboard-row'))
|
|
2914
|
+
expect(keys.indexOf('open-total')).toBeLessThan(keys.indexOf('group-frontier'))
|
|
2915
|
+
})
|
|
2916
|
+
|
|
2917
|
+
test(`the total line shows exactly once in the loading state and the no-active-map view [${surface}]`, async ($, on) => {
|
|
2918
|
+
const r = rig(on, $)
|
|
2919
|
+
r.active = null
|
|
2920
|
+
await $.session.start(START)
|
|
2921
|
+
const ui = await openAndMount($, r)
|
|
2922
|
+
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
2923
|
+
await completeTurn($)
|
|
2924
|
+
await settle(r)
|
|
2925
|
+
expect(await totalLines(ui)).toBe(1)
|
|
2926
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
2927
|
+
|
|
2928
|
+
// a map pressed while its read is held: Loading tickets, the last good total beside the Whiteboard row
|
|
2929
|
+
const base = r.defaultGh
|
|
2930
|
+
let release: () => void = () => undefined
|
|
2931
|
+
const held = new Promise<void>(resolve => (release = resolve))
|
|
2932
|
+
r.gh = async argv => {
|
|
2933
|
+
if (argv[2] === 'view' || isTicketsRead(argv)) await held
|
|
2934
|
+
return base(argv)
|
|
2935
|
+
}
|
|
2936
|
+
await ui.press({ key: 'map-14' })
|
|
2937
|
+
await settle(r)
|
|
2938
|
+
expect(await textOf(ui, 'loading')).toBe('Loading tickets…')
|
|
2939
|
+
expect(await totalLines(ui)).toBe(1)
|
|
2940
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
2941
|
+
release()
|
|
2942
|
+
await settle(r)
|
|
2943
|
+
})
|
|
2944
|
+
|
|
2945
|
+
test(`the total line shows exactly once under the Dashboard header row [${surface}]`, async ($, on) => {
|
|
2946
|
+
const r = rig(on, $)
|
|
2947
|
+
await $.session.start(START)
|
|
2948
|
+
const ui = await openAndMount($, r)
|
|
2949
|
+
await pressDashboard(ui, r)
|
|
2950
|
+
expect(await hasKey(ui, 'dashboard-title')).toBe(true)
|
|
2951
|
+
expect(await totalLines(ui)).toBe(1)
|
|
2952
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
2953
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
2954
|
+
expect(keys.indexOf('open-total')).toBeGreaterThan(keys.indexOf('header-row'))
|
|
2955
|
+
expect(keys.indexOf('open-total')).toBeLessThan(keys.indexOf('group-maps'))
|
|
2956
|
+
})
|
|
2957
|
+
|
|
2958
|
+
test(`the total is counted over the open maps' numbers, one extra call per read [${surface}]`, async ($, on) => {
|
|
2959
|
+
const r = rig(on, $)
|
|
2960
|
+
r.maps = [MAPS[0]]
|
|
2961
|
+
await $.session.start(START)
|
|
2962
|
+
const ui = await openAndMount($, r)
|
|
2963
|
+
expect(r.calls.filter(isCountRead)).toEqual([countCall(198)])
|
|
2964
|
+
// 5 open on the map + 3 on the whiteboard
|
|
2965
|
+
expect(await textOf(ui, 'open-total')).toBe('8 open across all maps + whiteboard')
|
|
2966
|
+
// a closed map is never counted: only the open maps are enumerated
|
|
2967
|
+
r.maps = MAPS
|
|
2968
|
+
await refreshOnce($, r)
|
|
2969
|
+
expect(r.calls.filter(isCountRead).at(-1)).toEqual(countCall(14, 198))
|
|
2970
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
2971
|
+
})
|
|
2972
|
+
|
|
2973
|
+
for (const mode of ['fail', 'errors', 'junk'] as const) {
|
|
2974
|
+
test(`a count read that fails (${mode}) before any good read draws no total line [${surface}]`, async ($, on) => {
|
|
2975
|
+
const r = rig(on, $)
|
|
2976
|
+
r.countMode = mode
|
|
2977
|
+
await $.session.start(START)
|
|
2978
|
+
const ui = await openAndMount($, r)
|
|
2979
|
+
expect(await totalLines(ui)).toBe(0)
|
|
2980
|
+
// the rest of the pane is unharmed and no footer error is raised for it
|
|
2981
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
2982
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2983
|
+
})
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
test(`a later failing count read keeps the last good total while the open maps are the same [${surface}]`, async ($, on) => {
|
|
2987
|
+
const r = rig(on, $)
|
|
2988
|
+
await $.session.start(START)
|
|
2989
|
+
const ui = await openAndMount($, r)
|
|
2990
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
2991
|
+
r.countMode = 'fail'
|
|
2992
|
+
r.openCounts = { ...r.openCounts, 'trailhead:whiteboard': 99 }
|
|
2993
|
+
await refreshOnce($, r)
|
|
2994
|
+
expect(await totalLines(ui)).toBe(1)
|
|
2995
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
2996
|
+
// a good read takes over again
|
|
2997
|
+
r.countMode = 'ok'
|
|
2998
|
+
await refreshOnce($, r)
|
|
2999
|
+
expect(await textOf(ui, 'open-total')).toBe('106 open across all maps + whiteboard')
|
|
3000
|
+
})
|
|
3001
|
+
|
|
3002
|
+
test(`a failing count read after the open maps changed hides the total, never stale [${surface}]`, async ($, on) => {
|
|
3003
|
+
const r = rig(on, $)
|
|
3004
|
+
await $.session.start(START)
|
|
3005
|
+
const ui = await openAndMount($, r)
|
|
3006
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
3007
|
+
r.countMode = 'fail'
|
|
3008
|
+
r.maps = [MAPS[0]]
|
|
3009
|
+
await refreshOnce($, r)
|
|
3010
|
+
expect(await totalLines(ui)).toBe(0)
|
|
3011
|
+
})
|
|
3012
|
+
|
|
3013
|
+
test(`the name-only maps fallback draws no total and makes no count call [${surface}]`, async ($, on) => {
|
|
3014
|
+
const r = rig(on, $)
|
|
3015
|
+
const base = r.defaultGh
|
|
3016
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
3017
|
+
await $.session.start(START)
|
|
3018
|
+
const ui = await openAndMount($, r)
|
|
3019
|
+
expect(r.calls.filter(isCountRead)).toEqual([])
|
|
3020
|
+
expect(await totalLines(ui)).toBe(0)
|
|
3021
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
3022
|
+
})
|
|
3023
|
+
|
|
3024
|
+
test(`a fallback after a good total hides it rather than keep a figure over a list of unknown completeness [${surface}]`, async ($, on) => {
|
|
3025
|
+
const r = rig(on, $)
|
|
3026
|
+
const base = r.defaultGh
|
|
3027
|
+
await $.session.start(START)
|
|
3028
|
+
const ui = await openAndMount($, r)
|
|
3029
|
+
expect(await totalLines(ui)).toBe(1)
|
|
3030
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
3031
|
+
await refreshOnce($, r)
|
|
3032
|
+
expect(await totalLines(ui)).toBe(0)
|
|
3033
|
+
})
|
|
3034
|
+
|
|
3035
|
+
test(`a narrow pane keeps the lower bound in the short form [${surface}]`, async ($, on) => {
|
|
3036
|
+
const r = rig(on, $)
|
|
3037
|
+
r.countTotal = 101
|
|
3038
|
+
await $.session.start(START)
|
|
3039
|
+
const ui = await openAndMount($, r, { bodyColumns: 24 })
|
|
3040
|
+
expect(await textOf(ui, 'open-total')).toBe('≥10 open in total')
|
|
3041
|
+
})
|
|
3042
|
+
|
|
3043
|
+
test(`a truncated maps read words the total as a lower bound only [${surface}]`, async ($, on) => {
|
|
3044
|
+
const r = rig(on, $)
|
|
3045
|
+
// more open maps exist (101) than the two the read returned
|
|
3046
|
+
r.countTotal = 101
|
|
3047
|
+
await $.session.start(START)
|
|
3048
|
+
const ui = await openAndMount($, r)
|
|
3049
|
+
expect(await textOf(ui, 'open-total')).toBe('at least 10 open across all maps + whiteboard')
|
|
3050
|
+
// exactly as many maps as were read is not a cut
|
|
3051
|
+
r.countTotal = null
|
|
3052
|
+
await refreshOnce($, r)
|
|
3053
|
+
expect(await textOf(ui, 'open-total')).toBe(TOTAL_TEXT)
|
|
3054
|
+
})
|
|
3055
|
+
|
|
3056
|
+
test(`the dashboard heading counts the whiteboard and every assigned trusted ticket is a row [${surface}]`, async ($, on) => {
|
|
3057
|
+
const r = rig(on, $)
|
|
3058
|
+
r.whiteboard = [
|
|
3059
|
+
...WHITEBOARD,
|
|
3060
|
+
issue(305, 'Assigned but quarantined', [...WB, 'trailhead:task', 'trailhead:unverified'], ['dave']),
|
|
3061
|
+
issue(306, 'Paused with a holder', [...WB, 'trailhead:task', 'trailhead:paused'], ['erin']),
|
|
3062
|
+
]
|
|
3063
|
+
await $.session.start(START)
|
|
3064
|
+
const ui = await openAndMount($, r)
|
|
3065
|
+
await pressDashboard(ui, r)
|
|
3066
|
+
expect(await textOf(ui, 'head-whiteboard')).toBe('Whiteboard · 4 open · 2 assigned')
|
|
3067
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-306', 'row-303'])
|
|
3068
|
+
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
3069
|
+
expect(await hasKey(ui, 'resume-306')).toBe(true)
|
|
3070
|
+
expect(await hasKey(ui, 'row-305')).toBe(false)
|
|
3071
|
+
expect(await textOf(ui, 'unverified')).toBe('2 unverified tickets hidden (quarantined)')
|
|
3072
|
+
})
|
|
3073
|
+
|
|
3074
|
+
test(`the map button goes back to the map shown before [${surface}]`, async ($, on) => {
|
|
3075
|
+
const r = rig(on, $)
|
|
3076
|
+
await $.session.start(START)
|
|
3077
|
+
const ui = await openAndMount($, r)
|
|
3078
|
+
await pressMap(ui, r, 14)
|
|
3079
|
+
await pressDashboard(ui, r)
|
|
3080
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
3081
|
+
|
|
3082
|
+
await ui.press({ key: 'to-map' })
|
|
3083
|
+
await settle(r)
|
|
3084
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3085
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
3086
|
+
expect(await hasKey(ui, 'dashboard-title')).toBe(false)
|
|
3087
|
+
})
|
|
3088
|
+
|
|
3089
|
+
test(`a map pressed on the dashboard shows that map [${surface}]`, async ($, on) => {
|
|
3090
|
+
const r = rig(on, $)
|
|
3091
|
+
await $.session.start(START)
|
|
3092
|
+
const ui = await openAndMount($, r)
|
|
3093
|
+
await pressDashboard(ui, r)
|
|
3094
|
+
await pressMap(ui, r, 14)
|
|
3095
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3096
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
3097
|
+
|
|
3098
|
+
await pressDashboard(ui, r)
|
|
3099
|
+
await pressMap(ui, r, 198)
|
|
3100
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3101
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
3102
|
+
})
|
|
3103
|
+
|
|
3104
|
+
test(`a work press on a whiteboard ticket clears and runs the work verb [${surface}]`, async ($, on) => {
|
|
3105
|
+
const r = rig(on, $)
|
|
3106
|
+
await $.session.start(START)
|
|
3107
|
+
const ui = await openAndMount($, r)
|
|
3108
|
+
await pressDashboard(ui, r)
|
|
3109
|
+
await ui.press({ key: 'work-301' })
|
|
3110
|
+
await settle(r)
|
|
3111
|
+
expect(r.runs).toEqual([
|
|
3112
|
+
{ command: 'clear', args: '' },
|
|
3113
|
+
{ command: 'trailhead:work', args: '301' },
|
|
3114
|
+
])
|
|
3115
|
+
// the dashboard stays shown across the reset
|
|
3116
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
3117
|
+
expect(await keysOf(ui, 'row-')).toContain('row-301')
|
|
3118
|
+
})
|
|
3119
|
+
|
|
3120
|
+
test(`an empty whiteboard says so, a failed read shows the reason [${surface}]`, async ($, on) => {
|
|
3121
|
+
const r = rig(on, $)
|
|
3122
|
+
r.whiteboard = []
|
|
3123
|
+
r.dashboard = []
|
|
3124
|
+
await $.session.start(START)
|
|
3125
|
+
const ui = await openAndMount($, r)
|
|
3126
|
+
await pressDashboard(ui, r)
|
|
3127
|
+
expect(await textOf(ui, 'no-tickets')).toBe('No open whiteboard tickets.')
|
|
3128
|
+
// no pinned index yet: the title is plain text
|
|
3129
|
+
expect((await ui.findAll({ type: 'Link' })).map((l: any) => l.props.href)).not.toContain(`${URL}/5`)
|
|
3130
|
+
|
|
3131
|
+
r.whiteboard = WHITEBOARD
|
|
3132
|
+
r.gh = argv => (argv.includes('trailhead:whiteboard') || argv.includes('label=trailhead:whiteboard') ? { exitCode: 1, stdout: '', stderr: 'boom' } : r.defaultGh(argv))
|
|
3133
|
+
await r.clock.advance(FLOOR)
|
|
3134
|
+
await ui.press({ key: 'refresh' })
|
|
3135
|
+
await settle(r)
|
|
3136
|
+
expect(await textOf(ui, 'age')).toBe('⚠ gh failed (exit 1) · list from 15s ago')
|
|
3137
|
+
// a list that may be stale says nothing about being empty
|
|
3138
|
+
expect(await hasKey(ui, 'no-tickets')).toBe(false)
|
|
3139
|
+
})
|
|
3140
|
+
|
|
3141
|
+
test(`with no active map the dashboard is one press away [${surface}]`, async ($, on) => {
|
|
3142
|
+
const r = rig(on, $)
|
|
3143
|
+
r.active = null
|
|
3144
|
+
await $.session.start(START)
|
|
3145
|
+
const ui = await openAndMount($, r)
|
|
3146
|
+
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
3147
|
+
await pressDashboard(ui, r)
|
|
3148
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-303'])
|
|
3149
|
+
await ui.press({ key: 'to-map' })
|
|
3150
|
+
await settle(r)
|
|
3151
|
+
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
3152
|
+
})
|
|
3153
|
+
|
|
3154
|
+
test(`the dashboard stays shown through /clear [${surface}]`, async ($, on) => {
|
|
3155
|
+
const r = rig(on, $)
|
|
3156
|
+
await $.session.start(START)
|
|
3157
|
+
const ui = await openAndMount($, r)
|
|
3158
|
+
await pressDashboard(ui, r)
|
|
3159
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
3160
|
+
await settle(r)
|
|
3161
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
3162
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-303'])
|
|
3163
|
+
})
|
|
3164
|
+
test(`the pane keeps its repo when the session cwd moves out of it [${surface}]`, async ($, on) => {
|
|
3165
|
+
const r = rig(on, $)
|
|
3166
|
+
await $.session.start(START)
|
|
3167
|
+
const ui = await openAndMount($, r)
|
|
3168
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3169
|
+
// A shell `cd /tmp`: no repo there, relative reads miss, and a `gh` without its own cwd fails.
|
|
3170
|
+
r.cwd = '/tmp'
|
|
3171
|
+
r.repo = null
|
|
3172
|
+
r.calls.length = 0
|
|
3173
|
+
r.cwds.length = 0
|
|
3174
|
+
await r.clock.advance(FLOOR)
|
|
3175
|
+
await completeTurn($)
|
|
3176
|
+
await settle(r)
|
|
3177
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3178
|
+
expect(await hasKey(ui, 'footer')).toBe(true)
|
|
3179
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
3180
|
+
expect(r.calls.length).toBeGreaterThan(0)
|
|
3181
|
+
expect(r.cwds.every(cwd => cwd === ROOT)).toBe(true)
|
|
3182
|
+
})
|
|
3183
|
+
|
|
3184
|
+
test(`the pane follows a move to another GitHub repo and drops the old one's map [${surface}]`, async ($, on) => {
|
|
3185
|
+
const r = rig(on, $)
|
|
3186
|
+
await $.session.start(START)
|
|
3187
|
+
const ui = await openAndMount($, r)
|
|
3188
|
+
expect(await keysOf(ui, 'row-')).toContain('row-209')
|
|
3189
|
+
r.cwd = ROOT_OTHER
|
|
3190
|
+
r.repo = { root: ROOT_OTHER, remote: 'https://github.com/o/other.git', internal: false }
|
|
3191
|
+
r.otherActive = '14\n'
|
|
3192
|
+
r.map = OTHER_MAP
|
|
3193
|
+
r.tickets = OTHER_TICKETS
|
|
3194
|
+
r.history = OTHER_HISTORY
|
|
3195
|
+
r.calls.length = 0
|
|
3196
|
+
r.cwds.length = 0
|
|
3197
|
+
await r.clock.advance(FLOOR)
|
|
3198
|
+
await completeTurn($)
|
|
3199
|
+
await settle(r)
|
|
3200
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3201
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
3202
|
+
expect(r.calls).toContainEqual(MAPS_CALL)
|
|
3203
|
+
expect(r.cwds.every(cwd => cwd === ROOT_OTHER)).toBe(true)
|
|
3204
|
+
// The toplevel is read where the session now is, not at its project root, which a shell `cd` leaves behind.
|
|
3205
|
+
expect(r.gitCalls.at(-1)).toEqual({ argv: ['git', 'rev-parse', '--show-toplevel'], cwd: ROOT_OTHER })
|
|
3206
|
+
expect(r.reads).toContain(`${ROOT_OTHER}/.trailhead/active-map`)
|
|
3207
|
+
})
|
|
3208
|
+
|
|
3209
|
+
test(`a session started outside any repo says so and runs no gh [${surface}]`, async ($, on) => {
|
|
3210
|
+
const r = rig(on, $)
|
|
3211
|
+
r.cwd = '/tmp'
|
|
3212
|
+
r.repo = null
|
|
3213
|
+
await $.session.start({ ...START, cwd: '/tmp' })
|
|
3214
|
+
const ui = await openAndMount($, r)
|
|
3215
|
+
expect(await textOf(ui, 'no-repo')).toBe('Not in a GitHub repository: open the session in a trailhead repo')
|
|
3216
|
+
await completeTurn($)
|
|
3217
|
+
await settle(r)
|
|
3218
|
+
expect(r.calls).toEqual([])
|
|
3219
|
+
})
|
|
3220
|
+
|
|
3221
|
+
test(`a remote that names no host and repo is no pin: the same message [${surface}]`, async ($, on) => {
|
|
3222
|
+
const r = rig(on, $)
|
|
3223
|
+
r.repo = { root: ROOT, remote: '/srv/git/r.git', internal: false }
|
|
3224
|
+
await $.session.start(START)
|
|
3225
|
+
const ui = await openAndMount($, r)
|
|
3226
|
+
expect(await textOf(ui, 'no-repo')).toBe('Not in a GitHub repository: open the session in a trailhead repo')
|
|
3227
|
+
expect(r.calls).toEqual([])
|
|
3228
|
+
})
|
|
3229
|
+
test(`a worktree session reads both files and runs gh under its own toplevel [${surface}]`, async ($, on) => {
|
|
3230
|
+
const r = rig(on, $)
|
|
3231
|
+
// The repo's root is the main tree's; the session works in the linked worktree.
|
|
3232
|
+
r.top = ROOT_TREE
|
|
3233
|
+
r.cwd = ROOT_TREE
|
|
3234
|
+
r.sessionRoot = ROOT_TREE
|
|
3235
|
+
r.active = '14\n'
|
|
3236
|
+
r.treeActive = '198\n'
|
|
3237
|
+
r.work = stateText()
|
|
3238
|
+
await $.session.start({ ...START, cwd: ROOT_TREE })
|
|
3239
|
+
const ui = await openAndMount($, r)
|
|
3240
|
+
expect(r.gitCalls[0]).toEqual({ argv: ['git', 'rev-parse', '--show-toplevel'], cwd: ROOT_TREE })
|
|
3241
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3242
|
+
expect(r.reads).toContain(`${ROOT_TREE}/.trailhead/active-map`)
|
|
3243
|
+
expect(r.reads).toContain(`${ROOT_TREE}/.trailhead/work-state.json`)
|
|
3244
|
+
// Il record del run vive nel main checkout (#296): e l'unica lettura ammessa fuori dal toplevel.
|
|
3245
|
+
expect(r.reads.filter(path => path.startsWith(`${ROOT}/`) && !path.endsWith('/.trailhead/auto-run.json'))).toEqual([])
|
|
3246
|
+
expect(r.cwds.length).toBeGreaterThan(0)
|
|
3247
|
+
expect(r.cwds.every(cwd => cwd === ROOT_TREE)).toBe(true)
|
|
3248
|
+
})
|
|
3249
|
+
|
|
3250
|
+
for (const remote of ['git@ghe.example.com:o/r.git', 'ssh://git@GitHub.com:22/o/r', 'https://user@github.com/o/r.git']) {
|
|
3251
|
+
test(`the remote ${remote} still pins; gh finds the host itself [${surface}]`, async ($, on) => {
|
|
3252
|
+
const r = rig(on, $)
|
|
3253
|
+
r.repo = { root: ROOT, remote, internal: false }
|
|
3254
|
+
r.authHosts = ['ghe.example.com']
|
|
3255
|
+
await $.session.start(START)
|
|
3256
|
+
const ui = await openAndMount($, r)
|
|
3257
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3258
|
+
expect(r.calls).toContainEqual(MAPS_CALL)
|
|
3259
|
+
expect(r.cwds.every(cwd => cwd === ROOT)).toBe(true)
|
|
3260
|
+
})
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
test(`a refresh in flight when only the band's refresh moves the pin never commits the old repo's tickets [${surface}]`, async ($, on) => {
|
|
3264
|
+
const r = rig(on, $)
|
|
3265
|
+
await $.session.start(START)
|
|
3266
|
+
let release: () => void = () => {}
|
|
3267
|
+
const gate = new Promise<void>(resolve => {
|
|
3268
|
+
release = resolve
|
|
3269
|
+
})
|
|
3270
|
+
// The answer is fixed when the call starts, so the reads begun in /repo keep /repo's tickets.
|
|
3271
|
+
r.gh = async argv => {
|
|
3272
|
+
const answer = r.defaultGh(argv)
|
|
3273
|
+
await gate
|
|
3274
|
+
return answer
|
|
3275
|
+
}
|
|
3276
|
+
r.open()
|
|
3277
|
+
const ui = await mountPane($)
|
|
3278
|
+
await settle(r)
|
|
3279
|
+
// The same map number in the other repo; a turn start refreshes the band, which moves the pin
|
|
3280
|
+
// while the pane has no hook on it.
|
|
3281
|
+
r.cwd = ROOT_OTHER
|
|
3282
|
+
r.repo = { root: ROOT_OTHER, remote: 'https://github.com/o/other.git', internal: false }
|
|
3283
|
+
r.otherActive = '198\n'
|
|
3284
|
+
r.tickets = OTHER_TICKETS
|
|
3285
|
+
r.history = OTHER_HISTORY
|
|
3286
|
+
await $.turn.start({ text: 'go', turnId: 'race-1' })
|
|
3287
|
+
release()
|
|
3288
|
+
await settle(r)
|
|
3289
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
1072
3290
|
})
|
|
1073
3291
|
|
|
1074
|
-
test(`
|
|
3292
|
+
test(`the pin survives /clear after the session cwd moved out of the repo [${surface}]`, async ($, on) => {
|
|
1075
3293
|
const r = rig(on, $)
|
|
1076
|
-
r.
|
|
3294
|
+
r.work = stateText()
|
|
1077
3295
|
await $.session.start(START)
|
|
1078
3296
|
const ui = await openAndMount($, r)
|
|
1079
|
-
expect(await textOf(ui, '
|
|
1080
|
-
|
|
1081
|
-
r.
|
|
1082
|
-
|
|
1083
|
-
|
|
3297
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3298
|
+
r.cwd = '/tmp'
|
|
3299
|
+
r.repo = null
|
|
3300
|
+
r.calls.length = 0
|
|
3301
|
+
r.cwds.length = 0
|
|
3302
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
1084
3303
|
await settle(r)
|
|
1085
|
-
expect(await ui
|
|
3304
|
+
expect(await hasKey(ui, 'no-repo')).toBe(false)
|
|
3305
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3306
|
+
expect(r.calls.length).toBeGreaterThan(0)
|
|
3307
|
+
expect(r.cwds.every(cwd => cwd === ROOT)).toBe(true)
|
|
1086
3308
|
})
|
|
1087
|
-
|
|
1088
|
-
test(`a pane found already up at auto-open ends the auto-open for good [${surface}]`, async ($, on) => {
|
|
3309
|
+
test(`a session started in a host gh does not serve says so and runs no gh read [${surface}]`, async ($, on) => {
|
|
1089
3310
|
const r = rig(on, $)
|
|
3311
|
+
r.repo = { root: ROOT, remote: `git@gitlab-${surface}.example.com:o/r.git`, internal: false }
|
|
1090
3312
|
await $.session.start(START)
|
|
1091
|
-
r
|
|
1092
|
-
|
|
1093
|
-
await settle(r)
|
|
1094
|
-
expect(r.opens).toEqual([])
|
|
1095
|
-
r.panes = []
|
|
1096
|
-
await ui.redraw()
|
|
1097
|
-
await mountBand($, FULLSCREEN)
|
|
3313
|
+
const ui = await openAndMount($, r)
|
|
3314
|
+
await completeTurn($)
|
|
1098
3315
|
await settle(r)
|
|
1099
|
-
expect(
|
|
3316
|
+
expect(await textOf(ui, 'no-repo')).toBe('Not in a GitHub repository: open the session in a trailhead repo')
|
|
3317
|
+
expect(r.calls).toEqual([])
|
|
3318
|
+
expect(r.authCalls).toEqual([`gitlab-${surface}.example.com`])
|
|
1100
3319
|
})
|
|
1101
3320
|
|
|
1102
|
-
test(`a
|
|
3321
|
+
test(`a cd into a GitLab repo keeps the pin and the map [${surface}]`, async ($, on) => {
|
|
1103
3322
|
const r = rig(on, $)
|
|
1104
3323
|
await $.session.start(START)
|
|
1105
|
-
r
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
r.
|
|
1110
|
-
await
|
|
1111
|
-
|
|
1112
|
-
// the map moves on while the pane is closed
|
|
1113
|
-
r.active = '199\n'
|
|
1114
|
-
r.map = { ...MAP, title: 'second map', url: `${URL}/199` }
|
|
1115
|
-
r.tickets = [issue(300, 'only one', ['trailhead:build'])]
|
|
1116
|
-
await r.clock.advance(3_000)
|
|
3324
|
+
const ui = await openAndMount($, r)
|
|
3325
|
+
r.cwd = ROOT_OTHER
|
|
3326
|
+
r.repo = { root: ROOT_OTHER, remote: 'https://lab.example.com/o/lab.git', internal: false }
|
|
3327
|
+
r.calls.length = 0
|
|
3328
|
+
r.cwds.length = 0
|
|
3329
|
+
await r.clock.advance(FLOOR)
|
|
1117
3330
|
await completeTurn($)
|
|
1118
3331
|
await settle(r)
|
|
1119
|
-
expect(
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
expect(await textOf(ui, 'map')).toBe('map #199')
|
|
1125
|
-
await r.clock.advance(FLOOR - 3_000)
|
|
1126
|
-
await settle(r)
|
|
1127
|
-
expect(await textOf(ui, 'map')).toBe('#199 second map')
|
|
1128
|
-
expect(fetches(r)).toBe(2)
|
|
3332
|
+
expect(r.authCalls).toEqual(['lab.example.com'])
|
|
3333
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3334
|
+
expect(await hasKey(ui, 'no-repo')).toBe(false)
|
|
3335
|
+
expect(r.calls.length).toBeGreaterThan(0)
|
|
3336
|
+
expect(r.cwds.every(cwd => cwd === ROOT)).toBe(true)
|
|
1129
3337
|
})
|
|
1130
3338
|
|
|
1131
|
-
|
|
1132
|
-
const pressMap = async (ui: any, r: Rig, n: number) => {
|
|
1133
|
-
await ui.press({ key: `map-${n}` })
|
|
1134
|
-
await settle(r)
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
test(`two open maps are listed, the active one marked [${surface}]`, async ($, on) => {
|
|
3339
|
+
test(`the host check runs once per host for the session [${surface}]`, async ($, on) => {
|
|
1138
3340
|
const r = rig(on, $)
|
|
3341
|
+
r.repo = { root: ROOT, remote: `git@once-${surface}.example.com:o/r.git`, internal: false }
|
|
3342
|
+
r.authHosts = [`once-${surface}.example.com`]
|
|
1139
3343
|
await $.session.start(START)
|
|
1140
3344
|
const ui = await openAndMount($, r)
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
expect(await textOf(ui, 'map
|
|
1147
|
-
expect(
|
|
1148
|
-
expect(await textOf(ui, 'map-mark-14')).toBe(' ')
|
|
1149
|
-
expect((await ui.find({ key: 'map-14' }))?.props).toMatchObject({ label: '#14 older map' })
|
|
1150
|
-
expect(await ui.find({ key: 'map-198' })).toBeUndefined()
|
|
1151
|
-
expect(await ui.find({ key: 'viewing' })).toBeUndefined()
|
|
1152
|
-
// under the header, before the tickets
|
|
1153
|
-
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
1154
|
-
expect(keys.indexOf('group-maps')).toBeGreaterThan(keys.indexOf('header'))
|
|
1155
|
-
expect(keys.indexOf('group-maps')).toBeLessThan(keys.indexOf('group-frontier'))
|
|
3345
|
+
for (let i = 0; i < 3; i += 1) {
|
|
3346
|
+
await r.clock.advance(FLOOR)
|
|
3347
|
+
await completeTurn($)
|
|
3348
|
+
await settle(r)
|
|
3349
|
+
}
|
|
3350
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3351
|
+
expect(r.authCalls).toEqual([`once-${surface}.example.com`])
|
|
1156
3352
|
})
|
|
1157
|
-
|
|
1158
|
-
test(`one open map draws no maps group [${surface}]`, async ($, on) => {
|
|
3353
|
+
test(`a failed host check is retried after a while, so a later login pins [${surface}]`, async ($, on) => {
|
|
1159
3354
|
const r = rig(on, $)
|
|
1160
|
-
|
|
3355
|
+
const host = `late-${surface}.example.com`
|
|
3356
|
+
r.repo = { root: ROOT, remote: `git@${host}:o/r.git`, internal: false }
|
|
1161
3357
|
await $.session.start(START)
|
|
1162
3358
|
const ui = await openAndMount($, r)
|
|
1163
|
-
expect(await
|
|
1164
|
-
|
|
1165
|
-
|
|
3359
|
+
expect(await textOf(ui, 'no-repo')).toBe('Not in a GitHub repository: open the session in a trailhead repo')
|
|
3360
|
+
// A turn soon after does not ask again; one after the retry window does, and the login now holds.
|
|
3361
|
+
await r.clock.advance(FLOOR)
|
|
3362
|
+
await completeTurn($)
|
|
3363
|
+
await settle(r)
|
|
3364
|
+
expect(r.authCalls).toEqual([host])
|
|
3365
|
+
r.authHosts = [host]
|
|
3366
|
+
await r.clock.advance(60_000)
|
|
3367
|
+
await completeTurn($)
|
|
3368
|
+
await settle(r)
|
|
3369
|
+
expect(r.authCalls).toEqual([host, host])
|
|
3370
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
1166
3371
|
})
|
|
1167
|
-
|
|
1168
|
-
test(`a press on another map shows its tickets, the active-map marker is never written [${surface}]`, async ($, on) => {
|
|
3372
|
+
test(`a pin moved by the band alone never leaves the old repo's rows drawn [${surface}]`, async ($, on) => {
|
|
1169
3373
|
const r = rig(on, $)
|
|
1170
|
-
r.work = stateText()
|
|
1171
3374
|
await $.session.start(START)
|
|
1172
3375
|
const ui = await openAndMount($, r)
|
|
1173
|
-
await
|
|
1174
|
-
|
|
3376
|
+
expect(await keysOf(ui, 'row-')).toContain('row-209')
|
|
3377
|
+
r.cwd = ROOT_OTHER
|
|
3378
|
+
r.repo = { root: ROOT_OTHER, remote: 'https://github.com/o/other.git', internal: false }
|
|
3379
|
+
r.otherActive = '14\n'
|
|
3380
|
+
r.map = OTHER_MAP
|
|
3381
|
+
r.tickets = OTHER_TICKETS
|
|
3382
|
+
r.history = OTHER_HISTORY
|
|
3383
|
+
// Only the band hooks a turn start: it moves the pin, no read of the pane is in flight.
|
|
3384
|
+
await $.turn.start({ text: 'go', turnId: 'band-1' })
|
|
3385
|
+
expect(await keysOf(ui, 'row-')).not.toContain('row-209')
|
|
3386
|
+
await settle(r)
|
|
1175
3387
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
1176
|
-
expect(await textOf(ui, 'destination')).toBe('An older destination.')
|
|
1177
3388
|
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
1178
|
-
expect(await textOf(ui, 'holder-141')).toBe('@carol')
|
|
1179
|
-
expect(await textOf(ui, 'burndown-counts')).toBe('open 2 · done 0 · removed 0 · peak 2')
|
|
1180
|
-
// the ticket in play belongs to the other map: nothing here is marked
|
|
1181
|
-
expect(await hasKey(ui, 'mark-208')).toBe(false)
|
|
1182
|
-
expect(r.calls.slice(-4).map(argv => argv.slice(0, 4))).toEqual([
|
|
1183
|
-
['gh', 'issue', 'view', '14'],
|
|
1184
|
-
['gh', 'issue', 'list', '--label'],
|
|
1185
|
-
['gh', 'issue', 'list', '--label'],
|
|
1186
|
-
['gh', 'issue', 'list', '--label'],
|
|
1187
|
-
])
|
|
1188
|
-
expect(r.calls.some(argv => argv.includes('trailhead:map-14'))).toBe(true)
|
|
1189
|
-
|
|
1190
|
-
// the viewed map is bold, the active one a button; one line says where we are
|
|
1191
|
-
expect(await textOf(ui, 'map-label-14')).toBe('#14 older map')
|
|
1192
|
-
expect((await ui.find({ key: 'map-198' }))?.props).toMatchObject({ label: '#198 trailhead mod' })
|
|
1193
|
-
expect(await hasKey(ui, 'map-14')).toBe(false)
|
|
1194
|
-
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
1195
|
-
expect((await ui.find({ key: 'viewing-text' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
1196
|
-
expect((await ui.find({ key: 'back' }))?.props).toMatchObject({ label: 'back to active' })
|
|
1197
|
-
// the work verb owns the marker: the pane never wrote it
|
|
1198
|
-
expect(r.writes).toEqual([])
|
|
1199
|
-
expect(r.active).toBe('198\n')
|
|
1200
3389
|
})
|
|
1201
3390
|
|
|
1202
|
-
|
|
3391
|
+
// The panel follows the whiteboard ticket this session takes (#306).
|
|
3392
|
+
const WB_STATE = { map: null, ticket: 301, title: 'Fix the README badge', step: 'discuss' }
|
|
3393
|
+
const BOARD_KEY = 'dashboard-title'
|
|
3394
|
+
const CREATE_WB = 'gh issue create -t t --label trailhead:whiteboard'
|
|
3395
|
+
|
|
3396
|
+
test(`an own whiteboard ticket in play switches the panel to the whiteboard [${surface}]`, async ($, on) => {
|
|
1203
3397
|
const r = rig(on, $)
|
|
3398
|
+
r.work = stateText(WB_STATE)
|
|
1204
3399
|
await $.session.start(START)
|
|
1205
3400
|
const ui = await openAndMount($, r)
|
|
1206
|
-
await
|
|
1207
|
-
expect(await
|
|
1208
|
-
|
|
1209
|
-
await ui.press({ key: 'back' })
|
|
1210
|
-
await settle(r)
|
|
1211
|
-
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
1212
|
-
expect(await keysOf(ui, 'row-')).toContain('row-208')
|
|
1213
|
-
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
1214
|
-
expect(await textOf(ui, 'map-label-198')).toBe('#198 trailhead mod')
|
|
1215
|
-
|
|
1216
|
-
await pressMap(ui, r, 14)
|
|
1217
|
-
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
1218
|
-
await pressMap(ui, r, 198)
|
|
1219
|
-
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
1220
|
-
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
1221
|
-
expect(r.writes).toEqual([])
|
|
3401
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(true)
|
|
3402
|
+
expect(await hasKey(ui, 'map')).toBe(false)
|
|
1222
3403
|
})
|
|
1223
3404
|
|
|
1224
|
-
test(`
|
|
3405
|
+
test(`going back to the map by hand is not undone by the next turn [${surface}]`, async ($, on) => {
|
|
1225
3406
|
const r = rig(on, $)
|
|
3407
|
+
r.work = stateText(WB_STATE)
|
|
1226
3408
|
await $.session.start(START)
|
|
1227
3409
|
const ui = await openAndMount($, r)
|
|
1228
|
-
await
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
// the work verb switched the active map to the viewed one at the end of a turn
|
|
1232
|
-
r.active = '14\n'
|
|
3410
|
+
await ui.press({ key: 'to-map' })
|
|
3411
|
+
await settle(r)
|
|
3412
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(false)
|
|
1233
3413
|
await r.clock.advance(FLOOR)
|
|
1234
3414
|
await completeTurn($)
|
|
1235
3415
|
await settle(r)
|
|
1236
|
-
expect(await
|
|
1237
|
-
expect(await
|
|
1238
|
-
expect(await textOf(ui, 'map-label-14')).toBe('#14 older map')
|
|
1239
|
-
expect(await textOf(ui, 'map-aside-14')).toBe(' active')
|
|
1240
|
-
expect((await ui.find({ key: 'map-198' }))?.props).toMatchObject({ label: '#198 trailhead mod' })
|
|
3416
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(false)
|
|
3417
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
1241
3418
|
})
|
|
1242
3419
|
|
|
1243
|
-
test(`
|
|
3420
|
+
test(`the followed ticket is not forced again after /clear [${surface}]`, async ($, on) => {
|
|
1244
3421
|
const r = rig(on, $)
|
|
3422
|
+
r.work = stateText(WB_STATE)
|
|
1245
3423
|
await $.session.start(START)
|
|
1246
3424
|
const ui = await openAndMount($, r)
|
|
1247
|
-
await
|
|
1248
|
-
await
|
|
3425
|
+
await ui.press({ key: 'to-map' })
|
|
3426
|
+
await settle(r)
|
|
3427
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
3428
|
+
r.work = stateText({ ...WB_STATE, session: r.session })
|
|
3429
|
+
await ui.redraw()
|
|
1249
3430
|
await settle(r)
|
|
1250
|
-
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '140' }])
|
|
1251
|
-
expect(r.writes).toEqual([])
|
|
1252
|
-
// still viewing until the verb has changed the marker
|
|
1253
|
-
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
1254
|
-
|
|
1255
|
-
r.active = '14\n'
|
|
1256
3431
|
await r.clock.advance(FLOOR)
|
|
1257
3432
|
await completeTurn($)
|
|
1258
3433
|
await settle(r)
|
|
1259
|
-
expect(await
|
|
1260
|
-
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
1261
|
-
expect(await textOf(ui, 'map-aside-14')).toBe(' active')
|
|
1262
|
-
expect(await hasKey(ui, 'map-aside-198')).toBe(false)
|
|
3434
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(false)
|
|
1263
3435
|
})
|
|
1264
3436
|
|
|
1265
|
-
test(`a
|
|
3437
|
+
test(`a state that appears mid-session is followed after the helper call [${surface}]`, async ($, on) => {
|
|
1266
3438
|
const r = rig(on, $)
|
|
1267
3439
|
await $.session.start(START)
|
|
1268
3440
|
const ui = await openAndMount($, r)
|
|
1269
|
-
expect(await textOf(ui, '
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
r.gh = argv => (argv.includes('trailhead:map') ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
1273
|
-
r.tickets = [issue(300, 'fresh one', ['trailhead:build'])]
|
|
1274
|
-
await r.clock.advance(FLOOR)
|
|
1275
|
-
await completeTurn($)
|
|
3441
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
3442
|
+
r.work = stateText(WB_STATE)
|
|
3443
|
+
await $.tool.call({ tool: 'Bash', command: 'node /x/trailhead-work-state.js set --ticket 301' } as never)
|
|
1276
3444
|
await settle(r)
|
|
1277
|
-
expect(await
|
|
1278
|
-
|
|
1279
|
-
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
3445
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(true)
|
|
3446
|
+
})
|
|
1280
3447
|
|
|
1281
|
-
|
|
1282
|
-
r
|
|
3448
|
+
test(`a state that appears before a turn ends is followed [${surface}]`, async ($, on) => {
|
|
3449
|
+
const r = rig(on, $)
|
|
3450
|
+
await $.session.start(START)
|
|
3451
|
+
const ui = await openAndMount($, r)
|
|
3452
|
+
r.work = stateText(WB_STATE)
|
|
1283
3453
|
await r.clock.advance(FLOOR)
|
|
1284
3454
|
await completeTurn($)
|
|
1285
3455
|
await settle(r)
|
|
1286
|
-
expect(await
|
|
1287
|
-
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
3456
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(true)
|
|
1288
3457
|
})
|
|
1289
3458
|
|
|
1290
|
-
test(`a
|
|
3459
|
+
test(`a map ticket in play does not switch the panel [${surface}]`, async ($, on) => {
|
|
1291
3460
|
const r = rig(on, $)
|
|
3461
|
+
r.work = stateText()
|
|
1292
3462
|
await $.session.start(START)
|
|
1293
3463
|
const ui = await openAndMount($, r)
|
|
1294
|
-
await
|
|
1295
|
-
|
|
3464
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(false)
|
|
3465
|
+
})
|
|
1296
3466
|
|
|
1297
|
-
|
|
3467
|
+
test(`a Resolve leaves the panel on the whiteboard [${surface}]`, async ($, on) => {
|
|
3468
|
+
const r = rig(on, $)
|
|
3469
|
+
r.work = stateText(WB_STATE)
|
|
3470
|
+
await $.session.start(START)
|
|
3471
|
+
const ui = await openAndMount($, r)
|
|
3472
|
+
r.work = null
|
|
1298
3473
|
await r.clock.advance(FLOOR)
|
|
1299
3474
|
await completeTurn($)
|
|
1300
3475
|
await settle(r)
|
|
1301
|
-
expect(await
|
|
1302
|
-
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
1303
|
-
expect(await hasKey(ui, 'head-maps')).toBe(false)
|
|
3476
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(true)
|
|
1304
3477
|
})
|
|
1305
3478
|
|
|
1306
|
-
test(`a
|
|
3479
|
+
test(`a whiteboard ticket created while a map is shown raises one toast and no switch [${surface}]`, async ($, on) => {
|
|
1307
3480
|
const r = rig(on, $)
|
|
1308
|
-
// every value the pane stores, to look at what the old answer wrote
|
|
1309
|
-
const paneWrites: unknown[] = []
|
|
1310
|
-
on('state.set', { plugin: 'trailhead', key: 'mapPane' }, ($$, e, next) => {
|
|
1311
|
-
paneWrites.push(e.value)
|
|
1312
|
-
return next(e)
|
|
1313
|
-
})
|
|
1314
3481
|
await $.session.start(START)
|
|
1315
3482
|
const ui = await openAndMount($, r)
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
let release: () => void = () => undefined
|
|
1319
|
-
let releaseOther: () => void = () => undefined
|
|
1320
|
-
const held = new Promise<void>(resolve => (release = resolve))
|
|
1321
|
-
const heldOther = new Promise<void>(resolve => (releaseOther = resolve))
|
|
1322
|
-
r.gh = async argv => {
|
|
1323
|
-
// the active map's open-list read is held, and the viewed map's reads too (to look at the screen between)
|
|
1324
|
-
if (argv.includes('trailhead:map-198') && argv.includes('open')) await held
|
|
1325
|
-
if (argv.includes('trailhead:map-14') || argv.includes('14')) await heldOther
|
|
1326
|
-
return r.defaultGh(argv)
|
|
1327
|
-
}
|
|
1328
|
-
r.tickets = [issue(300, 'fresh of 198', ['trailhead:build'])]
|
|
1329
|
-
await r.clock.advance(FLOOR)
|
|
1330
|
-
const refreshing = completeTurn($)
|
|
1331
|
-
await settle(r)
|
|
1332
|
-
|
|
1333
|
-
await ui.press({ key: 'map-14' })
|
|
1334
|
-
await settle(r)
|
|
1335
|
-
const before = paneWrites.length
|
|
1336
|
-
release()
|
|
1337
|
-
await refreshing
|
|
1338
|
-
await settle(r)
|
|
1339
|
-
// the old answer is in, the new one is not: nothing of 198 is on screen, only the wait
|
|
1340
|
-
expect(await textOf(ui, 'map')).toBe('map #14')
|
|
1341
|
-
expect(await textOf(ui, 'loading')).toBe('Loading tickets…')
|
|
1342
|
-
expect(await hasKey(ui, 'ticket-300')).toBe(false)
|
|
1343
|
-
// and the store itself: the stale answer was dropped, not committed behind the render's check
|
|
1344
|
-
const landed = paneWrites.slice(before).map(v => JSON.stringify(v))
|
|
1345
|
-
expect(landed.some(v => v.includes('fresh of 198'))).toBe(false)
|
|
1346
|
-
releaseOther()
|
|
3483
|
+
r.stdout = `${URL}/310\n`
|
|
3484
|
+
await $.tool.call({ tool: 'Bash', command: CREATE_WB } as never)
|
|
1347
3485
|
await settle(r)
|
|
1348
|
-
expect(
|
|
1349
|
-
expect(await
|
|
1350
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
3486
|
+
expect(r.toasts).toEqual(['new whiteboard ticket #310: open Whiteboard'])
|
|
3487
|
+
expect(await hasKey(ui, BOARD_KEY)).toBe(false)
|
|
1351
3488
|
})
|
|
1352
3489
|
|
|
1353
|
-
test(`
|
|
3490
|
+
test(`a whiteboard ticket created on the whiteboard view raises no toast [${surface}]`, async ($, on) => {
|
|
1354
3491
|
const r = rig(on, $)
|
|
1355
|
-
r.active = null
|
|
1356
3492
|
await $.session.start(START)
|
|
1357
3493
|
const ui = await openAndMount($, r)
|
|
1358
|
-
await
|
|
3494
|
+
await ui.press({ key: 'open-whiteboard' })
|
|
1359
3495
|
await settle(r)
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
expect(
|
|
3496
|
+
r.stdout = `${URL}/310\n`
|
|
3497
|
+
await $.tool.call({ tool: 'Bash', command: CREATE_WB } as never)
|
|
3498
|
+
await settle(r)
|
|
3499
|
+
expect(r.toasts).toEqual([])
|
|
3500
|
+
})
|
|
1364
3501
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
3502
|
+
test(`a ticket created without the whiteboard label raises no toast [${surface}]`, async ($, on) => {
|
|
3503
|
+
const r = rig(on, $)
|
|
3504
|
+
await $.session.start(START)
|
|
3505
|
+
await openAndMount($, r)
|
|
3506
|
+
r.stdout = `${URL}/310\n`
|
|
3507
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue create -t t --label trailhead:task' } as never)
|
|
1370
3508
|
await settle(r)
|
|
1371
|
-
expect(
|
|
1372
|
-
expect(r.writes).toEqual([])
|
|
3509
|
+
expect(r.toasts).toEqual([])
|
|
1373
3510
|
})
|
|
1374
3511
|
}
|