9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
187 lines
7.0 KiB
JavaScript
187 lines
7.0 KiB
JavaScript
import { JSDOM } from 'jsdom';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { ArgumentError, TimeoutError } from '@jackwener/opencli/errors';
|
|
import {
|
|
buildChatwiseInjectTextJs,
|
|
buildChatwiseMessageCountJs,
|
|
buildChatwiseResponseAfterJs,
|
|
requirePositiveTimeout,
|
|
scoreChatwiseComposerCandidate,
|
|
selectBestChatwiseComposer,
|
|
} from './utils.js';
|
|
import { askCommand } from './ask.js';
|
|
|
|
function candidate(overrides = {}) {
|
|
return {
|
|
index: 0,
|
|
hidden: false,
|
|
role: 'textbox',
|
|
classes: 'cm-content cm-lineWrapping',
|
|
editorClasses: 'cm-editor',
|
|
ariaLabel: '',
|
|
placeholder: '',
|
|
text: '',
|
|
rect: { y: 0, h: 30 },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function runBrowserScript(html, script) {
|
|
const dom = new JSDOM(html, { url: 'app://chatwise.local/', runScripts: 'outside-only' });
|
|
Object.defineProperty(dom.window.HTMLElement.prototype, 'offsetWidth', { configurable: true, get: () => 400 });
|
|
Object.defineProperty(dom.window.HTMLElement.prototype, 'offsetHeight', { configurable: true, get: () => 32 });
|
|
dom.window.HTMLElement.prototype.getClientRects = () => [{ length: 1 }];
|
|
dom.window.document.execCommand = () => false;
|
|
return { dom, result: dom.window.eval(script) };
|
|
}
|
|
|
|
function makePage(evaluateResults = []) {
|
|
const evaluate = vi.fn();
|
|
for (const result of evaluateResults) evaluate.mockResolvedValueOnce(result);
|
|
evaluate.mockResolvedValue(null);
|
|
return {
|
|
evaluate,
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
}
|
|
|
|
describe('chatwise composer selection', () => {
|
|
it('prefers the main composer over auxiliary contenteditable editors', () => {
|
|
const mainComposer = candidate({
|
|
index: 0,
|
|
placeholder: 'placeholder Enter a message here, press ⏎ to send',
|
|
rect: { y: 860, h: 32 },
|
|
});
|
|
const optionalDescription = candidate({
|
|
index: 1,
|
|
placeholder: 'placeholder Optional description',
|
|
editorClasses: 'cm-editor simple-editor',
|
|
rect: { y: 400, h: 32 },
|
|
});
|
|
const userContext = candidate({
|
|
index: 2,
|
|
text: '# User Context Document',
|
|
editorClasses: 'cm-editor simple-editor',
|
|
rect: { y: 460, h: 1200 },
|
|
});
|
|
|
|
expect(scoreChatwiseComposerCandidate(mainComposer, 900)).toBeGreaterThan(
|
|
scoreChatwiseComposerCandidate(optionalDescription, 900),
|
|
);
|
|
expect(scoreChatwiseComposerCandidate(mainComposer, 900)).toBeGreaterThan(
|
|
scoreChatwiseComposerCandidate(userContext, 900),
|
|
);
|
|
|
|
expect(selectBestChatwiseComposer([
|
|
optionalDescription,
|
|
userContext,
|
|
mainComposer,
|
|
], 900)?.index).toBe(0);
|
|
});
|
|
|
|
it('rejects hidden or low-confidence candidates instead of injecting into the wrong editor', () => {
|
|
expect(selectBestChatwiseComposer([
|
|
candidate({
|
|
index: 0,
|
|
hidden: true,
|
|
placeholder: 'Enter a message here, press ⏎ to send',
|
|
rect: { y: 860, h: 32 },
|
|
}),
|
|
], 900)).toBeNull();
|
|
|
|
expect(selectBestChatwiseComposer([
|
|
candidate({
|
|
index: 1,
|
|
placeholder: 'Optional description',
|
|
editorClasses: 'cm-editor simple-editor',
|
|
rect: { y: 860, h: 32 },
|
|
}),
|
|
candidate({
|
|
index: 2,
|
|
text: '# User Context Document',
|
|
editorClasses: 'cm-editor simple-editor',
|
|
rect: { y: 870, h: 32 },
|
|
}),
|
|
], 900)).toBeNull();
|
|
});
|
|
|
|
it('injects text into the scored main composer instead of the last contenteditable', () => {
|
|
const html = `
|
|
<div class="cm-editor simple-editor">
|
|
<div class="cm-placeholder">Optional description</div>
|
|
<div id="optional" contenteditable="true" role="textbox"></div>
|
|
</div>
|
|
<div class="cm-editor">
|
|
<div class="cm-placeholder">Enter a message here, press ⏎ to send</div>
|
|
<div id="main" class="cm-content" contenteditable="true" role="textbox"></div>
|
|
</div>
|
|
<div class="cm-editor simple-editor">
|
|
<div id="context" contenteditable="true" role="textbox"># User Context Document</div>
|
|
</div>
|
|
`;
|
|
|
|
const { dom, result } = runBrowserScript(html, buildChatwiseInjectTextJs('hello'));
|
|
|
|
expect(result).toBe(true);
|
|
expect(dom.window.document.querySelector('#main')?.textContent).toBe('hello');
|
|
expect(dom.window.document.querySelector('#optional')?.textContent).toBe('');
|
|
expect(dom.window.document.querySelector('#context')?.textContent).toBe('# User Context Document');
|
|
});
|
|
|
|
it('fails injection when only auxiliary editors are present', () => {
|
|
const html = `
|
|
<div class="cm-editor simple-editor">
|
|
<div class="cm-placeholder">Optional description</div>
|
|
<div id="optional" contenteditable="true" role="textbox"></div>
|
|
</div>
|
|
<div class="cm-editor simple-editor">
|
|
<div id="context" contenteditable="true" role="textbox"># User Context Document</div>
|
|
</div>
|
|
`;
|
|
|
|
const { dom, result } = runBrowserScript(html, buildChatwiseInjectTextJs('hello'));
|
|
|
|
expect(result).toBe(false);
|
|
expect(dom.window.document.querySelector('#optional')?.textContent).toBe('');
|
|
expect(dom.window.document.querySelector('#context')?.textContent).toBe('# User Context Document');
|
|
});
|
|
|
|
it('reads only real message wrapper content after the previous count', () => {
|
|
const html = `
|
|
<div class="group/message">old message</div>
|
|
<div class="timestamp">12:00</div>
|
|
<div class="group/message">new assistant answer</div>
|
|
`;
|
|
|
|
expect(runBrowserScript(html, buildChatwiseMessageCountJs()).result).toBe(2);
|
|
expect(runBrowserScript(html, buildChatwiseResponseAfterJs(1, 'user prompt')).result).toBe('new assistant answer');
|
|
});
|
|
|
|
it('does not treat the user prompt wrapper as an assistant response', () => {
|
|
const html = `
|
|
<div class="group/message">old message</div>
|
|
<div class="group/message">user prompt</div>
|
|
`;
|
|
|
|
expect(runBrowserScript(html, buildChatwiseResponseAfterJs(1, 'user prompt')).result).toBeNull();
|
|
});
|
|
|
|
it('validates timeout explicitly', () => {
|
|
expect(requirePositiveTimeout(30)).toBe(30);
|
|
expect(() => requirePositiveTimeout('30')).toThrow(ArgumentError);
|
|
expect(() => requirePositiveTimeout(0)).toThrow(ArgumentError);
|
|
});
|
|
|
|
it('fails fast when ask times out instead of returning a System success row', async () => {
|
|
const page = makePage([
|
|
1,
|
|
true,
|
|
null,
|
|
]);
|
|
|
|
await expect(askCommand.func(page, { text: 'hello', timeout: 1 }))
|
|
.rejects.toBeInstanceOf(TimeoutError);
|
|
});
|
|
});
|