feat(web): state the run state on the terminal card's prompt line
The terminal card showed no run state: a running command and a settled command that produced no output rendered the same prompt line, so whether a command was still running had to be inferred from the absence of output. Lead the prompt line with a StateDot in three of its states — the spinning ring while running, red for the same exit status that renders the status pill, green for a clean settle. That is the same indicator a tool row's leading icon carries, so a row and its own card cannot disagree about one command; the row/card agreement is pinned in the ui-conversation spec. StateDot is aria-hidden, so a visually hidden text label rides beside it, which is what the refreshed aria goldens now record. The e2e adds what jsdom cannot compute: the dot's color resolves to the green success token through the real theme stylesheet, and the dot precedes the prompt label in document order.
This commit is contained in:
@@ -207,6 +207,34 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
return { whiteSpace: getComputedStyle(row).whiteSpace, overflowX: getComputedStyle(pane).overflowX, ...squeezed }
|
||||
})
|
||||
expect(layout).toEqual({ whiteSpace: 'pre', overflowX: 'auto', wrapped: false, scrollsSideways: true })
|
||||
// The run-state dot's color is the whole point of it and is the one thing
|
||||
// jsdom cannot report: --dsw-* tokens resolve only against the real theme
|
||||
// stylesheet. This command settled cleanly, so the dot must be the green
|
||||
// success token — a red one here would read as a failed command.
|
||||
const dot = await card.locator('[class*="_runState_"][data-state]').first().evaluate((node) => {
|
||||
// The token lives on body, so the probe must sit in the same cascade.
|
||||
const probe = document.createElement('span')
|
||||
probe.style.color = 'var(--dsw-alias-state-success-primary)'
|
||||
document.body.appendChild(probe)
|
||||
const success = getComputedStyle(probe).color
|
||||
probe.remove()
|
||||
return {
|
||||
state: node.getAttribute('data-state'),
|
||||
color: getComputedStyle(node as HTMLElement).color,
|
||||
success,
|
||||
label: node.parentElement?.querySelector('[class*="_runStateLabel_"]')?.textContent ?? null,
|
||||
// The dot precedes the prompt label in document order, which is what
|
||||
// puts it to the left of the `$`.
|
||||
beforePrompt: node.compareDocumentPosition(node.parentElement!.querySelector('[class*="_cwd_"]')!)
|
||||
=== Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
}
|
||||
})
|
||||
expect(dot.state).toBe('done')
|
||||
expect(dot.label).toBe('已完成')
|
||||
expect(dot.beforePrompt).toBe(true)
|
||||
// Resolved through the theme token, not a literal hex in the component.
|
||||
expect(dot.success).toMatch(/^rgb/)
|
||||
expect(dot.color).toBe(dot.success)
|
||||
// Golden of the card at rest — captured before the copy click, whose
|
||||
// confirmation label self-reverts on a timer and would not hold still.
|
||||
const snapshot = (await captureStableAria(page, '[data-terminal]', scaffold.workspaceCwd))
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
- text: Input json
|
||||
- button "复制"
|
||||
- code: "{ \"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\" }"
|
||||
- text: Output $ echo NAVIGATION_OK
|
||||
- text: Output 已完成 $ echo NAVIGATION_OK
|
||||
- button "复制"
|
||||
- text: NAVIGATION_OK
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
- text: $ echo NAVIGATION_OK
|
||||
- text: 已完成 $ echo NAVIGATION_OK
|
||||
- button "复制"
|
||||
- text: NAVIGATION_OK
|
||||
|
||||
@@ -120,9 +120,14 @@ function readCard(card: Element) {
|
||||
text: expander.textContent,
|
||||
expanded: expander.getAttribute('aria-expanded'),
|
||||
},
|
||||
// The run-state dot at the head of the prompt line, by its StateDot state.
|
||||
runState: card.querySelector('[class*="_runState_"][data-state]')?.getAttribute('data-state') ?? null,
|
||||
runStateLabel: card.querySelector('[class*="_runStateLabel_"]')?.textContent ?? null,
|
||||
// Every color the ANSI parser emits resolves through a --dsw-* token, so
|
||||
// the card follows the theme instead of painting literal terminal rgb.
|
||||
colors: [...new Set([...card.querySelectorAll('span[style]')]
|
||||
// Scoped to the output lines: the run-state dot is an inline-styled span
|
||||
// too, and its geometry is not an ANSI-resolved color.
|
||||
colors: [...new Set([...card.querySelectorAll('[class*="_line_"] span[style]')]
|
||||
.map(span => span.getAttribute('style')))],
|
||||
}
|
||||
}
|
||||
@@ -198,6 +203,8 @@ it('renders the keyed bash row with a resident terminal card', async () => {
|
||||
"[exit code: 1]",
|
||||
],
|
||||
"prompt": "nested pnpm run check",
|
||||
"runState": "error",
|
||||
"runStateLabel": "失败",
|
||||
"status": "退出码 1",
|
||||
}
|
||||
`)
|
||||
@@ -229,6 +236,8 @@ it('the fallback row reaches the same card through its expand control', async ()
|
||||
"-rw-r--r-- demo.txt",
|
||||
],
|
||||
"prompt": "fixture ls -la",
|
||||
"runState": "done",
|
||||
"runStateLabel": "已完成",
|
||||
"status": null,
|
||||
}
|
||||
`)
|
||||
@@ -318,6 +327,8 @@ it('the details panel Output section renders the same call at full height', asyn
|
||||
],
|
||||
"panelLines": 16,
|
||||
"prompt": "nested pnpm run check",
|
||||
"runState": "error",
|
||||
"runStateLabel": "失败",
|
||||
"status": "退出码 1",
|
||||
}
|
||||
`)
|
||||
|
||||
Reference in New Issue
Block a user