@marcomigozzi/trailhead 0.14.0 → 0.15.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 +118 -30
- package/README.md +107 -29
- package/bin/base-preflight-docs.test.js +65 -0
- package/bin/execute-split-docs.test.js +146 -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 +1 -1
- package/bin/lib/codex-projection.test.js +41 -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 +3 -3
- package/bin/trailhead.test.js +341 -1
- 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/hooks/lib/base-preflight.test.js +513 -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 +409 -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/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/uat-backfill.test.js +150 -0
- package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
- package/plugins/trailhead/hooks/mod/README.md +38 -6
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +168 -15
- package/plugins/trailhead/hooks/mod/map-pane.tsx +275 -106
- package/plugins/trailhead/hooks/mod/register.tsx +15 -2
- package/plugins/trailhead/hooks/mod/work-state-view.ts +17 -0
- 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-search-guard.js +43 -7
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
- package/plugins/trailhead/skills/_shared/choices.md +4 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +15 -3
- 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 +22 -11
- package/plugins/trailhead/skills/_shared/substrate.md +6 -4
- package/plugins/trailhead/skills/_shared/teamwork.md +16 -7
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +17 -9
- 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/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 +2 -2
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- 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 +8 -6
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +14 -12
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +4 -4
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +26 -10
- package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -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 +340 -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-report-status.js +274 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
- package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
- package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
- package/plugins/trailhead/tests/map-pane-ui.test.ts +566 -19
- package/plugins/trailhead/tests/map-pane-view.test.ts +304 -16
- package/plugins/trailhead/tests/work-state-view.test.ts +25 -1
- package/plugins/trailhead/types/index.d.ts +37 -3
|
@@ -61,9 +61,28 @@ const MAPS = [
|
|
|
61
61
|
{ number: 198, title: 'trailhead mod', url: `${URL}/198` },
|
|
62
62
|
{ number: 14, title: 'older map', url: `${URL}/14` },
|
|
63
63
|
]
|
|
64
|
+
// The maps read: one GraphQL call that carries each map's sub-issue counts and the whiteboard's.
|
|
65
|
+
const MAPS_QUERY =
|
|
66
|
+
'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}}}whiteboardOpen:issues(states:OPEN,labels:["trailhead:whiteboard"]){totalCount}whiteboardAll:issues(labels:["trailhead:whiteboard"]){totalCount}}}'
|
|
67
|
+
const MAPS_CALL = ['gh', 'api', 'graphql', '-F', 'owner={owner}', '-F', 'repo={repo}', '-f', `query=${MAPS_QUERY}`]
|
|
68
|
+
const MAPS_LIST_CALL = ['gh', 'issue', 'list', '--label', 'trailhead:map', '--state', 'open', '--limit', '100', '--json', 'number,title,url']
|
|
69
|
+
// The pending-UAT read of one scope (a map label, or the whiteboard): the tickets with their bodies.
|
|
70
|
+
const uatCall = (scope: string) => ['gh', 'issue', 'list', '--label', 'trailhead:uat', '--label', scope, '--state', 'open', '--limit', '200', '--json', 'number,body']
|
|
71
|
+
const isGraphql = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql'
|
|
72
|
+
const isMapsList = (argv: readonly string[]) => argv[2] === 'list' && argv.includes('trailhead:map')
|
|
64
73
|
const OTHER_TICKETS = [issue(140, 'Old frontier ticket', ['trailhead:build']), issue(141, 'Old held ticket', ['trailhead:task'], ['carol'])]
|
|
65
74
|
const OTHER_HISTORY = OTHER_TICKETS.map(t => hrow(t.number, t.labels.map(l => l.name), null))
|
|
66
75
|
|
|
76
|
+
// The whiteboard (map-less tickets) and the pinned dashboard issue, as the dashboard view reads them.
|
|
77
|
+
const WB = ['trailhead:ticket', 'trailhead:whiteboard']
|
|
78
|
+
const WHITEBOARD = [
|
|
79
|
+
issue(301, 'Fix the README badge', [...WB, 'trailhead:bug']),
|
|
80
|
+
issue(302, 'Bump the CI node', [...WB, 'trailhead:task'], ['alice']),
|
|
81
|
+
issue(303, 'Waits on upstream', [...WB, 'trailhead:task', 'trailhead:blocked']),
|
|
82
|
+
issue(304, 'Quarantined loose one', [...WB, 'trailhead:task', 'trailhead:unverified']),
|
|
83
|
+
]
|
|
84
|
+
const DASHBOARD = [{ number: 5, url: `${URL}/5` }]
|
|
85
|
+
|
|
67
86
|
const stateText = (over: Record<string, unknown> = {}): string =>
|
|
68
87
|
JSON.stringify({
|
|
69
88
|
map: 198,
|
|
@@ -78,7 +97,7 @@ const stateText = (over: Record<string, unknown> = {}): string =>
|
|
|
78
97
|
})
|
|
79
98
|
|
|
80
99
|
// The plugin's atoms, which the engine resets on /clear.
|
|
81
|
-
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'pendingWork', 'paneAutoOpened']
|
|
100
|
+
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'isDashboard', 'dashboardPane', 'pendingWork', 'paneAutoOpened', 'ownTicket']
|
|
82
101
|
|
|
83
102
|
const PANE_PROPS = {
|
|
84
103
|
title: 'Map',
|
|
@@ -104,9 +123,17 @@ const rig = (on: On, engine?: any) => {
|
|
|
104
123
|
map: MAP as unknown,
|
|
105
124
|
// The open maps list, and the second map's own answers (view, open list, history).
|
|
106
125
|
maps: MAPS as unknown[],
|
|
126
|
+
// Sub-issue totals per map number, as the GraphQL read reports them (a map without one has no summary).
|
|
127
|
+
mapSummary: { 198: { total: 23, completed: 16 }, 14: { total: 5, completed: 2 } } as Record<number, unknown>,
|
|
128
|
+
// The whiteboard's open and total tickets as the same call reports them (null: the fields are absent).
|
|
129
|
+
whiteboardCounts: { open: 3, total: 9 } as { open: number; total: number } | null,
|
|
107
130
|
otherMap: OTHER_MAP as unknown,
|
|
108
131
|
otherTickets: OTHER_TICKETS as unknown[],
|
|
109
132
|
otherHistory: OTHER_HISTORY as unknown[],
|
|
133
|
+
whiteboard: WHITEBOARD as unknown[],
|
|
134
|
+
// The UAT read's answer: one row per UAT ticket, `{ number, body }`; null makes the read fail.
|
|
135
|
+
uat: [] as unknown[] | null,
|
|
136
|
+
dashboard: DASHBOARD as unknown[],
|
|
110
137
|
gh: null as Gh | null,
|
|
111
138
|
// What `gh` answers when no test replaced it; a test can wrap it.
|
|
112
139
|
defaultGh: null as unknown as Gh,
|
|
@@ -137,7 +164,16 @@ const rig = (on: On, engine?: any) => {
|
|
|
137
164
|
},
|
|
138
165
|
}
|
|
139
166
|
const defaultGh: Gh = argv => {
|
|
140
|
-
if (argv
|
|
167
|
+
if (argv.includes('trailhead:uat')) return r.uat === null ? { exitCode: 1, stdout: '', stderr: 'boom' } : ok(JSON.stringify(r.uat))
|
|
168
|
+
if (argv.includes('trailhead:whiteboard')) return ok(JSON.stringify(r.whiteboard))
|
|
169
|
+
if (argv.includes('trailhead:dashboard')) return ok(JSON.stringify(r.dashboard))
|
|
170
|
+
if (isGraphql(argv)) {
|
|
171
|
+
const nodes = (r.maps as { number: number }[]).map(m => ({ ...m, subIssuesSummary: r.mapSummary[m.number] }))
|
|
172
|
+
const wb = r.whiteboardCounts
|
|
173
|
+
const counts = wb === null ? {} : { whiteboardOpen: { totalCount: wb.open }, whiteboardAll: { totalCount: wb.total } }
|
|
174
|
+
return ok(JSON.stringify({ data: { repository: { issues: { nodes }, ...counts } } }))
|
|
175
|
+
}
|
|
176
|
+
if (isMapsList(argv)) return ok(JSON.stringify(r.maps))
|
|
141
177
|
const isOther = argv.includes('14') || argv.includes('trailhead:map-14')
|
|
142
178
|
if (argv[2] === 'view') return ok(JSON.stringify(isOther ? r.otherMap : r.map))
|
|
143
179
|
return ok(JSON.stringify(argv.includes('all') ? (isOther ? r.otherHistory : r.history) : isOther ? r.otherTickets : r.tickets))
|
|
@@ -243,11 +279,11 @@ const settle = async (r: Rig) => {
|
|
|
243
279
|
for (let i = 0; i < 10; i += 1) await r.clock.settle()
|
|
244
280
|
}
|
|
245
281
|
// 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[2] === 'list' && argv.includes('open') && !argv.includes('trailhead:map')).length
|
|
282
|
+
const fetches = (r: Rig) => r.calls.filter(argv => argv[2] === 'list' && argv.includes('open') && !argv.includes('trailhead:map') && !argv.includes('trailhead:uat')).length
|
|
247
283
|
// A gh that answers as the default does, except for the history read.
|
|
248
284
|
const withHistory = (r: Rig, answer: GhOut): Gh => argv => {
|
|
249
285
|
if (argv.includes('all')) return answer
|
|
250
|
-
if (argv
|
|
286
|
+
if (isGraphql(argv) || isMapsList(argv)) return r.defaultGh(argv)
|
|
251
287
|
return argv[2] === 'view' ? ok(JSON.stringify(r.map)) : ok(JSON.stringify(r.tickets))
|
|
252
288
|
}
|
|
253
289
|
const hasKey = async (ui: any, key: string): Promise<boolean> => (await ui.find({ key })) !== undefined
|
|
@@ -275,7 +311,8 @@ for (const surface of SURFACES) {
|
|
|
275
311
|
['gh', 'issue', 'view', '198', '--json', 'title,body,url'],
|
|
276
312
|
['gh', 'issue', 'list', '--label', 'trailhead:map-198', '--state', 'open', '--limit', '200', '--json', 'number,title,labels,assignees,url'],
|
|
277
313
|
['gh', 'issue', 'list', '--label', 'trailhead:map-198', '--state', 'all', '--limit', '5000', '--json', 'number,labels,createdAt,closedAt'],
|
|
278
|
-
|
|
314
|
+
MAPS_CALL,
|
|
315
|
+
uatCall('trailhead:map-198'),
|
|
279
316
|
])
|
|
280
317
|
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
281
318
|
expect((await ui.find({ type: 'Link' }))?.props).toMatchObject({ href: `${URL}/198` })
|
|
@@ -297,8 +334,10 @@ for (const surface of SURFACES) {
|
|
|
297
334
|
expect(links).toContain(`${URL}/209`)
|
|
298
335
|
|
|
299
336
|
// 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: '
|
|
337
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
338
|
+
expect((await ui.find({ key: 'self-208' }))?.children[0]).toMatchObject({ props: { color: ACCENT } })
|
|
339
|
+
expect((await ui.find({ key: 'self-208' }))?.children[0].props).not.toHaveProperty('bold')
|
|
340
|
+
expect(await ui.find({ key: 'holder-208' })).toBeUndefined()
|
|
302
341
|
expect(await textOf(ui, 'mark-208')).toBe('▸ ')
|
|
303
342
|
expect((await ui.find({ key: 'mark-208' }))?.children[0]).toMatchObject({ props: { bold: true, color: ACCENT } })
|
|
304
343
|
expect((await ui.find({ key: 'ticket-208' }))?.children[0]).toMatchObject({ props: { bold: true, color: ACCENT } })
|
|
@@ -319,6 +358,117 @@ for (const surface of SURFACES) {
|
|
|
319
358
|
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
320
359
|
})
|
|
321
360
|
|
|
361
|
+
test(`a UAT ticket sits under UAT between In progress and Paused, with its parent line and its work button [${surface}]`, async ($, on) => {
|
|
362
|
+
const r = rig(on, $)
|
|
363
|
+
r.work = stateText()
|
|
364
|
+
r.tickets = [
|
|
365
|
+
...TICKETS,
|
|
366
|
+
issue(240, 'UAT of the burn-down', ['trailhead:task', 'trailhead:uat']),
|
|
367
|
+
issue(241, 'Device UAT seed', ['trailhead:seed', 'trailhead:blocked', 'trailhead:uat']),
|
|
368
|
+
issue(242, 'Held UAT', ['trailhead:task', 'trailhead:uat'], ['alice']),
|
|
369
|
+
issue(243, 'Paused UAT', ['trailhead:task', 'trailhead:uat', 'trailhead:paused']),
|
|
370
|
+
]
|
|
371
|
+
r.uat = [
|
|
372
|
+
{ number: 240, body: `Run it\n\nUAT of: [Burn-down view of the map](${URL}/209)\n` },
|
|
373
|
+
{ number: 241, body: `UAT of: #212` },
|
|
374
|
+
{ number: 243, body: `UAT of: [Paused parent](${URL}/150)` },
|
|
375
|
+
]
|
|
376
|
+
await $.session.start(START)
|
|
377
|
+
const ui = await openAndMount($, r)
|
|
378
|
+
|
|
379
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 3')
|
|
380
|
+
expect((await ui.find({ key: 'head-uat' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
381
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-frontier', 'group-in-progress', 'group-uat', 'group-paused', 'group-blocked'])
|
|
382
|
+
// each ticket once: a UAT row is in no other group, a paused UAT stays under Paused
|
|
383
|
+
expect(await keysOf(ui, 'row-')).toEqual([
|
|
384
|
+
'row-209', 'row-210', 'row-214', 'row-208', 'row-212', 'row-240', 'row-241', 'row-242', 'row-243', 'row-211', 'row-213',
|
|
385
|
+
])
|
|
386
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
387
|
+
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
388
|
+
// the parent line, dim, under the row; none without a parent
|
|
389
|
+
expect(await textOf(ui, 'uat-of-240')).toBe('tests Burn-down view of the map #209')
|
|
390
|
+
expect((await ui.find({ key: 'uat-of-240' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
391
|
+
expect(await textOf(ui, 'uat-of-241')).toBe('tests #212')
|
|
392
|
+
expect(await hasKey(ui, 'uat-of-242')).toBe(false)
|
|
393
|
+
expect(await textOf(ui, 'uat-of-243')).toBe('tests Paused parent #150')
|
|
394
|
+
// a marker, not a state: a frontier UAT task keeps its work button, a held one its holder, a seed stays dim
|
|
395
|
+
expect(await hasKey(ui, 'work-240')).toBe(true)
|
|
396
|
+
expect(await textOf(ui, 'holder-242')).toBe('@alice')
|
|
397
|
+
expect(await hasKey(ui, 'work-242')).toBe(false)
|
|
398
|
+
expect(await hasKey(ui, 'work-241')).toBe(false)
|
|
399
|
+
expect((await ui.find({ key: 'ticket-241' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test(`no UAT ticket draws no UAT group [${surface}]`, async ($, on) => {
|
|
403
|
+
const none = rig(on, $)
|
|
404
|
+
await $.session.start(START)
|
|
405
|
+
const ui0 = await openAndMount($, none)
|
|
406
|
+
expect(await hasKey(ui0, 'group-uat')).toBe(false)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test(`a failed UAT read draws the UAT rows without the parent line, no error [${surface}]`, async ($, on) => {
|
|
410
|
+
const r = rig(on, $)
|
|
411
|
+
r.tickets = [...TICKETS, issue(240, 'UAT of the burn-down', ['trailhead:task', 'trailhead:uat'])]
|
|
412
|
+
r.uat = null
|
|
413
|
+
await $.session.start(START)
|
|
414
|
+
const ui = await openAndMount($, r)
|
|
415
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
416
|
+
expect(await hasKey(ui, 'uat-of-240')).toBe(false)
|
|
417
|
+
expect(await hasKey(ui, 'error')).toBe(false)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test(`the dashboard view groups UAT whiteboard tickets with their parent, reading the whiteboard scope [${surface}]`, async ($, on) => {
|
|
421
|
+
const r = rig(on, $)
|
|
422
|
+
r.whiteboard = [...WHITEBOARD, issue(306, 'Loose UAT', [...WB, 'trailhead:task', 'trailhead:uat'])]
|
|
423
|
+
r.uat = [{ number: 306, body: `UAT of: [Loose parent](${URL}/290)` }]
|
|
424
|
+
await $.session.start(START)
|
|
425
|
+
const ui = await openAndMount($, r)
|
|
426
|
+
await pressDashboard(ui, r)
|
|
427
|
+
expect(r.calls.some(argv => JSON.stringify(argv) === JSON.stringify(uatCall('trailhead:whiteboard')))).toBe(true)
|
|
428
|
+
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
429
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-306', 'row-303'])
|
|
430
|
+
expect(await textOf(ui, 'uat-of-306')).toBe('tests Loose parent #290')
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
test(`a paused ticket sits under Paused between In progress and Blocked, with no work button [${surface}]`, async ($, on) => {
|
|
434
|
+
const r = rig(on, $)
|
|
435
|
+
r.work = stateText()
|
|
436
|
+
r.tickets = [
|
|
437
|
+
...TICKETS,
|
|
438
|
+
issue(230, 'Paused with a holder', ['trailhead:build', 'trailhead:paused'], ['ToRvaLDz']),
|
|
439
|
+
issue(231, 'Paused and released', ['trailhead:build', 'trailhead:paused']),
|
|
440
|
+
issue(232, 'Paused and blocked', ['trailhead:build', 'trailhead:paused', 'trailhead:blocked'], ['bob']),
|
|
441
|
+
]
|
|
442
|
+
await $.session.start(START)
|
|
443
|
+
const ui = await openAndMount($, r)
|
|
444
|
+
|
|
445
|
+
expect(await textOf(ui, 'head-paused')).toBe('Paused · 3')
|
|
446
|
+
expect((await ui.find({ key: 'head-paused' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
447
|
+
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 2')
|
|
448
|
+
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 2')
|
|
449
|
+
// a paused ticket appears in one group only: Frontier, In progress, Paused, Blocked in order
|
|
450
|
+
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'])
|
|
451
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-frontier', 'group-in-progress', 'group-paused', 'group-blocked'])
|
|
452
|
+
expect(await textOf(ui, 'holder-230')).toBe('@ToRvaLDz')
|
|
453
|
+
expect((await ui.find({ key: 'holder-230' }))?.children[0].props).not.toHaveProperty('dimColor')
|
|
454
|
+
expect(await textOf(ui, 'holder-232')).toBe('@bob')
|
|
455
|
+
expect(await ui.find({ key: 'holder-231' })).toBeUndefined()
|
|
456
|
+
for (const n of [230, 231, 232]) expect(await hasKey(ui, `work-${n}`)).toBe(false)
|
|
457
|
+
expect((await ui.find({ key: 'ticket-231' }))?.children[0].props).not.toHaveProperty('dimColor')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test(`the dashboard view groups paused whiteboard tickets the same way [${surface}]`, async ($, on) => {
|
|
461
|
+
const r = rig(on, $)
|
|
462
|
+
r.whiteboard = [...WHITEBOARD, issue(305, 'Paused loose one', [...WB, 'trailhead:task', 'trailhead:paused'], ['alice'])]
|
|
463
|
+
await $.session.start(START)
|
|
464
|
+
const ui = await openAndMount($, r)
|
|
465
|
+
await pressDashboard(ui, r)
|
|
466
|
+
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
467
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-305', 'row-303'])
|
|
468
|
+
expect(await hasKey(ui, 'work-305')).toBe(false)
|
|
469
|
+
expect(await textOf(ui, 'holder-305')).toBe('@alice')
|
|
470
|
+
})
|
|
471
|
+
|
|
322
472
|
test(`ticket rows are spaced apart on the desktop only [${surface}]`, async ($, on) => {
|
|
323
473
|
const r = rig(on)
|
|
324
474
|
await $.session.start(START)
|
|
@@ -489,6 +639,70 @@ for (const surface of SURFACES) {
|
|
|
489
639
|
expect(await ui.find({ key: 'mark-212' })).toBeUndefined()
|
|
490
640
|
})
|
|
491
641
|
|
|
642
|
+
test(`another session's work state does not move this session's mark [${surface}]`, async ($, on) => {
|
|
643
|
+
const r = rig(on, $)
|
|
644
|
+
r.work = stateText()
|
|
645
|
+
await $.session.start(START)
|
|
646
|
+
const ui = await openAndMount($, r)
|
|
647
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
648
|
+
|
|
649
|
+
// a second session on the same map takes over the shared file
|
|
650
|
+
r.work = stateText({ ticket: 212, title: 'Native gh-backed tools', session: NEXT_SESSION })
|
|
651
|
+
await r.clock.advance(FLOOR)
|
|
652
|
+
await completeTurn($)
|
|
653
|
+
await settle(r)
|
|
654
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
655
|
+
expect(await textOf(ui, 'mark-208')).toBe('▸ ')
|
|
656
|
+
expect(await ui.find({ key: 'self-212' })).toBeUndefined()
|
|
657
|
+
expect(await textOf(ui, 'holder-212')).toBe('@alice')
|
|
658
|
+
|
|
659
|
+
// and then removes it: the last state read was not ours, so the mark stays
|
|
660
|
+
r.work = null
|
|
661
|
+
await r.clock.advance(FLOOR)
|
|
662
|
+
await completeTurn($)
|
|
663
|
+
await settle(r)
|
|
664
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
test(`this session's own Resolve, which removes the file, clears the mark [${surface}]`, async ($, on) => {
|
|
668
|
+
const r = rig(on, $)
|
|
669
|
+
r.work = stateText()
|
|
670
|
+
await $.session.start(START)
|
|
671
|
+
const ui = await openAndMount($, r)
|
|
672
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
673
|
+
r.work = null
|
|
674
|
+
await r.clock.advance(FLOOR)
|
|
675
|
+
await completeTurn($)
|
|
676
|
+
await settle(r)
|
|
677
|
+
expect(await ui.find({ key: 'self-208' })).toBeUndefined()
|
|
678
|
+
expect(await ui.find({ key: 'mark-208' })).toBeUndefined()
|
|
679
|
+
expect(await textOf(ui, 'holder-208')).toBe('@ToRvaLDz')
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
test(`each session marks its own ticket on the same map [${surface}]`, async ($, on) => {
|
|
683
|
+
const r = rig(on, $)
|
|
684
|
+
r.session = NEXT_SESSION
|
|
685
|
+
r.work = stateText({ ticket: 212, title: 'Native gh-backed tools', session: NEXT_SESSION })
|
|
686
|
+
await $.session.start(START)
|
|
687
|
+
const ui = await openAndMount($, r)
|
|
688
|
+
expect(await textOf(ui, 'self-212')).toBe('this session')
|
|
689
|
+
expect(await textOf(ui, 'mark-212')).toBe('▸ ')
|
|
690
|
+
expect(await textOf(ui, 'holder-208')).toBe('@ToRvaLDz')
|
|
691
|
+
expect(await ui.find({ key: 'self-208' })).toBeUndefined()
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
test(`a /clear starts the new session with no ticket of its own [${surface}]`, async ($, on) => {
|
|
695
|
+
const r = rig(on, $)
|
|
696
|
+
r.work = stateText()
|
|
697
|
+
await $.session.start(START)
|
|
698
|
+
const ui = await openAndMount($, r)
|
|
699
|
+
expect(await textOf(ui, 'self-208')).toBe('this session')
|
|
700
|
+
await $.command.run({ command: 'clear', args: '' })
|
|
701
|
+
await settle(r)
|
|
702
|
+
expect(await ui.find({ key: 'self-208' })).toBeUndefined()
|
|
703
|
+
expect(await ui.find({ key: 'mark-208' })).toBeUndefined()
|
|
704
|
+
})
|
|
705
|
+
|
|
492
706
|
test(`unverified tickets are counted in the plural [${surface}]`, async ($, on) => {
|
|
493
707
|
const r = rig(on, $)
|
|
494
708
|
r.tickets = [issue(1, 'a', ['trailhead:unverified']), issue(2, 'b', ['trailhead:unverified']), issue(3, 'c', ['trailhead:build'])]
|
|
@@ -522,7 +736,7 @@ for (const surface of SURFACES) {
|
|
|
522
736
|
await completeTurn($)
|
|
523
737
|
await settle(r)
|
|
524
738
|
// the open maps are still read, so one can be picked; no map is
|
|
525
|
-
expect(r.calls).toEqual([
|
|
739
|
+
expect(r.calls).toEqual([MAPS_CALL])
|
|
526
740
|
})
|
|
527
741
|
|
|
528
742
|
test(`first fetch in flight: the map number and Loading tickets [${surface}]`, async ($, on) => {
|
|
@@ -754,7 +968,7 @@ for (const surface of SURFACES) {
|
|
|
754
968
|
await $.session.start(START)
|
|
755
969
|
const ui = await openAndMount($, r)
|
|
756
970
|
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'])
|
|
971
|
+
expect(buttons).toEqual(['open-whiteboard', 'map-14', 'work-209', 'work-210', 'work-214', 'refresh', 'close'])
|
|
758
972
|
expect((await ui.find({ key: 'work-209' }))?.props).toMatchObject({ label: 'work', variant: 'primary' })
|
|
759
973
|
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
760
974
|
|
|
@@ -876,7 +1090,7 @@ for (const surface of SURFACES) {
|
|
|
876
1090
|
await settle(r)
|
|
877
1091
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
878
1092
|
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
879
|
-
expect(await textOf(ui, 'map-aside-14')).toBe(' active')
|
|
1093
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5 · active')
|
|
880
1094
|
})
|
|
881
1095
|
|
|
882
1096
|
test(`a manual /clear keeps the pane filled, with no turn and inside the floor [${surface}]`, async ($, on) => {
|
|
@@ -1142,7 +1356,7 @@ for (const surface of SURFACES) {
|
|
|
1142
1356
|
expect(await keysOf(ui, 'map-row-')).toEqual(['map-row-14', 'map-row-198'])
|
|
1143
1357
|
expect(await textOf(ui, 'map-mark-198')).toBe('● ')
|
|
1144
1358
|
expect((await ui.find({ key: 'map-mark-198' }))?.children[0]).toMatchObject({ props: { color: ACCENT } })
|
|
1145
|
-
expect(await textOf(ui, 'map-aside-198')).toBe(' active')
|
|
1359
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
1146
1360
|
expect(await textOf(ui, 'map-label-198')).toBe('#198 trailhead mod')
|
|
1147
1361
|
expect((await ui.find({ key: 'map-label-198' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
1148
1362
|
expect(await textOf(ui, 'map-mark-14')).toBe(' ')
|
|
@@ -1162,7 +1376,7 @@ for (const surface of SURFACES) {
|
|
|
1162
1376
|
const ui = await openAndMount($, r)
|
|
1163
1377
|
expect(await hasKey(ui, 'head-maps')).toBe(false)
|
|
1164
1378
|
expect(await hasKey(ui, 'group-maps')).toBe(false)
|
|
1165
|
-
expect((await ui.findAll({ type: 'Button' })).map((b: any) => b.key)).toEqual(['work-209', 'work-210', 'work-214', 'refresh', 'close'])
|
|
1379
|
+
expect((await ui.findAll({ type: 'Button' })).map((b: any) => b.key)).toEqual(['open-whiteboard', 'work-209', 'work-210', 'work-214', 'refresh', 'close'])
|
|
1166
1380
|
})
|
|
1167
1381
|
|
|
1168
1382
|
test(`a press on another map shows its tickets, the active-map marker is never written [${surface}]`, async ($, on) => {
|
|
@@ -1179,10 +1393,11 @@ for (const surface of SURFACES) {
|
|
|
1179
1393
|
expect(await textOf(ui, 'burndown-counts')).toBe('open 2 · done 0 · removed 0 · peak 2')
|
|
1180
1394
|
// the ticket in play belongs to the other map: nothing here is marked
|
|
1181
1395
|
expect(await hasKey(ui, 'mark-208')).toBe(false)
|
|
1182
|
-
expect(r.calls.slice(-
|
|
1396
|
+
expect(r.calls.slice(-5).map(argv => argv.slice(0, 4))).toEqual([
|
|
1183
1397
|
['gh', 'issue', 'view', '14'],
|
|
1184
1398
|
['gh', 'issue', 'list', '--label'],
|
|
1185
1399
|
['gh', 'issue', 'list', '--label'],
|
|
1400
|
+
['gh', 'api', 'graphql', '-F'],
|
|
1186
1401
|
['gh', 'issue', 'list', '--label'],
|
|
1187
1402
|
])
|
|
1188
1403
|
expect(r.calls.some(argv => argv.includes('trailhead:map-14'))).toBe(true)
|
|
@@ -1236,7 +1451,7 @@ for (const surface of SURFACES) {
|
|
|
1236
1451
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
1237
1452
|
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
1238
1453
|
expect(await textOf(ui, 'map-label-14')).toBe('#14 older map')
|
|
1239
|
-
expect(await textOf(ui, 'map-aside-14')).toBe(' active')
|
|
1454
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5 · active')
|
|
1240
1455
|
expect((await ui.find({ key: 'map-198' }))?.props).toMatchObject({ label: '#198 trailhead mod' })
|
|
1241
1456
|
})
|
|
1242
1457
|
|
|
@@ -1258,18 +1473,18 @@ for (const surface of SURFACES) {
|
|
|
1258
1473
|
await settle(r)
|
|
1259
1474
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
1260
1475
|
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
1261
|
-
expect(await textOf(ui, 'map-aside-14')).toBe(' active')
|
|
1262
|
-
expect(await
|
|
1476
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5 · active')
|
|
1477
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23')
|
|
1263
1478
|
})
|
|
1264
1479
|
|
|
1265
|
-
test(`
|
|
1480
|
+
test(`both maps reads failing keep the last list and set no footer error [${surface}]`, async ($, on) => {
|
|
1266
1481
|
const r = rig(on, $)
|
|
1267
1482
|
await $.session.start(START)
|
|
1268
1483
|
const ui = await openAndMount($, r)
|
|
1269
1484
|
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
1270
1485
|
|
|
1271
1486
|
const base: Gh = argv => withHistory(r, ok(JSON.stringify(r.history)))(argv)
|
|
1272
|
-
r.gh = argv => (argv
|
|
1487
|
+
r.gh = argv => (isGraphql(argv) || isMapsList(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
1273
1488
|
r.tickets = [issue(300, 'fresh one', ['trailhead:build'])]
|
|
1274
1489
|
await r.clock.advance(FLOOR)
|
|
1275
1490
|
await completeTurn($)
|
|
@@ -1279,7 +1494,7 @@ for (const surface of SURFACES) {
|
|
|
1279
1494
|
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
1280
1495
|
|
|
1281
1496
|
// unreadable output is the same
|
|
1282
|
-
r.gh = argv => (argv
|
|
1497
|
+
r.gh = argv => (isGraphql(argv) || isMapsList(argv) ? ok('not json') : base(argv))
|
|
1283
1498
|
await r.clock.advance(FLOOR)
|
|
1284
1499
|
await completeTurn($)
|
|
1285
1500
|
await settle(r)
|
|
@@ -1371,4 +1586,336 @@ for (const surface of SURFACES) {
|
|
|
1371
1586
|
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
1372
1587
|
expect(r.writes).toEqual([])
|
|
1373
1588
|
})
|
|
1589
|
+
|
|
1590
|
+
const pressDashboard = async (ui: any, r: Rig) => {
|
|
1591
|
+
await ui.press({ key: 'open-whiteboard' })
|
|
1592
|
+
await settle(r)
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
test(`the Whiteboard row shows the maps index and the whiteboard tickets [${surface}]`, async ($, on) => {
|
|
1596
|
+
const r = rig(on, $)
|
|
1597
|
+
await $.session.start(START)
|
|
1598
|
+
const ui = await openAndMount($, r)
|
|
1599
|
+
expect((await ui.find({ key: 'open-whiteboard' }))?.props).toMatchObject({ label: 'Whiteboard' })
|
|
1600
|
+
const before = r.calls.length
|
|
1601
|
+
await pressDashboard(ui, r)
|
|
1602
|
+
|
|
1603
|
+
expect(r.calls.slice(before)).toEqual([
|
|
1604
|
+
['gh', 'issue', 'list', '--label', 'trailhead:ticket', '--label', 'trailhead:whiteboard', '--state', 'open', '--limit', '200', '--json', 'number,title,labels,assignees,url'],
|
|
1605
|
+
['gh', 'issue', 'list', '--label', 'trailhead:dashboard', '--state', 'open', '--limit', '10', '--json', 'number,url'],
|
|
1606
|
+
MAPS_CALL,
|
|
1607
|
+
uatCall('trailhead:whiteboard'),
|
|
1608
|
+
])
|
|
1609
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
1610
|
+
expect((await ui.find({ key: 'dashboard-title' }))?.children[0]).toMatchObject({ props: { bold: true, color: ACCENT } })
|
|
1611
|
+
expect((await ui.findAll({ type: 'Link' })).map((l: any) => l.props.href)).toContain(`${URL}/5`)
|
|
1612
|
+
expect((await ui.find({ key: 'to-map' }))?.props).toMatchObject({ label: 'map' })
|
|
1613
|
+
expect(await hasKey(ui, 'dashboard')).toBe(false)
|
|
1614
|
+
// the view is the whiteboard: its row is not drawn again
|
|
1615
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(false)
|
|
1616
|
+
|
|
1617
|
+
// every open map is a button, the active one marked
|
|
1618
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
1619
|
+
expect((await ui.find({ key: 'map-198' }))?.props).toMatchObject({ label: '#198 trailhead mod' })
|
|
1620
|
+
expect(await hasKey(ui, 'map-14')).toBe(true)
|
|
1621
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
1622
|
+
|
|
1623
|
+
// the whiteboard by state, the quarantined one only counted, no map tickets
|
|
1624
|
+
expect(await hasKey(ui, 'whiteboard')).toBe(true)
|
|
1625
|
+
expect(await textOf(ui, 'head-whiteboard')).toBe('Whiteboard · 3 open · 1 assigned')
|
|
1626
|
+
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 1')
|
|
1627
|
+
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 1')
|
|
1628
|
+
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 1')
|
|
1629
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-303'])
|
|
1630
|
+
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
1631
|
+
expect(await hasKey(ui, 'work-301')).toBe(true)
|
|
1632
|
+
expect(await textOf(ui, 'unverified')).toBe('1 unverified ticket hidden (quarantined)')
|
|
1633
|
+
expect(await hasKey(ui, 'burndown-section')).toBe(false)
|
|
1634
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
1635
|
+
expect(r.writes).toEqual([])
|
|
1636
|
+
})
|
|
1637
|
+
|
|
1638
|
+
test(`a Whiteboard row above the maps list carries the counts, the header has no dashboard button [${surface}]`, async ($, on) => {
|
|
1639
|
+
const r = rig(on, $)
|
|
1640
|
+
r.maps = [MAPS[0]]
|
|
1641
|
+
await $.session.start(START)
|
|
1642
|
+
const ui = await openAndMount($, r)
|
|
1643
|
+
// one map open: no maps group, the row is still there, without a per-ticket query
|
|
1644
|
+
expect(await hasKey(ui, 'group-maps')).toBe(false)
|
|
1645
|
+
expect((await ui.find({ key: 'open-whiteboard' }))?.props).toMatchObject({ label: 'Whiteboard' })
|
|
1646
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
1647
|
+
expect((await ui.find({ key: 'whiteboard-aside' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
1648
|
+
expect(await hasKey(ui, 'dashboard')).toBe(false)
|
|
1649
|
+
expect(r.calls.filter(argv => argv.includes('trailhead:whiteboard'))).toEqual([])
|
|
1650
|
+
expect(r.calls.filter(isGraphql)).toEqual([MAPS_CALL])
|
|
1651
|
+
// above the maps list and the tickets, below the heading
|
|
1652
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
1653
|
+
expect(keys.indexOf('whiteboard-row')).toBeGreaterThan(keys.indexOf('header'))
|
|
1654
|
+
expect(keys.indexOf('whiteboard-row')).toBeLessThan(keys.indexOf('group-frontier'))
|
|
1655
|
+
})
|
|
1656
|
+
|
|
1657
|
+
test(`the Whiteboard row sits above the Maps list [${surface}]`, async ($, on) => {
|
|
1658
|
+
const r = rig(on, $)
|
|
1659
|
+
await $.session.start(START)
|
|
1660
|
+
const ui = await openAndMount($, r)
|
|
1661
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
1662
|
+
expect(keys.indexOf('whiteboard-row')).toBeLessThan(keys.indexOf('group-maps'))
|
|
1663
|
+
expect(keys.indexOf('whiteboard-row')).toBeGreaterThan(keys.indexOf('header'))
|
|
1664
|
+
})
|
|
1665
|
+
|
|
1666
|
+
test(`a press on the Whiteboard row shows the whiteboard, the map button goes back [${surface}]`, async ($, on) => {
|
|
1667
|
+
const r = rig(on, $)
|
|
1668
|
+
r.maps = [MAPS[0]]
|
|
1669
|
+
await $.session.start(START)
|
|
1670
|
+
const ui = await openAndMount($, r)
|
|
1671
|
+
await pressDashboard(ui, r)
|
|
1672
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
1673
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(false)
|
|
1674
|
+
await ui.press({ key: 'to-map' })
|
|
1675
|
+
await settle(r)
|
|
1676
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
1677
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
test(`the whiteboard counts are drawn as read, zero included, and follow the next read [${surface}]`, async ($, on) => {
|
|
1681
|
+
const r = rig(on, $)
|
|
1682
|
+
r.whiteboardCounts = { open: 0, total: 0 }
|
|
1683
|
+
await $.session.start(START)
|
|
1684
|
+
const ui = await openAndMount($, r)
|
|
1685
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 0 open / 0')
|
|
1686
|
+
r.whiteboardCounts = { open: 4, total: 11 }
|
|
1687
|
+
await r.clock.advance(FLOOR)
|
|
1688
|
+
await completeTurn($)
|
|
1689
|
+
await settle(r)
|
|
1690
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 4 open / 11')
|
|
1691
|
+
})
|
|
1692
|
+
|
|
1693
|
+
test(`a failed counts read shows the row name-only, never an error [${surface}]`, async ($, on) => {
|
|
1694
|
+
const r = rig(on, $)
|
|
1695
|
+
const base = r.defaultGh
|
|
1696
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
1697
|
+
await $.session.start(START)
|
|
1698
|
+
const ui = await openAndMount($, r)
|
|
1699
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
1700
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
1701
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
1702
|
+
|
|
1703
|
+
// counts that were read once are not kept through a failed read
|
|
1704
|
+
r.gh = null
|
|
1705
|
+
await r.clock.advance(FLOOR)
|
|
1706
|
+
await completeTurn($)
|
|
1707
|
+
await settle(r)
|
|
1708
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
1709
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
1710
|
+
await r.clock.advance(FLOOR)
|
|
1711
|
+
await completeTurn($)
|
|
1712
|
+
await settle(r)
|
|
1713
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
1714
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
test(`both maps reads failing keep the maps list but draw the row name-only [${surface}]`, async ($, on) => {
|
|
1718
|
+
const r = rig(on, $)
|
|
1719
|
+
await $.session.start(START)
|
|
1720
|
+
const ui = await openAndMount($, r)
|
|
1721
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
1722
|
+
const base: Gh = argv => withHistory(r, ok(JSON.stringify(r.history)))(argv)
|
|
1723
|
+
r.gh = argv => (isGraphql(argv) || isMapsList(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
1724
|
+
await r.clock.advance(FLOOR)
|
|
1725
|
+
await completeTurn($)
|
|
1726
|
+
await settle(r)
|
|
1727
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
1728
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
1729
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
1730
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
1731
|
+
})
|
|
1732
|
+
|
|
1733
|
+
test(`unreadable whiteboard fields leave the maps with counts and the row name-only [${surface}]`, async ($, on) => {
|
|
1734
|
+
const r = rig(on, $)
|
|
1735
|
+
r.whiteboardCounts = null
|
|
1736
|
+
await $.session.start(START)
|
|
1737
|
+
const ui = await openAndMount($, r)
|
|
1738
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
1739
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
1740
|
+
expect(await hasKey(ui, 'whiteboard-aside')).toBe(false)
|
|
1741
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
1742
|
+
})
|
|
1743
|
+
|
|
1744
|
+
test(`with no active map the Whiteboard row sits above the maps [${surface}]`, async ($, on) => {
|
|
1745
|
+
const none = rig(on, $)
|
|
1746
|
+
none.active = null
|
|
1747
|
+
await $.session.start(START)
|
|
1748
|
+
const ui = await openAndMount($, none)
|
|
1749
|
+
await completeTurn($)
|
|
1750
|
+
await settle(none)
|
|
1751
|
+
expect(await hasKey(ui, 'dashboard')).toBe(false)
|
|
1752
|
+
expect(await textOf(ui, 'whiteboard-aside')).toBe(' 3 open / 9')
|
|
1753
|
+
const keys = (await ui.findAll({ type: 'Box' })).map((el: any) => el.key)
|
|
1754
|
+
expect(keys.indexOf('whiteboard-row')).toBeLessThan(keys.indexOf('group-maps'))
|
|
1755
|
+
})
|
|
1756
|
+
|
|
1757
|
+
test(`the Whiteboard row stays in the loading view of a map [${surface}]`, async ($, on) => {
|
|
1758
|
+
const r = rig(on, $)
|
|
1759
|
+
let release: () => void = () => undefined
|
|
1760
|
+
const held = new Promise<void>(resolve => (release = resolve))
|
|
1761
|
+
const base = r.defaultGh
|
|
1762
|
+
r.gh = async argv => {
|
|
1763
|
+
if (argv[2] === 'view') await held
|
|
1764
|
+
return base(argv)
|
|
1765
|
+
}
|
|
1766
|
+
await $.session.start(START)
|
|
1767
|
+
r.open()
|
|
1768
|
+
const ui = await mountPane($)
|
|
1769
|
+
await settle(r)
|
|
1770
|
+
expect(await textOf(ui, 'loading')).toBe('Loading tickets…')
|
|
1771
|
+
expect(await hasKey(ui, 'open-whiteboard')).toBe(true)
|
|
1772
|
+
release()
|
|
1773
|
+
await settle(r)
|
|
1774
|
+
})
|
|
1775
|
+
|
|
1776
|
+
test(`the maps list shows each map's open and total from one call [${surface}]`, async ($, on) => {
|
|
1777
|
+
const r = rig(on, $)
|
|
1778
|
+
await $.session.start(START)
|
|
1779
|
+
const ui = await openAndMount($, r)
|
|
1780
|
+
expect(r.calls.filter(argv => argv.includes('trailhead:map') || isGraphql(argv))).toEqual([MAPS_CALL])
|
|
1781
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
1782
|
+
expect(await textOf(ui, 'map-aside-14')).toBe(' 3 open / 5')
|
|
1783
|
+
// a map the read gave no summary for is drawn name-only
|
|
1784
|
+
r.mapSummary = { 198: { total: 23, completed: 16 } }
|
|
1785
|
+
await r.clock.advance(FLOOR)
|
|
1786
|
+
await completeTurn($)
|
|
1787
|
+
await settle(r)
|
|
1788
|
+
expect(await hasKey(ui, 'map-aside-14')).toBe(false)
|
|
1789
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
1790
|
+
})
|
|
1791
|
+
|
|
1792
|
+
test(`a failing counts read falls back to the name-only list with no footer error [${surface}]`, async ($, on) => {
|
|
1793
|
+
const r = rig(on, $)
|
|
1794
|
+
const base = r.defaultGh
|
|
1795
|
+
r.gh = argv => (isGraphql(argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : base(argv))
|
|
1796
|
+
await $.session.start(START)
|
|
1797
|
+
const ui = await openAndMount($, r)
|
|
1798
|
+
expect(r.calls.filter(argv => argv.includes('trailhead:map') || isGraphql(argv))).toEqual([MAPS_CALL, MAPS_LIST_CALL])
|
|
1799
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
1800
|
+
expect(await textOf(ui, 'map-aside-198')).toBe(' active')
|
|
1801
|
+
expect(await hasKey(ui, 'map-aside-14')).toBe(false)
|
|
1802
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
1803
|
+
|
|
1804
|
+
// a reply with errors is a failed read too
|
|
1805
|
+
r.gh = argv => (isGraphql(argv) ? ok(JSON.stringify({ data: { repository: { issues: { nodes: [] } } }, errors: [{ message: 'x' }] })) : base(argv))
|
|
1806
|
+
await r.clock.advance(FLOOR)
|
|
1807
|
+
await completeTurn($)
|
|
1808
|
+
await settle(r)
|
|
1809
|
+
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
1810
|
+
expect(await hasKey(ui, 'map-aside-14')).toBe(false)
|
|
1811
|
+
})
|
|
1812
|
+
|
|
1813
|
+
test(`the dashboard heading counts the whiteboard and every assigned trusted ticket is a row [${surface}]`, async ($, on) => {
|
|
1814
|
+
const r = rig(on, $)
|
|
1815
|
+
r.whiteboard = [
|
|
1816
|
+
...WHITEBOARD,
|
|
1817
|
+
issue(305, 'Assigned but quarantined', [...WB, 'trailhead:task', 'trailhead:unverified'], ['dave']),
|
|
1818
|
+
issue(306, 'Paused with a holder', [...WB, 'trailhead:task', 'trailhead:paused'], ['erin']),
|
|
1819
|
+
]
|
|
1820
|
+
await $.session.start(START)
|
|
1821
|
+
const ui = await openAndMount($, r)
|
|
1822
|
+
await pressDashboard(ui, r)
|
|
1823
|
+
expect(await textOf(ui, 'head-whiteboard')).toBe('Whiteboard · 4 open · 2 assigned')
|
|
1824
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-306', 'row-303'])
|
|
1825
|
+
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
1826
|
+
expect(await textOf(ui, 'holder-306')).toBe('@erin')
|
|
1827
|
+
expect(await hasKey(ui, 'row-305')).toBe(false)
|
|
1828
|
+
expect(await textOf(ui, 'unverified')).toBe('2 unverified tickets hidden (quarantined)')
|
|
1829
|
+
})
|
|
1830
|
+
|
|
1831
|
+
test(`the map button goes back to the map shown before [${surface}]`, async ($, on) => {
|
|
1832
|
+
const r = rig(on, $)
|
|
1833
|
+
await $.session.start(START)
|
|
1834
|
+
const ui = await openAndMount($, r)
|
|
1835
|
+
await pressMap(ui, r, 14)
|
|
1836
|
+
await pressDashboard(ui, r)
|
|
1837
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
1838
|
+
|
|
1839
|
+
await ui.press({ key: 'to-map' })
|
|
1840
|
+
await settle(r)
|
|
1841
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
1842
|
+
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
1843
|
+
expect(await hasKey(ui, 'dashboard-title')).toBe(false)
|
|
1844
|
+
})
|
|
1845
|
+
|
|
1846
|
+
test(`a map pressed on the dashboard shows that map [${surface}]`, async ($, on) => {
|
|
1847
|
+
const r = rig(on, $)
|
|
1848
|
+
await $.session.start(START)
|
|
1849
|
+
const ui = await openAndMount($, r)
|
|
1850
|
+
await pressDashboard(ui, r)
|
|
1851
|
+
await pressMap(ui, r, 14)
|
|
1852
|
+
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
1853
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-140', 'row-141'])
|
|
1854
|
+
|
|
1855
|
+
await pressDashboard(ui, r)
|
|
1856
|
+
await pressMap(ui, r, 198)
|
|
1857
|
+
expect(await textOf(ui, 'map')).toBe('#198 trailhead mod')
|
|
1858
|
+
expect(await hasKey(ui, 'viewing')).toBe(false)
|
|
1859
|
+
})
|
|
1860
|
+
|
|
1861
|
+
test(`a work press on a whiteboard ticket clears and runs the work verb [${surface}]`, async ($, on) => {
|
|
1862
|
+
const r = rig(on, $)
|
|
1863
|
+
await $.session.start(START)
|
|
1864
|
+
const ui = await openAndMount($, r)
|
|
1865
|
+
await pressDashboard(ui, r)
|
|
1866
|
+
await ui.press({ key: 'work-301' })
|
|
1867
|
+
await settle(r)
|
|
1868
|
+
expect(r.runs).toEqual([
|
|
1869
|
+
{ command: 'clear', args: '' },
|
|
1870
|
+
{ command: 'trailhead:work', args: '301' },
|
|
1871
|
+
])
|
|
1872
|
+
// the dashboard stays shown across the reset
|
|
1873
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
1874
|
+
expect(await keysOf(ui, 'row-')).toContain('row-301')
|
|
1875
|
+
})
|
|
1876
|
+
|
|
1877
|
+
test(`an empty whiteboard says so, a failed read shows the reason [${surface}]`, async ($, on) => {
|
|
1878
|
+
const r = rig(on, $)
|
|
1879
|
+
r.whiteboard = []
|
|
1880
|
+
r.dashboard = []
|
|
1881
|
+
await $.session.start(START)
|
|
1882
|
+
const ui = await openAndMount($, r)
|
|
1883
|
+
await pressDashboard(ui, r)
|
|
1884
|
+
expect(await textOf(ui, 'no-tickets')).toBe('No open whiteboard tickets.')
|
|
1885
|
+
// no pinned index yet: the title is plain text
|
|
1886
|
+
expect((await ui.findAll({ type: 'Link' })).map((l: any) => l.props.href)).not.toContain(`${URL}/5`)
|
|
1887
|
+
|
|
1888
|
+
r.whiteboard = WHITEBOARD
|
|
1889
|
+
r.gh = argv => (argv.includes('trailhead:whiteboard') ? { exitCode: 1, stdout: '', stderr: 'boom' } : r.defaultGh(argv))
|
|
1890
|
+
await r.clock.advance(FLOOR)
|
|
1891
|
+
await ui.press({ key: 'refresh' })
|
|
1892
|
+
await settle(r)
|
|
1893
|
+
expect(await textOf(ui, 'age')).toBe('⚠ gh failed (exit 1) · list from 15s ago')
|
|
1894
|
+
// a list that may be stale says nothing about being empty
|
|
1895
|
+
expect(await hasKey(ui, 'no-tickets')).toBe(false)
|
|
1896
|
+
})
|
|
1897
|
+
|
|
1898
|
+
test(`with no active map the dashboard is one press away [${surface}]`, async ($, on) => {
|
|
1899
|
+
const r = rig(on, $)
|
|
1900
|
+
r.active = null
|
|
1901
|
+
await $.session.start(START)
|
|
1902
|
+
const ui = await openAndMount($, r)
|
|
1903
|
+
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
1904
|
+
await pressDashboard(ui, r)
|
|
1905
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-303'])
|
|
1906
|
+
await ui.press({ key: 'to-map' })
|
|
1907
|
+
await settle(r)
|
|
1908
|
+
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
1909
|
+
})
|
|
1910
|
+
|
|
1911
|
+
test(`the dashboard stays shown through /clear [${surface}]`, async ($, on) => {
|
|
1912
|
+
const r = rig(on, $)
|
|
1913
|
+
await $.session.start(START)
|
|
1914
|
+
const ui = await openAndMount($, r)
|
|
1915
|
+
await pressDashboard(ui, r)
|
|
1916
|
+
await $.command.run({ command: 'clear', args: '' } as never)
|
|
1917
|
+
await settle(r)
|
|
1918
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
1919
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-301', 'row-302', 'row-303'])
|
|
1920
|
+
})
|
|
1374
1921
|
}
|