qwen-code/packages/web-shell/client/components/messages/SystemMessage.test.tsx
carffuca fce79dd10b
feat(web-shell): friendlier Esc interruption + queued-prompt UX (#6025)
* fix(web-shell): prevent queued-prompt loss from drain race

The auto-drain effect popped a queued prompt, called setQueuedPrompts,
then submitted via setTimeout(0). Because the daemon flips streamingState
asynchronously, the setState re-render could re-run the effect and pop a
second prompt before the first registered as streaming — both submitted
back-to-back and the first was lost.

Arm an "awaiting turn start" gate synchronously at pop so the re-run is
blocked until streamingState goes non-idle, released by a dedicated
effect with a safety-net timer for a prompt that never streams (e.g. a
queued slash command). Cleanup no longer cancels/re-queues the pending
submit while the gate is armed.

* feat(web-shell): friendlier Esc interruption + queued-prompt UX

* refactor(web-shell): tidy Esc/queue code per review

Behavior-preserving cleanups addressing review feedback on the Esc-interruption
and queued-prompt changes:

- Remove the now-dead queue.footer i18n key (EN + ZH) and the unreferenced
  .queuedHint CSS, orphaned when the Esc-clears-queue behavior was dropped.
- Co-locate the queued-prompt styles in QueuedPromptDisplay.module.css instead
  of reaching into the parent App.module.css.
- Make the Esc confirm-window constants the single source of truth: export them
  from escapeIntent.ts and drive the countdown-ring duration from one of them
  via a CSS custom property.
- Nudge the queue-drain safety net with a dedicated tick counter instead of
  cloning queuedPrompts, so it no longer re-renders the composer for a no-op.
- Drop a redundant !compact guard in StatusBar left over from flattening a
  ternary.
- Document the pop/gate-arm ordering invariant in the drain effect.
2026-06-30 01:27:16 +00:00

61 lines
2 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, describe, expect, it } from 'vitest';
import { act, type ReactNode } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { I18nProvider } from '../../i18n';
import { SystemMessage } from './SystemMessage';
(
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
const mounted: Array<{ root: Root; container: HTMLElement }> = [];
afterEach(() => {
for (const { root, container } of mounted.splice(0)) {
act(() => root.unmount());
container.remove();
}
});
function render(node: ReactNode): HTMLElement {
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(<I18nProvider language="en">{node}</I18nProvider>);
});
mounted.push({ root, container });
return container;
}
describe('SystemMessage — prompt_cancelled marker', () => {
it('renders the user-cancelled marker as a status region', () => {
const container = render(
<SystemMessage content="" variant="info" source="prompt_cancelled" />,
);
const status = container.querySelector('[role="status"]');
expect(status).not.toBeNull();
expect(status?.textContent).toBe('You cancelled this request');
});
it('ignores message content when rendering the cancelled marker', () => {
const container = render(
<SystemMessage
content="raw daemon text that must not leak"
variant="info"
source="prompt_cancelled"
/>,
);
expect(container.textContent).toBe('You cancelled this request');
expect(container.textContent).not.toContain('raw daemon text');
});
it('renders a normal message without the status marker for other sources', () => {
const container = render(
<SystemMessage content="a plain note" variant="error" />,
);
expect(container.querySelector('[role="status"]')).toBeNull();
expect(container.textContent).toContain('a plain note');
});
});