Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

153 lines
5.4 KiB
JavaScript

/**
* Tests for the per-message Copy action in chat.js (#4659 / PR #4689).
*
* The Copy/Retry/Delete affordances are wired through a single delegated
* click listener on #chat-messages (handleMessageActionClick), so we drive
* them by building the real action-button DOM and dispatching clicks — the
* handlers themselves are private to the chat IIFE.
*
* Focus: handleCopyMessage clipboard write + the transient icon swap, and a
* regression guard for the rapid-double-click timer race (clicking copy
* twice within 1.2s must NOT leave the button stuck on the checkmark).
*/
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
function buildChatDom() {
document.body.innerHTML = `
<div id="chat-welcome"></div>
<textarea id="chat-input"></textarea>
<button id="send-btn"></button>
<div id="chat-title"></div>
<div id="chat-messages" role="log"></div>
`;
}
/** Build an assistant message + action row matching _appendMessageActions. */
function addCopyableMessage(copyContent = 'hello world') {
const messages = document.getElementById('chat-messages');
const msg = document.createElement('div');
msg.className = 'ldr-chat-message ldr-chat-message-assistant';
const content = document.createElement('div');
content.className = 'ldr-chat-message-content';
const actions = document.createElement('div');
actions.className = 'ldr-chat-message-actions';
actions.dataset.copyContent = copyContent;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ldr-chat-msg-action ldr-chat-msg-action-copy';
btn.dataset.action = 'copy';
btn.setAttribute('aria-label', 'Copy response');
const icon = document.createElement('i');
icon.className = 'fas fa-copy';
btn.appendChild(icon);
actions.appendChild(btn);
content.appendChild(actions);
msg.appendChild(content);
messages.appendChild(msg);
return { btn, icon };
}
const flush = async (n = 6) => {
for (let i = 0; i < n; i++) await Promise.resolve();
};
const click = (el) =>
el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
describe('chat.js — per-message Copy action', () => {
let writeText;
beforeEach(async () => {
vi.resetModules();
buildChatDom();
writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
value: { writeText },
configurable: true,
});
Object.defineProperty(window, 'isSecureContext', {
value: true,
configurable: true,
});
// Keep init()'s awaited session restore from throwing.
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ sessions: [] }),
});
window.ui = { renderMarkdown: (t) => t };
window.api = { getCsrfToken: () => '' };
window.socket = {
on: vi.fn(),
subscribeToResearch: vi.fn(),
unsubscribeFromResearch: vi.fn(),
};
// Import AFTER the DOM + globals exist so the IIFE's init() finds
// #chat-messages and attaches the delegated click listener.
await import('@js/components/chat.js');
await flush();
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
document.body.innerHTML = '';
});
it('writes the message content to the clipboard and shows a checkmark', async () => {
const { btn, icon } = addCopyableMessage('copy me please');
click(icon);
await flush();
expect(writeText).toHaveBeenCalledWith('copy me please');
expect(icon.className).toBe('fas fa-check');
expect(btn.getAttribute('aria-label')).toBe('Copied');
});
it('restores the original icon and label after the 1.2s window', async () => {
vi.useFakeTimers();
const { btn, icon } = addCopyableMessage();
click(icon);
await flush();
expect(icon.className).toBe('fas fa-check');
vi.advanceTimersByTime(1200);
expect(icon.className).toBe('fas fa-copy');
expect(btn.getAttribute('aria-label')).toBe('Copy response');
});
it('rapid double-click does not leave the icon stuck on the checkmark', async () => {
// Regression guard: before the fix the second click captured the
// checkmark as the "original", so the later restore timer reverted
// the icon to the checkmark and it stayed stuck.
vi.useFakeTimers();
const { icon } = addCopyableMessage();
click(icon);
await flush();
vi.advanceTimersByTime(500); // second click well within the window
click(icon);
await flush();
expect(icon.className).toBe('fas fa-check');
// Let every scheduled restore fire.
vi.advanceTimersByTime(2000);
expect(icon.className).toBe('fas fa-copy');
});
it('falls back to execCommand when the async clipboard is unavailable', async () => {
Object.defineProperty(navigator, 'clipboard', {
value: undefined,
configurable: true,
});
const exec = vi.fn().mockReturnValue(true);
document.execCommand = exec;
const { icon } = addCopyableMessage('legacy path');
click(icon);
await flush();
expect(exec).toHaveBeenCalledWith('copy');
expect(icon.className).toBe('fas fa-check');
});
});