diff --git a/content-scripts/text-injection-all-providers.js b/content-scripts/text-injection-all-providers.js index 5152706..6a4acbc 100644 --- a/content-scripts/text-injection-all-providers.js +++ b/content-scripts/text-injection-all-providers.js @@ -172,9 +172,10 @@ ], gemini: [ 'button[aria-label="Send message"]', + 'button[aria-label="发送"]', 'button.send-button', 'button[mattooltip="Send message"]', - '.input-area-container button:has(mat-icon)', + '.input-area-container button:has(mat-icon[fonticon="arrow_upward"])', 'button[aria-label="Submit"]' ], grok: [ diff --git a/tests/gemini-content-script.test.js b/tests/gemini-content-script.test.js new file mode 100644 index 0000000..833a1af --- /dev/null +++ b/tests/gemini-content-script.test.js @@ -0,0 +1,145 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; + +const contentScriptSource = readFileSync( + resolve(process.cwd(), 'content-scripts/text-injection-all-providers.js'), + 'utf8' +); + +function markVisible(element) { + Object.defineProperty(element, 'offsetParent', { + configurable: true, + get: () => document.body, + }); +} + +function dispatchMultiPanelMessage(payload) { + window.dispatchEvent(new MessageEvent('message', { data: payload })); +} + +const sampleImage = { + name: 'sample.png', + type: 'image/png', + dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2Z2ioAAAAASUVORK5CYII=', +}; + +function createGeminiDom({ sendLabel = '发送', sendDisabled = false, includeSend = true } = {}) { + document.body.innerHTML = ` +
+
+ + ${includeSend ? ` + + ` : ''} +
+ `; + + const editor = document.querySelector('.ql-editor'); + const uploadButton = document.getElementById('gemini-upload'); + const sendButton = document.getElementById('gemini-send'); + + [editor, uploadButton, sendButton].filter(Boolean).forEach(markVisible); + if (sendButton) { + sendButton.disabled = sendDisabled; + } + + return { editor, uploadButton, sendButton }; +} + +describe('Gemini content script integration', () => { + beforeAll(() => { + window.eval(contentScriptSource); + }); + + beforeEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); + window.happyDOM.setURL('https://gemini.google.com/app'); + }); + + it.each([ + ['Chinese aria label', '发送'], + ['English aria label', 'Send message'], + ['locale-independent icon fallback', 'Envoyer'], + ])('clicks the Gemini send button using the %s', (_name, sendLabel) => { + const { uploadButton, sendButton } = createGeminiDom({ sendLabel }); + const uploadClickSpy = vi.fn(); + const sendClickSpy = vi.fn(); + uploadButton.addEventListener('click', uploadClickSpy); + sendButton.addEventListener('click', sendClickSpy); + + dispatchMultiPanelMessage({ type: 'TRIGGER_SEND', context: 'multi-panel' }); + + expect(sendClickSpy).toHaveBeenCalledTimes(1); + expect(uploadClickSpy).not.toHaveBeenCalled(); + }); + + it('uses the verified Gemini send button after text auto-submit', async () => { + vi.useFakeTimers(); + const { editor, uploadButton, sendButton } = createGeminiDom(); + const uploadClickSpy = vi.fn(); + const sendClickSpy = vi.fn(); + uploadButton.addEventListener('click', uploadClickSpy); + sendButton.addEventListener('click', sendClickSpy); + + dispatchMultiPanelMessage({ + type: 'INJECT_TEXT', + text: 'hello gemini', + autoSubmit: true, + context: 'multi-panel', + }); + await vi.advanceTimersByTimeAsync(500); + + expect(editor.textContent).toContain('hello gemini'); + expect(sendClickSpy).toHaveBeenCalledTimes(1); + expect(uploadClickSpy).not.toHaveBeenCalled(); + }); + + it('keeps Gemini image injection on the editor paste and drop path', async () => { + vi.useFakeTimers(); + const { editor, uploadButton, sendButton } = createGeminiDom(); + const pasteSpy = vi.fn(); + const dropSpy = vi.fn(); + const uploadClickSpy = vi.fn(); + const sendClickSpy = vi.fn(); + editor.addEventListener('paste', pasteSpy); + editor.addEventListener('drop', dropSpy); + uploadButton.addEventListener('click', uploadClickSpy); + sendButton.addEventListener('click', sendClickSpy); + + dispatchMultiPanelMessage({ + type: 'INJECT_TEXT_WITH_IMAGES', + text: '', + images: [sampleImage], + autoSubmit: false, + context: 'multi-panel', + }); + await vi.runAllTimersAsync(); + + expect(pasteSpy).toHaveBeenCalledTimes(1); + expect(dropSpy).toHaveBeenCalledTimes(1); + expect(uploadClickSpy).not.toHaveBeenCalled(); + expect(sendClickSpy).not.toHaveBeenCalled(); + }); + + it.each([ + ['missing', { includeSend: false }], + ['disabled', { sendDisabled: true }], + ])('does not click the upload button when the send button is %s', (_name, options) => { + const { uploadButton, sendButton } = createGeminiDom(options); + const uploadClickSpy = vi.fn(); + const sendClickSpy = vi.fn(); + uploadButton.addEventListener('click', uploadClickSpy); + sendButton?.addEventListener('click', sendClickSpy); + + dispatchMultiPanelMessage({ type: 'TRIGGER_SEND', context: 'multi-panel' }); + + expect(sendClickSpy).not.toHaveBeenCalled(); + expect(uploadClickSpy).not.toHaveBeenCalled(); + }); +});