@marcomigozzi/trailhead 0.16.1 → 0.18.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 +82 -25
- package/README.md +82 -25
- package/bin/base-preflight-docs.test.js +4 -1
- package/bin/bulk-cross-reference-docs.test.js +36 -0
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/engine-route-docs.test.js +155 -0
- package/bin/facet-stamp-docs.test.js +79 -0
- package/bin/forced-mutation-docs.test.js +38 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/guard-axis-docs.test.js +69 -0
- package/bin/inbox-needs-info-docs.test.js +68 -0
- package/bin/lib/codex-projection.js +28 -19
- package/bin/lib/codex-projection.test.js +77 -20
- package/bin/lib/host-descriptor.js +1 -1
- package/bin/lib/host-descriptor.test.js +6 -0
- package/bin/needs-you-docs.test.js +9 -2
- package/bin/options-in-chat-docs.test.js +65 -0
- package/bin/pick-stress-test-docs.test.js +1 -1
- package/bin/pipeline-review-docs.test.js +118 -0
- package/bin/plan-review-budget-docs.test.js +63 -0
- package/bin/repo-target-docs.test.js +41 -0
- package/bin/report-status-docs.test.js +10 -2
- package/bin/seam-coverage-docs.test.js +46 -0
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/standards-brief-docs.test.js +112 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/test-once-docs.test.js +101 -0
- package/bin/test-run-docs.test.js +47 -0
- package/bin/trailhead.js +15 -8
- package/bin/trailhead.test.js +71 -17
- package/bin/unblock-docs.test.js +56 -0
- package/bin/yes-accepts-recommended-docs.test.js +54 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-plan.md +1 -1
- package/plugins/trailhead/agents/trailhead-verify.md +1 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
- package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
- package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
- package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
- package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
- package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
- package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
- package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
- package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
- package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
- package/plugins/trailhead/hooks/mod/README.md +42 -17
- package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
- package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
- package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
- package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
- package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
- package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
- package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
- package/plugins/trailhead/skills/_shared/substrate.md +4 -3
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
- package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques.md +8 -6
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- 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 +11 -6
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
- package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
- package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
- package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
- package/plugins/trailhead/templates/trailhead-capture.js +419 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
- package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
- package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
- package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
- package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
- package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
- package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
- package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
- package/plugins/trailhead/tests/git-env.test.ts +20 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
- package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
- package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
- package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/tests/unscrub.ts +8 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
- package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
- package/plugins/trailhead/types/index.d.ts +7 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { On } from 'claude-code'
|
|
2
2
|
import { expect, mock, test } from 'claude-code/testing'
|
|
3
|
+
import { unscrub } from './unscrub'
|
|
3
4
|
|
|
4
5
|
const SURFACES = ['terminal', 'desktop'] as const
|
|
5
6
|
const SESSION = 'sess-A'
|
|
@@ -15,13 +16,15 @@ const ROOT = '/repo'
|
|
|
15
16
|
const ROOT_OTHER = '/other'
|
|
16
17
|
// A linked worktree of ROOT: the repo's root is the main tree's, the checkout's own is this.
|
|
17
18
|
const ROOT_TREE = '/repo-t5'
|
|
18
|
-
// The checkout handed to a verb: base64url of the root,
|
|
19
|
+
// The checkout handed to a verb: base64url of the root, a `.`, then base64url of the pinned repo (`host/owner/name`),
|
|
20
|
+
// computed here by hand so the pane's own encoder is checked.
|
|
19
21
|
// The helper's decodeIn cannot be imported here (the CommonJS template does not load in this suite); the node suite
|
|
20
22
|
// checks decodeIn against Buffer's base64url, and these tests pin the same alphabet.
|
|
21
|
-
const
|
|
22
|
-
const b64 = btoa(Array.from(new TextEncoder().encode(
|
|
23
|
-
return
|
|
23
|
+
const b64url = (text: string) => {
|
|
24
|
+
const b64 = btoa(Array.from(new TextEncoder().encode(text), byte => String.fromCharCode(byte)).join(''))
|
|
25
|
+
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
24
26
|
}
|
|
27
|
+
const IN = (root: string) => `--in=${b64url(root)}.${b64url('github.com/o/r')}`
|
|
25
28
|
|
|
26
29
|
const MAP = {
|
|
27
30
|
title: 'trailhead mod',
|
|
@@ -101,6 +104,9 @@ const ticketsCall = (label: string, after?: string) => [
|
|
|
101
104
|
const ticketsListCall = (label: string) => ['gh', 'issue', 'list', '--label', label, '--state', 'open', '--limit', '200', '--json', 'number,title,labels,assignees,url']
|
|
102
105
|
const isTicketsRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.includes(`query=${TICKETS_QUERY}`)
|
|
103
106
|
// The maps read only: the ticket reads are GraphQL too, but answered and failed on their own.
|
|
107
|
+
// The login and my-tickets reads (any map, the whiteboard): answered apart from the pane's own reads, recorded in `mineCalls`.
|
|
108
|
+
const isLoginRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.includes('query=query{viewer{login}}')
|
|
109
|
+
const isMineRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.some(a => a.startsWith('login='))
|
|
104
110
|
const isGraphql = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && !isTicketsRead(argv) && !isUatRead(argv) && !isCountRead(argv)
|
|
105
111
|
// The open-ticket total: one aliased `totalCount` per open map (`m<n>`) plus the whiteboard (`wb`), in one call.
|
|
106
112
|
const countQuery = (maps: number[]) =>
|
|
@@ -137,7 +143,7 @@ const stateText = (over: Record<string, unknown> = {}): string =>
|
|
|
137
143
|
})
|
|
138
144
|
|
|
139
145
|
// The plugin's atoms, which the engine resets on /clear.
|
|
140
|
-
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'isDashboard', 'dashboardPane', 'pendingWork', 'pendingPhase', 'paneAutoOpened', 'ownTicket', 'repoPin']
|
|
146
|
+
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'isDashboard', 'dashboardPane', 'pendingWork', 'pendingPhase', 'paneAutoOpened', 'ownTicket', 'repoPin', 'myTickets']
|
|
141
147
|
|
|
142
148
|
const PANE_PROPS = {
|
|
143
149
|
title: 'Map',
|
|
@@ -179,6 +185,9 @@ const rig = (on: On, engine?: any) => {
|
|
|
179
185
|
// The UAT read's answer: one row per open UAT ticket, `{ number, body, labels }` (the scope label rides
|
|
180
186
|
// in `labels`); null makes the read fail. `uatErrors` answers a reply with `errors`, `uatBroken` non-JSON.
|
|
181
187
|
uat: [] as unknown[] | null,
|
|
188
|
+
// My open tickets on any map or the whiteboard (the `issue()` shape); null makes the read fail.
|
|
189
|
+
mine: [] as unknown[] | null,
|
|
190
|
+
mineCalls: [] as (readonly string[])[],
|
|
182
191
|
uatErrors: false,
|
|
183
192
|
uatBroken: false,
|
|
184
193
|
dashboard: DASHBOARD as unknown[],
|
|
@@ -209,6 +218,8 @@ const rig = (on: On, engine?: any) => {
|
|
|
209
218
|
calls: [] as (readonly string[])[],
|
|
210
219
|
panes: [] as { id: string; title: string; isShown: boolean; isFocused: boolean; isPlaced: boolean }[],
|
|
211
220
|
isPlaced: true,
|
|
221
|
+
// Why `ui.open` leaves the pane unplaced (the SDK's `reason`).
|
|
222
|
+
reason: 'terminal too narrow',
|
|
212
223
|
logs: [] as string[],
|
|
213
224
|
toasts: [] as string[],
|
|
214
225
|
// What a Bash call prints on stdout (the created issue's URL for a `gh issue create`).
|
|
@@ -373,15 +384,18 @@ const rig = (on: On, engine?: any) => {
|
|
|
373
384
|
return { value: undefined }
|
|
374
385
|
})
|
|
375
386
|
on('session.repo', () => (r.repoFails ? { deny: 'repo read failed' } : { value: r.repo }))
|
|
387
|
+
// A checkout in `gone` no longer exists on disk; nothing else is listed beside it.
|
|
388
|
+
on('fs.exists', (_$, e) => ({ value: !r.gone.includes(e.path) }))
|
|
389
|
+
on('fs.list', () => ({ value: [] }))
|
|
376
390
|
on('session.root', () => ({ value: r.sessionRoot }))
|
|
377
391
|
on('session.cwd', () => ({ value: r.cwd }))
|
|
378
392
|
on('process.run', async (_$, e) => {
|
|
379
393
|
// The pin's own `git rev-parse --show-toplevel`: answered apart, never counted as a `gh` call.
|
|
380
|
-
if (e.argv[0] === 'git') {
|
|
381
|
-
r.gitCalls.push({ argv: [...e.argv], cwd: e.init?.cwd })
|
|
394
|
+
if (unscrub(e.argv)[0] === 'git') {
|
|
395
|
+
r.gitCalls.push({ argv: [...unscrub(e.argv)], cwd: e.init?.cwd })
|
|
382
396
|
// Answered per directory, like git: the worktree gives `top`, the other checkouts their own root.
|
|
383
397
|
const where = e.init?.cwd
|
|
384
|
-
if (e.argv[1] === 'remote') {
|
|
398
|
+
if (unscrub(e.argv)[1] === 'remote') {
|
|
385
399
|
const remote = r.gitRemote === null ? { exitCode: 2, stdout: '', stderr: '' } : { exitCode: 0, stdout: `${r.gitRemote}\n`, stderr: '' }
|
|
386
400
|
return { value: { ...remote, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
387
401
|
}
|
|
@@ -390,21 +404,31 @@ const rig = (on: On, engine?: any) => {
|
|
|
390
404
|
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
391
405
|
}
|
|
392
406
|
// The pin's host check: answered apart, never counted as a read of the pane.
|
|
393
|
-
if (e.argv[1] === 'auth') {
|
|
394
|
-
const host = e.argv[4] ?? ''
|
|
407
|
+
if (unscrub(e.argv)[1] === 'auth') {
|
|
408
|
+
const host = unscrub(e.argv)[4] ?? ''
|
|
395
409
|
r.authCalls.push(host)
|
|
396
410
|
const out = r.authHosts.includes(host) ? { exitCode: 0, stdout: '', stderr: '' } : { exitCode: 1, stdout: '', stderr: `You are not logged into any accounts on ${host}` }
|
|
397
411
|
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
398
412
|
}
|
|
399
|
-
|
|
413
|
+
if (isLoginRead(unscrub(e.argv)) || isMineRead(unscrub(e.argv))) {
|
|
414
|
+
r.mineCalls.push([...unscrub(e.argv)])
|
|
415
|
+
const body = isLoginRead(unscrub(e.argv))
|
|
416
|
+
? { data: { viewer: { login: 'me' } } }
|
|
417
|
+
: { data: { repository: { issues: { nodes: (r.mine ?? []).map(flatNode), pageInfo: { hasNextPage: false, endCursor: null } } } } }
|
|
418
|
+
const out = r.mine === null && isMineRead(unscrub(e.argv)) ? { exitCode: 1, stdout: '', stderr: 'boom' } : { exitCode: 0, stdout: JSON.stringify(body), stderr: '' }
|
|
419
|
+
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
420
|
+
}
|
|
421
|
+
r.calls.push([...unscrub(e.argv)])
|
|
400
422
|
r.cwds.push(e.init?.cwd)
|
|
401
423
|
// `gh` needs a repo around it: without its own cwd it uses the session's, wherever a `cd` left it.
|
|
402
424
|
const where = e.init?.cwd ?? r.cwd
|
|
425
|
+
// A spawn in a deleted directory fails the way a missing binary does.
|
|
426
|
+
if (r.gone.includes(where)) return { deny: 'spawn gh ENOENT' }
|
|
403
427
|
if (where !== ROOT && where !== ROOT_OTHER && where !== ROOT_TREE) {
|
|
404
428
|
return { value: { exitCode: 1, stdout: '', stderr: 'failed to run git: fatal: not a git repository', isStdoutTruncated: false, isStderrTruncated: false } }
|
|
405
429
|
}
|
|
406
430
|
if (r.deny !== null) return { deny: r.deny }
|
|
407
|
-
const out = await (r.gh ?? defaultGh)(e.argv)
|
|
431
|
+
const out = await (r.gh ?? defaultGh)(unscrub(e.argv))
|
|
408
432
|
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
409
433
|
})
|
|
410
434
|
on('ui.open', (_$, e) => {
|
|
@@ -413,7 +437,7 @@ const rig = (on: On, engine?: any) => {
|
|
|
413
437
|
...r.panes.filter(p => p.id !== e.id),
|
|
414
438
|
{ id: e.id, title: e.title ?? e.id, isShown: true, isFocused: false, isPlaced: r.isPlaced },
|
|
415
439
|
]
|
|
416
|
-
return { value: r.isPlaced ? { isPlaced: true as const } : { isPlaced: false as const, reason:
|
|
440
|
+
return { value: r.isPlaced ? { isPlaced: true as const } : { isPlaced: false as const, reason: r.reason } }
|
|
417
441
|
})
|
|
418
442
|
on('ui.close', (_$, e) => {
|
|
419
443
|
r.panes = r.panes.filter(p => p.id !== e.id)
|
|
@@ -426,6 +450,7 @@ const rig = (on: On, engine?: any) => {
|
|
|
426
450
|
on('turn.start', (_$, e) => ({ turnId: e.turnId }))
|
|
427
451
|
on('turn.complete', () => ({ text: '' }))
|
|
428
452
|
on('session.end', (_$, e) => ({ sessionId: e.sessionId }))
|
|
453
|
+
on('session.attach', (_$, e) => ({ clientId: e.clientId }))
|
|
429
454
|
on('ui.render', () => ({ type: 'Box', props: { key: 'engine' }, children: [] }))
|
|
430
455
|
return r
|
|
431
456
|
}
|
|
@@ -439,6 +464,8 @@ const BAND_PROPS = {
|
|
|
439
464
|
scroll: { offset: 0, bodyRows: 10 },
|
|
440
465
|
view: {},
|
|
441
466
|
} as const
|
|
467
|
+
// What the command answers for an undrawn pane (never the host's raw reason).
|
|
468
|
+
const NOT_SHOWN = 'Map panel is open but not shown yet: it appears once the screen can place it.'
|
|
442
469
|
const FULLSCREEN = { columns: 200, rows: 50, isFullscreen: true } as const
|
|
443
470
|
|
|
444
471
|
const START = { cwd: '/repo', surface: 'terminal', isInteractive: true } as const
|
|
@@ -495,13 +522,24 @@ for (const surface of SURFACES) {
|
|
|
495
522
|
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
496
523
|
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 2')
|
|
497
524
|
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 2')
|
|
498
|
-
expect((await ui.find({ key: 'head-frontier' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
499
|
-
|
|
525
|
+
expect((await ui.find({ key: 'head-frontier' }))?.children[0]).toMatchObject({ props: { bold: true, color: '#9ece6a' } })
|
|
526
|
+
// ogni sezione ha il suo glifo e un filetto colorato davanti a ogni riga
|
|
527
|
+
expect(await textOf(ui, 'glyph-frontier')).toBe('◆ ')
|
|
528
|
+
expect(await textOf(ui, 'glyph-blocked')).toBe('■ ')
|
|
529
|
+
if (surface === 'terminal') {
|
|
530
|
+
expect(await textOf(ui, 'gutter-209')).toBe('│ ')
|
|
531
|
+
expect((await ui.find({ key: 'gutter-211' }))?.children[0]).toMatchObject({ props: { color: '#f7768e' } })
|
|
532
|
+
} else {
|
|
533
|
+
// sul desktop il filetto è una barra continua, alta quanto il blocco
|
|
534
|
+
expect((await ui.find({ key: 'gutter-211-bar' }))?.props).toMatchObject({ backgroundColor: '#f7768e' })
|
|
535
|
+
}
|
|
536
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-209', 'row-210', 'row-214', 'row-211', 'row-213'])
|
|
500
537
|
|
|
501
538
|
expect(await textOf(ui, 'ticket-209')).toBe('#209 Burn-down view of the map')
|
|
502
539
|
expect(await textOf(ui, 'type-209')).toBe(' build')
|
|
503
|
-
expect((await ui.find({ key: 'type-209' }))?.children[0]).toMatchObject({ props: {
|
|
540
|
+
expect((await ui.find({ key: 'type-209' }))?.children[0]).toMatchObject({ props: { color: '#7dcfff' } })
|
|
504
541
|
expect(await textOf(ui, 'type-214')).toBe(' decision')
|
|
542
|
+
expect((await ui.find({ key: 'type-214' }))?.children[0]).toMatchObject({ props: { color: '#ff9e64' } })
|
|
505
543
|
const links = (await ui.findAll({ type: 'Link' })).map((l: any) => l.props.href)
|
|
506
544
|
expect(links).toContain(`${URL}/209`)
|
|
507
545
|
|
|
@@ -527,6 +565,7 @@ for (const surface of SURFACES) {
|
|
|
527
565
|
// the quarantined ticket is counted, never listed
|
|
528
566
|
expect(await ui.find({ key: 'row-215' })).toBeUndefined()
|
|
529
567
|
expect(await textOf(ui, 'unverified')).toBe('1 unverified ticket hidden (quarantined)')
|
|
568
|
+
expect((await ui.find({ key: 'unverified' }))?.children[0]).toMatchObject({ props: { color: '#e0af68' } })
|
|
530
569
|
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
531
570
|
})
|
|
532
571
|
|
|
@@ -550,16 +589,20 @@ for (const surface of SURFACES) {
|
|
|
550
589
|
|
|
551
590
|
expect(await textOf(ui, 'head-uat')).toBe('UAT · 3')
|
|
552
591
|
expect((await ui.find({ key: 'head-uat' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
553
|
-
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-
|
|
592
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-in-progress', 'group-frontier', 'group-uat', 'group-paused', 'group-blocked'])
|
|
554
593
|
// each ticket once: a UAT row is in no other group, a paused UAT stays under Paused
|
|
555
594
|
expect(await keysOf(ui, 'row-')).toEqual([
|
|
556
|
-
'row-
|
|
595
|
+
'row-208', 'row-212', 'row-209', 'row-210', 'row-214', 'row-240', 'row-241', 'row-242', 'row-243', 'row-211', 'row-213',
|
|
557
596
|
])
|
|
558
597
|
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
559
598
|
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
560
599
|
// the parent line, dim, under the row; none without a parent
|
|
561
600
|
expect(await textOf(ui, 'uat-of-240')).toBe('tests Burn-down view of the map #209')
|
|
562
601
|
expect((await ui.find({ key: 'uat-of-240' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
602
|
+
// la riga del genitore sta nel blocco del ticket, dopo il filetto che copre entrambe le righe
|
|
603
|
+
expect(await keysOf(ui, 'entry-lines-240')).toEqual(['entry-lines-240'])
|
|
604
|
+
expect(await textOf(ui, 'entry-lines-240')).toContain('tests Burn-down view of the map #209')
|
|
605
|
+
if (surface === 'terminal') expect(await textOf(ui, 'gutter-240')).toBe('│ │ ')
|
|
563
606
|
expect(await textOf(ui, 'uat-of-241')).toBe('tests #212')
|
|
564
607
|
expect(await hasKey(ui, 'uat-of-242')).toBe(false)
|
|
565
608
|
expect(await textOf(ui, 'uat-of-243')).toBe('tests Paused parent #150')
|
|
@@ -654,7 +697,7 @@ for (const surface of SURFACES) {
|
|
|
654
697
|
expect(r.calls.filter(isUatRead)).toContainEqual(uatCall())
|
|
655
698
|
expect(r.calls.filter(isUatSearch)).toEqual([])
|
|
656
699
|
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
657
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
700
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-306', 'row-303'])
|
|
658
701
|
expect(await textOf(ui, 'uat-of-306')).toBe('tests Loose parent #290')
|
|
659
702
|
})
|
|
660
703
|
|
|
@@ -675,8 +718,8 @@ for (const surface of SURFACES) {
|
|
|
675
718
|
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 2')
|
|
676
719
|
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 2')
|
|
677
720
|
// a paused ticket appears in one group only: Frontier, In progress, Paused, Blocked in order
|
|
678
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
679
|
-
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-
|
|
721
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-209', 'row-210', 'row-214', 'row-230', 'row-231', 'row-232', 'row-211', 'row-213'])
|
|
722
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-in-progress', 'group-frontier', 'group-paused', 'group-blocked'])
|
|
680
723
|
for (const n of [230, 231, 232]) {
|
|
681
724
|
expect(await hasKey(ui, `resume-${n}`)).toBe(true)
|
|
682
725
|
expect(await hasKey(ui, `work-${n}`)).toBe(false)
|
|
@@ -692,12 +735,12 @@ for (const surface of SURFACES) {
|
|
|
692
735
|
const ui = await openAndMount($, r)
|
|
693
736
|
await pressDashboard(ui, r)
|
|
694
737
|
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
695
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
738
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-305', 'row-303'])
|
|
696
739
|
expect(await hasKey(ui, 'work-305')).toBe(false)
|
|
697
740
|
expect(await hasKey(ui, 'resume-305')).toBe(true)
|
|
698
741
|
})
|
|
699
742
|
|
|
700
|
-
test(`needs-you tickets sit in their own group
|
|
743
|
+
test(`needs-you tickets sit in their own group right after In progress, once each, with a work button unless held [${surface}]`, async ($, on) => {
|
|
701
744
|
const r = rig(on, $)
|
|
702
745
|
r.tickets = [
|
|
703
746
|
...TICKETS,
|
|
@@ -713,9 +756,9 @@ for (const surface of SURFACES) {
|
|
|
713
756
|
|
|
714
757
|
expect(await textOf(ui, 'head-needs-you')).toBe('Needs you · 6')
|
|
715
758
|
expect((await ui.find({ key: 'head-needs-you' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
716
|
-
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-
|
|
717
|
-
// each ticket once, ahead of the frontier rows; the other counts leave them out
|
|
718
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
759
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-in-progress', 'group-needs-you', 'group-frontier', 'group-blocked'])
|
|
760
|
+
// each ticket once, after my in-progress rows and ahead of the frontier rows; the other counts leave them out
|
|
761
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-250', 'row-251', 'row-252', 'row-253', 'row-254', 'row-255', 'row-209', 'row-210', 'row-214', 'row-211', 'row-213'])
|
|
719
762
|
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
720
763
|
expect(await hasKey(ui, 'head-paused')).toBe(false)
|
|
721
764
|
expect(await hasKey(ui, 'head-uat')).toBe(false)
|
|
@@ -738,16 +781,16 @@ for (const surface of SURFACES) {
|
|
|
738
781
|
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '251' }])
|
|
739
782
|
})
|
|
740
783
|
|
|
741
|
-
test(`the dashboard view puts a needs-you whiteboard ticket
|
|
784
|
+
test(`the dashboard view puts a needs-you whiteboard ticket in its own group right after In progress [${surface}]`, async ($, on) => {
|
|
742
785
|
const r = rig(on, $)
|
|
743
786
|
r.whiteboard = [...WHITEBOARD, issue(307, 'Loose needs you', [...WB, 'trailhead:task', 'trailhead:needs-you', 'trailhead:paused'])]
|
|
744
787
|
await $.session.start(START)
|
|
745
788
|
const ui = await openAndMount($, r)
|
|
746
789
|
await pressDashboard(ui, r)
|
|
747
790
|
expect(await textOf(ui, 'head-needs-you')).toBe('Needs you · 1')
|
|
748
|
-
//
|
|
749
|
-
expect((await keysOf(ui, 'group-')).slice(0,
|
|
750
|
-
expect((await keysOf(ui, 'row-'))
|
|
791
|
+
// right after the maps group and In progress
|
|
792
|
+
expect((await keysOf(ui, 'group-')).slice(0, 3)).toEqual(['group-maps', 'group-in-progress', 'group-needs-you'])
|
|
793
|
+
expect((await keysOf(ui, 'row-')).slice(0, 2)).toEqual(['row-302', 'row-307'])
|
|
751
794
|
expect(await hasKey(ui, 'head-paused')).toBe(false)
|
|
752
795
|
expect(await hasKey(ui, 'work-307')).toBe(true)
|
|
753
796
|
expect(await hasKey(ui, 'resume-307')).toBe(false)
|
|
@@ -1107,6 +1150,17 @@ for (const surface of SURFACES) {
|
|
|
1107
1150
|
expect(await reasonFor(() => (r.gh = () => ({ exitCode: 1, stdout: '', stderr: 'boom' })))).toMatch(/^⚠ gh failed \(exit 1\) · list from /)
|
|
1108
1151
|
})
|
|
1109
1152
|
|
|
1153
|
+
test(`a pinned checkout that is gone is named, not blamed on gh [${surface}]`, async ($, on) => {
|
|
1154
|
+
const r = rig(on, $)
|
|
1155
|
+
await $.session.start(START)
|
|
1156
|
+
const ui = await openAndMount($, r)
|
|
1157
|
+
r.gone = [ROOT]
|
|
1158
|
+
await r.clock.advance(FLOOR)
|
|
1159
|
+
await completeTurn($)
|
|
1160
|
+
await settle(r)
|
|
1161
|
+
expect(await textOf(ui, 'age')).toMatch(/^⚠ checkout not found: \/repo · list from /)
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1110
1164
|
test(`a first fetch that fails shows the reason without a list [${surface}]`, async ($, on) => {
|
|
1111
1165
|
const r = rig(on, $)
|
|
1112
1166
|
r.gh = () => ({ exitCode: 1, stdout: '', stderr: 'boom' })
|
|
@@ -1280,6 +1334,22 @@ for (const surface of SURFACES) {
|
|
|
1280
1334
|
expect(listReads(r)).toEqual([])
|
|
1281
1335
|
})
|
|
1282
1336
|
|
|
1337
|
+
// A heredoc body is data: a write-looking line inside one starts no read, a real write after it still does (#369).
|
|
1338
|
+
test(`a heredoc body that names a tracker write starts no pane read [${surface}]`, async ($, on) => {
|
|
1339
|
+
const r = rig(on, $)
|
|
1340
|
+
await $.session.start(START)
|
|
1341
|
+
await openAndMount($, r)
|
|
1342
|
+
await r.clock.advance(FLOOR)
|
|
1343
|
+
const before = fetches(r)
|
|
1344
|
+
await $.tool.call({ tool: 'Bash', command: "cat > /tmp/x.md <<'EOF'\ngh issue close 209\nEOF" } as never)
|
|
1345
|
+
await settle(r)
|
|
1346
|
+
expect(fetches(r)).toBe(before)
|
|
1347
|
+
|
|
1348
|
+
await $.tool.call({ tool: 'Bash', command: "cat > /tmp/x.md <<'EOF'\nbody\nEOF\ngh issue close 209" } as never)
|
|
1349
|
+
await settle(r)
|
|
1350
|
+
expect(fetches(r)).toBe(before + 1)
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1283
1353
|
// The reads asked while the pane was undrawn (the person on another session) are dropped: drawn again with a
|
|
1284
1354
|
// list older than one slow-timer period, the pane reads at once instead of showing a closed ticket.
|
|
1285
1355
|
test(`a pane drawn again with an overdue list reads at once [${surface}]`, async ($, on) => {
|
|
@@ -1370,7 +1440,7 @@ for (const surface of SURFACES) {
|
|
|
1370
1440
|
await $.session.start(START)
|
|
1371
1441
|
const ui = await openAndMount($, r)
|
|
1372
1442
|
await pressDashboard(ui, r)
|
|
1373
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
1443
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
1374
1444
|
})
|
|
1375
1445
|
|
|
1376
1446
|
test(`a write that reads at once is followed by one more read at the floor's end [${surface}]`, async ($, on) => {
|
|
@@ -1462,6 +1532,34 @@ for (const surface of SURFACES) {
|
|
|
1462
1532
|
}
|
|
1463
1533
|
})
|
|
1464
1534
|
|
|
1535
|
+
test(`a multi-line Bash write refreshes, a multi-line read does not [${surface}]`, async ($, on) => {
|
|
1536
|
+
const r = rig(on, $)
|
|
1537
|
+
await $.session.start(START)
|
|
1538
|
+
await openAndMount($, r)
|
|
1539
|
+
const heredoc = "cat > /tmp/x.md <<'EOF'\nbody line\nEOF"
|
|
1540
|
+
const base = fetches(r)
|
|
1541
|
+
for (const command of [`${heredoc}\ngh issue view 5`, `${heredoc}\ngh issue list --state open`]) {
|
|
1542
|
+
await r.clock.advance(FLOOR)
|
|
1543
|
+
await $.tool.call({ tool: 'Bash', command } as never)
|
|
1544
|
+
await settle(r)
|
|
1545
|
+
}
|
|
1546
|
+
expect(fetches(r)).toBe(base)
|
|
1547
|
+
|
|
1548
|
+
for (const command of [`${heredoc}\ncd /repo && gh issue edit 325 --title "t"`, `${heredoc}\ngh issue create --body-file /tmp/x.md`]) {
|
|
1549
|
+
// until a whole floor passes with no read (the previous write's follow-up, the slow timer), so this write starts outside one
|
|
1550
|
+
let seen = -1
|
|
1551
|
+
while (seen !== fetches(r)) {
|
|
1552
|
+
seen = fetches(r)
|
|
1553
|
+
await r.clock.advance(FLOOR)
|
|
1554
|
+
await settle(r)
|
|
1555
|
+
}
|
|
1556
|
+
const before = fetches(r)
|
|
1557
|
+
await $.tool.call({ tool: 'Bash', command } as never)
|
|
1558
|
+
await settle(r)
|
|
1559
|
+
expect([command, fetches(r)]).toEqual([command, before + 1])
|
|
1560
|
+
}
|
|
1561
|
+
})
|
|
1562
|
+
|
|
1465
1563
|
test(`closing the pane stops refreshing [${surface}]`, async ($, on) => {
|
|
1466
1564
|
const r = rig(on, $)
|
|
1467
1565
|
await $.session.start(START)
|
|
@@ -1623,9 +1721,11 @@ for (const surface of SURFACES) {
|
|
|
1623
1721
|
// The token the verb receives decodes (plain base64url, the helper's alphabet) to the pinned root; the suite has no Buffer.
|
|
1624
1722
|
const [number, word] = r.runs[1].args.split(' ')
|
|
1625
1723
|
expect(number).toBe('209')
|
|
1626
|
-
expect(word).toMatch(/^--in=[A-Za-z0-9_-]+$/)
|
|
1724
|
+
expect(word).toMatch(/^--in=[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)
|
|
1627
1725
|
expect(word).toBe(IN(ROOT))
|
|
1628
|
-
|
|
1726
|
+
const [rootPart, repoPart] = word.slice('--in='.length).split('.')
|
|
1727
|
+
expect(atob(rootPart.replace(/-/g, '+').replace(/_/g, '/'))).toBe(ROOT)
|
|
1728
|
+
expect(atob(repoPart.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(repoPart.length / 4) * 4, '='))).toBe('github.com/o/r')
|
|
1629
1729
|
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1630
1730
|
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1631
1731
|
})
|
|
@@ -2280,7 +2380,7 @@ for (const surface of SURFACES) {
|
|
|
2280
2380
|
await settle(r)
|
|
2281
2381
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2282
2382
|
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
2283
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
2383
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
2284
2384
|
})
|
|
2285
2385
|
|
|
2286
2386
|
test(`the refresh button obeys the same floor [${surface}]`, async ($, on) => {
|
|
@@ -2367,17 +2467,60 @@ for (const surface of SURFACES) {
|
|
|
2367
2467
|
expect(r.logs).toEqual([])
|
|
2368
2468
|
})
|
|
2369
2469
|
|
|
2370
|
-
test(`/trailhead:panel says
|
|
2470
|
+
test(`/trailhead:panel says plainly that a pane cannot be placed yet [${surface}]`, async ($, on) => {
|
|
2371
2471
|
const r = rig(on, $)
|
|
2372
2472
|
r.isPlaced = false
|
|
2373
2473
|
await $.session.start(START)
|
|
2374
|
-
expect((await panel($)).text).toBe(
|
|
2474
|
+
expect((await panel($)).text).toBe(NOT_SHOWN)
|
|
2375
2475
|
await settle(r)
|
|
2376
2476
|
expect(fetches(r)).toBe(0)
|
|
2377
|
-
// a pane
|
|
2477
|
+
// a pane the person never saw is not closed by a repeat: it stays open, undrawn, until it can be placed
|
|
2478
|
+
expect((await panel($)).text).toBe(NOT_SHOWN)
|
|
2479
|
+
expect((await panel($)).text).toBe(NOT_SHOWN)
|
|
2480
|
+
expect(r.panes.map(p => [p.id, p.isPlaced])).toEqual([[PANE, false]])
|
|
2481
|
+
// placed at last, the command sees it drawn and closes it
|
|
2482
|
+
r.isPlaced = true
|
|
2483
|
+
expect((await panel($)).text).toBe('Map panel opened.')
|
|
2484
|
+
expect((await panel($)).text).toBe('Map panel closed.')
|
|
2485
|
+
expect(r.panes).toEqual([])
|
|
2486
|
+
})
|
|
2487
|
+
|
|
2488
|
+
// #348: l'auto-open su una superficie che non ha ancora comunicato il layout resta non disegnato; il comando
|
|
2489
|
+
// non deve chiudere un pannello che la persona non ha mai visto.
|
|
2490
|
+
test(`/trailhead:panel never closes a pane the auto-open left undrawn on a surface without layout [${surface}]`, async ($, on) => {
|
|
2491
|
+
const r = rig(on, $)
|
|
2492
|
+
r.isPlaced = false
|
|
2493
|
+
r.reason = 'attached surfaces place no panes: desktop has not reported its layout'
|
|
2494
|
+
await $.session.start(START)
|
|
2495
|
+
await mountBand($, FULLSCREEN)
|
|
2496
|
+
await settle(r)
|
|
2497
|
+
expect(r.panes.map(p => [p.id, p.isPlaced])).toEqual([[PANE, false]])
|
|
2498
|
+
|
|
2499
|
+
const shown = NOT_SHOWN
|
|
2500
|
+
expect((await panel($)).text).toBe(shown)
|
|
2501
|
+
expect((await panel($)).text).toBe(shown)
|
|
2502
|
+
expect(r.panes.map(p => [p.id, p.isPlaced])).toEqual([[PANE, false]])
|
|
2503
|
+
|
|
2504
|
+
// the layout arrives: the command draws it, and only then does a repeat close it
|
|
2505
|
+
r.isPlaced = true
|
|
2506
|
+
expect((await panel($)).text).toBe('Map panel opened.')
|
|
2378
2507
|
expect((await panel($)).text).toBe('Map panel closed.')
|
|
2379
2508
|
})
|
|
2380
2509
|
|
|
2510
|
+
// #321: nessuna superficie collegata (inizio sessione) non è un errore: il pannello resta aperto in attesa.
|
|
2511
|
+
test(`/trailhead:panel with no surface attached keeps the pane pending and says so plainly [${surface}]`, async ($, on) => {
|
|
2512
|
+
const r = rig(on, $)
|
|
2513
|
+
r.isPlaced = false
|
|
2514
|
+
r.reason = 'no-surface: no surface is attached ($.session.surfaces() is empty)'
|
|
2515
|
+
await $.session.start(START)
|
|
2516
|
+
const pending = 'Map panel will open as soon as the screen is ready.'
|
|
2517
|
+
expect((await panel($)).text).toBe(pending)
|
|
2518
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2519
|
+
// a second command while still pending is idempotent: it never closes the pane
|
|
2520
|
+
expect((await panel($)).text).toBe(pending)
|
|
2521
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2522
|
+
})
|
|
2523
|
+
|
|
2381
2524
|
test(`/trailhead:panel shows a pane the auto-open left undrawn instead of closing it [${surface}]`, async ($, on) => {
|
|
2382
2525
|
const r = rig(on, $)
|
|
2383
2526
|
// sessione vuota, terminale sotto la soglia dei pannelli non richiesti: l'auto-open resta non disegnato
|
|
@@ -2399,6 +2542,148 @@ for (const surface of SURFACES) {
|
|
|
2399
2542
|
expect(r.panes).toEqual([])
|
|
2400
2543
|
})
|
|
2401
2544
|
|
|
2545
|
+
// #348: un auto-open che resta non disegnato non conta come fatto, così un trigger successivo può riprovare.
|
|
2546
|
+
test(`an auto-open left undrawn does not end the auto-open, a drawn pane does [${surface}]`, async ($, on) => {
|
|
2547
|
+
const r = rig(on, $)
|
|
2548
|
+
r.isPlaced = false
|
|
2549
|
+
r.reason = 'attached surfaces place no panes: desktop has not reported its layout'
|
|
2550
|
+
await $.session.start(START)
|
|
2551
|
+
await mountBand($, FULLSCREEN)
|
|
2552
|
+
await settle(r)
|
|
2553
|
+
expect(r.opens).toHaveLength(1)
|
|
2554
|
+
|
|
2555
|
+
// listed but undrawn: further renders do not open it again (the host places it when it can)
|
|
2556
|
+
await mountBand($, FULLSCREEN)
|
|
2557
|
+
await settle(r)
|
|
2558
|
+
expect(r.opens).toHaveLength(1)
|
|
2559
|
+
|
|
2560
|
+
// the host dropped it while still undrawn: the next render tries again
|
|
2561
|
+
r.panes = []
|
|
2562
|
+
await mountBand($, FULLSCREEN)
|
|
2563
|
+
await settle(r)
|
|
2564
|
+
expect(r.opens).toHaveLength(2)
|
|
2565
|
+
|
|
2566
|
+
// drawn at last: its own render completes the auto-open for good
|
|
2567
|
+
r.panes = r.panes.map(p => ({ ...p, isPlaced: true }))
|
|
2568
|
+
await mountPane($)
|
|
2569
|
+
await settle(r)
|
|
2570
|
+
r.panes = []
|
|
2571
|
+
await mountBand($, FULLSCREEN)
|
|
2572
|
+
await settle(r)
|
|
2573
|
+
expect(r.opens).toHaveLength(2)
|
|
2574
|
+
})
|
|
2575
|
+
|
|
2576
|
+
test(`an auto-open that finds the pane listed and drawn ends the auto-open [${surface}]`, async ($, on) => {
|
|
2577
|
+
const r = rig(on, $)
|
|
2578
|
+
r.isPlaced = false
|
|
2579
|
+
await $.session.start(START)
|
|
2580
|
+
await mountBand($, FULLSCREEN)
|
|
2581
|
+
await settle(r)
|
|
2582
|
+
expect(r.opens).toHaveLength(1)
|
|
2583
|
+
|
|
2584
|
+
// placed by the host with no pane render yet (the pane's render needs the dock): the next band render sees it
|
|
2585
|
+
r.panes = r.panes.map(p => ({ ...p, isPlaced: true }))
|
|
2586
|
+
await mountBand($, FULLSCREEN)
|
|
2587
|
+
await settle(r)
|
|
2588
|
+
r.panes = []
|
|
2589
|
+
await mountBand($, FULLSCREEN)
|
|
2590
|
+
await settle(r)
|
|
2591
|
+
expect(r.opens).toHaveLength(1)
|
|
2592
|
+
})
|
|
2593
|
+
|
|
2594
|
+
test(`the person's own request on an undrawn pane ends the auto-open, a close of it stays closed [${surface}]`, async ($, on) => {
|
|
2595
|
+
const r = rig(on, $)
|
|
2596
|
+
r.isPlaced = false
|
|
2597
|
+
await $.session.start(START)
|
|
2598
|
+
await mountBand($, FULLSCREEN)
|
|
2599
|
+
await settle(r)
|
|
2600
|
+
expect(r.opens).toHaveLength(1)
|
|
2601
|
+
|
|
2602
|
+
// the command is the person acting on it: the auto-open is done even though the pane is still undrawn
|
|
2603
|
+
expect((await panel($)).text).toBe(NOT_SHOWN)
|
|
2604
|
+
expect(r.opens).toHaveLength(2)
|
|
2605
|
+
r.panes = []
|
|
2606
|
+
await mountBand($, FULLSCREEN)
|
|
2607
|
+
await settle(r)
|
|
2608
|
+
expect(r.opens).toHaveLength(2)
|
|
2609
|
+
})
|
|
2610
|
+
|
|
2611
|
+
// Il desktop avvia la sessione al primo invio: il comando che l'ha avviata arriva subito dopo l'auto-open.
|
|
2612
|
+
test(`/trailhead:panel right after the auto-open keeps the pane the person never saw [${surface}]`, async ($, on) => {
|
|
2613
|
+
const r = rig(on, $)
|
|
2614
|
+
await $.session.start(START)
|
|
2615
|
+
await $.session.attach({ surface, clientId: `${surface}:default`, viewport: FULLSCREEN } as never)
|
|
2616
|
+
await settle(r)
|
|
2617
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2618
|
+
|
|
2619
|
+
const opened = await panel($)
|
|
2620
|
+
expect(opened.text).toBe('Map panel opened.')
|
|
2621
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2622
|
+
|
|
2623
|
+
// the next press acts on a pane the person asked for: it closes, however soon
|
|
2624
|
+
const closed = await panel($)
|
|
2625
|
+
expect(closed.text).toBe('Map panel closed.')
|
|
2626
|
+
expect(r.panes).toEqual([])
|
|
2627
|
+
})
|
|
2628
|
+
|
|
2629
|
+
test(`/trailhead:panel closes an auto-opened pane once it has been on screen [${surface}]`, async ($, on) => {
|
|
2630
|
+
const r = rig(on, $)
|
|
2631
|
+
await $.session.start(START)
|
|
2632
|
+
await $.session.attach({ surface, clientId: `${surface}:default`, viewport: FULLSCREEN } as never)
|
|
2633
|
+
await settle(r)
|
|
2634
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2635
|
+
|
|
2636
|
+
await r.clock.advance(2_000)
|
|
2637
|
+
const closed = await panel($)
|
|
2638
|
+
expect(closed.text).toBe('Map panel closed.')
|
|
2639
|
+
expect(r.panes).toEqual([])
|
|
2640
|
+
})
|
|
2641
|
+
|
|
2642
|
+
test(`a surface attaching in fullscreen retries the auto-open [${surface}]`, async ($, on) => {
|
|
2643
|
+
const r = rig(on, $)
|
|
2644
|
+
r.isPlaced = false
|
|
2645
|
+
r.reason = 'no-surface: no surface is attached ($.session.surfaces() is empty)'
|
|
2646
|
+
await $.session.start(START)
|
|
2647
|
+
await settle(r)
|
|
2648
|
+
expect(r.opens).toEqual([])
|
|
2649
|
+
|
|
2650
|
+
// not fullscreen, or no layout said: the render trigger decides
|
|
2651
|
+
await $.session.attach({ surface, clientId: `${surface}:default`, viewport: { columns: 200, rows: 50, isFullscreen: false } } as never)
|
|
2652
|
+
await $.session.attach({ surface, clientId: `${surface}:default` } as never)
|
|
2653
|
+
await settle(r)
|
|
2654
|
+
expect(r.opens).toEqual([])
|
|
2655
|
+
|
|
2656
|
+
await $.session.attach({ surface, clientId: `${surface}:default`, viewport: FULLSCREEN } as never)
|
|
2657
|
+
await settle(r)
|
|
2658
|
+
expect(r.opens).toHaveLength(1)
|
|
2659
|
+
|
|
2660
|
+
// still undrawn and listed: another attach does not open again
|
|
2661
|
+
await $.session.attach({ surface, clientId: `${surface}:other`, viewport: FULLSCREEN } as never)
|
|
2662
|
+
await settle(r)
|
|
2663
|
+
expect(r.opens).toHaveLength(1)
|
|
2664
|
+
})
|
|
2665
|
+
|
|
2666
|
+
test(`an attach never opens the pane after the person closed it [${surface}]`, async ($, on) => {
|
|
2667
|
+
const r = rig(on, $)
|
|
2668
|
+
await $.session.start(START)
|
|
2669
|
+
await panel($)
|
|
2670
|
+
await panel($)
|
|
2671
|
+
expect(r.panes).toEqual([])
|
|
2672
|
+
await $.session.attach({ surface, clientId: `${surface}:default`, viewport: FULLSCREEN } as never)
|
|
2673
|
+
await settle(r)
|
|
2674
|
+
expect(r.opens).toHaveLength(1)
|
|
2675
|
+
|
|
2676
|
+
})
|
|
2677
|
+
|
|
2678
|
+
test(`an attach without an active map does not open the pane [${surface}]`, async ($, on) => {
|
|
2679
|
+
const r = rig(on, $)
|
|
2680
|
+
r.active = null
|
|
2681
|
+
await $.session.start(START)
|
|
2682
|
+
await $.session.attach({ surface, clientId: `${surface}:default`, viewport: FULLSCREEN } as never)
|
|
2683
|
+
await settle(r)
|
|
2684
|
+
expect(r.opens).toEqual([])
|
|
2685
|
+
})
|
|
2686
|
+
|
|
2402
2687
|
for (const withBand of [false, true]) {
|
|
2403
2688
|
test(`the pane opens by itself once in fullscreen, the band ${withBand ? 'drawing' : 'yielding'} [${surface}]`, async ($, on) => {
|
|
2404
2689
|
const r = rig(on, $)
|
|
@@ -2519,7 +2804,7 @@ for (const surface of SURFACES) {
|
|
|
2519
2804
|
expect(await textOf(ui, 'head-maps')).toBe('Maps · 2')
|
|
2520
2805
|
expect(await keysOf(ui, 'map-row-')).toEqual(['map-row-14', 'map-row-198'])
|
|
2521
2806
|
expect(await textOf(ui, 'map-mark-198')).toBe('● ')
|
|
2522
|
-
expect((await ui.find({ key: 'map-mark-198' }))?.children[0]).toMatchObject({ props: { color:
|
|
2807
|
+
expect((await ui.find({ key: 'map-mark-198' }))?.children[0]).toMatchObject({ props: { bold: true, color: '#bb9af7' } })
|
|
2523
2808
|
expect(await textOf(ui, 'map-aside-198')).toBe(' 7 open / 23 · active')
|
|
2524
2809
|
expect(await textOf(ui, 'map-label-198')).toBe('#198 trailhead mod')
|
|
2525
2810
|
expect((await ui.find({ key: 'map-label-198' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
@@ -2552,7 +2837,7 @@ for (const surface of SURFACES) {
|
|
|
2552
2837
|
|
|
2553
2838
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2554
2839
|
expect(await textOf(ui, 'destination')).toBe('An older destination.')
|
|
2555
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
2840
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
2556
2841
|
expect(await textOf(ui, 'holder-141')).toBe('@carol')
|
|
2557
2842
|
expect(await textOf(ui, 'burndown-counts')).toBe('open 2 · done 0 · removed 0 · peak 2')
|
|
2558
2843
|
// the ticket in play belongs to the other map: nothing here is marked
|
|
@@ -2727,7 +3012,7 @@ for (const surface of SURFACES) {
|
|
|
2727
3012
|
await settle(r)
|
|
2728
3013
|
expect(await hasKey(ui, 'ticket-300')).toBe(false)
|
|
2729
3014
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2730
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3015
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
2731
3016
|
})
|
|
2732
3017
|
|
|
2733
3018
|
test(`with no active map the open maps are listed and one can be viewed [${surface}]`, async ($, on) => {
|
|
@@ -2757,6 +3042,57 @@ for (const surface of SURFACES) {
|
|
|
2757
3042
|
await settle(r)
|
|
2758
3043
|
}
|
|
2759
3044
|
|
|
3045
|
+
// My tickets in play on other maps and the whiteboard, under every view's In progress.
|
|
3046
|
+
const MINE = [
|
|
3047
|
+
issue(150, 'My whiteboard fix', [...WB, 'trailhead:build'], ['me']),
|
|
3048
|
+
issue(160, 'My other map ticket', ['trailhead:ticket', 'trailhead:build', 'trailhead:map-14'], ['me']),
|
|
3049
|
+
issue(161, 'My paused one', ['trailhead:ticket', 'trailhead:build', 'trailhead:map-14', 'trailhead:paused'], ['me']),
|
|
3050
|
+
issue(212, 'Native gh-backed tools', ['trailhead:ticket', 'trailhead:build', 'trailhead:map-198'], ['me']),
|
|
3051
|
+
]
|
|
3052
|
+
|
|
3053
|
+
test(`the map's In progress adds my tickets of the other maps and the whiteboard, each saying where [${surface}]`, async ($, on) => {
|
|
3054
|
+
const r = rig(on, $)
|
|
3055
|
+
r.mine = MINE
|
|
3056
|
+
await $.session.start(START)
|
|
3057
|
+
const ui = await openAndMount($, r)
|
|
3058
|
+
expect(r.mineCalls[0]).toEqual(['gh', 'api', 'graphql', '-f', 'query=query{viewer{login}}'])
|
|
3059
|
+
expect(r.mineCalls.some(argv => argv.includes('login=me'))).toBe(true)
|
|
3060
|
+
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 4')
|
|
3061
|
+
// the map's own first, then mine elsewhere; a paused one and one the map already lists are not added
|
|
3062
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-150', 'row-160', 'row-209', 'row-210', 'row-214', 'row-211', 'row-213'])
|
|
3063
|
+
expect(await textOf(ui, 'scope-of-150')).toBe('on whiteboard')
|
|
3064
|
+
expect(await textOf(ui, 'scope-of-160')).toBe('on map #14 older map')
|
|
3065
|
+
expect((await ui.find({ key: 'scope-of-160' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
3066
|
+
expect(await textOf(ui, 'holder-160')).toBe('@me')
|
|
3067
|
+
expect(await hasKey(ui, 'scope-of-212')).toBe(false)
|
|
3068
|
+
expect(await hasKey(ui, 'row-161')).toBe(false)
|
|
3069
|
+
})
|
|
3070
|
+
|
|
3071
|
+
test(`the dashboard's In progress adds my tickets of every map [${surface}]`, async ($, on) => {
|
|
3072
|
+
const r = rig(on, $)
|
|
3073
|
+
r.mine = MINE
|
|
3074
|
+
await $.session.start(START)
|
|
3075
|
+
const ui = await openAndMount($, r)
|
|
3076
|
+
await pressDashboard(ui, r)
|
|
3077
|
+
expect(await textOf(ui, 'scope-of-160')).toBe('on map #14 older map')
|
|
3078
|
+
expect(await textOf(ui, 'scope-of-212')).toBe('on map #198 trailhead mod')
|
|
3079
|
+
// a whiteboard ticket of mine belongs to this view's own list, never a second time with a scope line
|
|
3080
|
+
expect(await hasKey(ui, 'scope-of-150')).toBe(false)
|
|
3081
|
+
})
|
|
3082
|
+
|
|
3083
|
+
test(`a failed read of my tickets keeps the last ones and sets no error [${surface}]`, async ($, on) => {
|
|
3084
|
+
const r = rig(on, $)
|
|
3085
|
+
r.mine = MINE
|
|
3086
|
+
await $.session.start(START)
|
|
3087
|
+
const ui = await openAndMount($, r)
|
|
3088
|
+
expect(await hasKey(ui, 'row-160')).toBe(true)
|
|
3089
|
+
r.mine = null
|
|
3090
|
+
await r.clock.advance(TIMER)
|
|
3091
|
+
await settle(r)
|
|
3092
|
+
expect(await hasKey(ui, 'row-160')).toBe(true)
|
|
3093
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
3094
|
+
})
|
|
3095
|
+
|
|
2760
3096
|
test(`the Whiteboard row shows the maps index and the whiteboard tickets [${surface}]`, async ($, on) => {
|
|
2761
3097
|
const r = rig(on, $)
|
|
2762
3098
|
await $.session.start(START)
|
|
@@ -2792,7 +3128,7 @@ for (const surface of SURFACES) {
|
|
|
2792
3128
|
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 1')
|
|
2793
3129
|
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 1')
|
|
2794
3130
|
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 1')
|
|
2795
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3131
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
2796
3132
|
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
2797
3133
|
expect(await hasKey(ui, 'work-301')).toBe(true)
|
|
2798
3134
|
expect(await textOf(ui, 'unverified')).toBe('1 unverified ticket hidden (quarantined)')
|
|
@@ -3147,7 +3483,7 @@ for (const surface of SURFACES) {
|
|
|
3147
3483
|
const ui = await openAndMount($, r)
|
|
3148
3484
|
await pressDashboard(ui, r)
|
|
3149
3485
|
expect(await textOf(ui, 'head-whiteboard')).toBe('Whiteboard · 4 open · 2 assigned')
|
|
3150
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3486
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-306', 'row-303'])
|
|
3151
3487
|
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
3152
3488
|
expect(await hasKey(ui, 'resume-306')).toBe(true)
|
|
3153
3489
|
expect(await hasKey(ui, 'row-305')).toBe(false)
|
|
@@ -3176,7 +3512,7 @@ for (const surface of SURFACES) {
|
|
|
3176
3512
|
await pressDashboard(ui, r)
|
|
3177
3513
|
await pressMap(ui, r, 14)
|
|
3178
3514
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3179
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3515
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3180
3516
|
|
|
3181
3517
|
await pressDashboard(ui, r)
|
|
3182
3518
|
await pressMap(ui, r, 198)
|
|
@@ -3228,7 +3564,7 @@ for (const surface of SURFACES) {
|
|
|
3228
3564
|
const ui = await openAndMount($, r)
|
|
3229
3565
|
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
3230
3566
|
await pressDashboard(ui, r)
|
|
3231
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3567
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
3232
3568
|
await ui.press({ key: 'to-map' })
|
|
3233
3569
|
await settle(r)
|
|
3234
3570
|
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
@@ -3242,7 +3578,7 @@ for (const surface of SURFACES) {
|
|
|
3242
3578
|
await $.command.run({ command: 'clear', args: '' } as never)
|
|
3243
3579
|
await settle(r)
|
|
3244
3580
|
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
3245
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3581
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
3246
3582
|
})
|
|
3247
3583
|
test(`the pane keeps its repo when the session cwd moves out of it [${surface}]`, async ($, on) => {
|
|
3248
3584
|
const r = rig(on, $)
|
|
@@ -3281,7 +3617,7 @@ for (const surface of SURFACES) {
|
|
|
3281
3617
|
await completeTurn($)
|
|
3282
3618
|
await settle(r)
|
|
3283
3619
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3284
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3620
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3285
3621
|
expect(r.calls).toContainEqual(MAPS_CALL)
|
|
3286
3622
|
expect(r.cwds.every(cwd => cwd === ROOT_OTHER)).toBe(true)
|
|
3287
3623
|
// The toplevel is read where the session now is, not at its project root, which a shell `cd` leaves behind.
|
|
@@ -3369,7 +3705,7 @@ for (const surface of SURFACES) {
|
|
|
3369
3705
|
await $.turn.start({ text: 'go', turnId: 'race-1' })
|
|
3370
3706
|
release()
|
|
3371
3707
|
await settle(r)
|
|
3372
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3708
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3373
3709
|
})
|
|
3374
3710
|
|
|
3375
3711
|
test(`a refresh in flight when the pin moves to another checkout of the same repo is read again [${surface}]`, async ($, on) => {
|
|
@@ -3401,7 +3737,7 @@ for (const surface of SURFACES) {
|
|
|
3401
3737
|
await r.clock.advance(FLOOR)
|
|
3402
3738
|
await settle(r)
|
|
3403
3739
|
// The dropped read is repeated: the pane shows the new tracker state, not the old list.
|
|
3404
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3740
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3405
3741
|
})
|
|
3406
3742
|
|
|
3407
3743
|
test(`a same-repo pin move landing inside the stale check is read again [${surface}]`, async ($, on) => {
|
|
@@ -3433,7 +3769,7 @@ for (const surface of SURFACES) {
|
|
|
3433
3769
|
await settle(r)
|
|
3434
3770
|
await r.clock.advance(FLOOR)
|
|
3435
3771
|
await settle(r)
|
|
3436
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3772
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3437
3773
|
})
|
|
3438
3774
|
|
|
3439
3775
|
test(`a dashboard read in flight when the pin moves to another checkout of the same repo is read again [${surface}]`, async ($, on) => {
|
|
@@ -3562,7 +3898,7 @@ for (const surface of SURFACES) {
|
|
|
3562
3898
|
expect(await keysOf(ui, 'row-')).not.toContain('row-209')
|
|
3563
3899
|
await settle(r)
|
|
3564
3900
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3565
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3901
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3566
3902
|
})
|
|
3567
3903
|
|
|
3568
3904
|
// The panel follows the whiteboard ticket this session takes (#306).
|