diff --git a/extension/background.ts b/extension/background.ts index aad09f7b5..fa7f1d0f2 100644 --- a/extension/background.ts +++ b/extension/background.ts @@ -32,6 +32,19 @@ chrome.runtime.onMessage.addListener(async (request, sender, response) => { } rcxMain.onTabSelect(sender.tab.id); break; + case 'forceDocsHtml?': + console.log('forceDocsHtml?'); + if (rcxMain.enabled === 1) { + response(true); + chrome.tabs.sendMessage(sender.tab!.id!, { + type: 'showPopup', + text: ` + rikaikun is forcing Google Docs to render using HTML instead of canvas.
+ rikaikun can't work with canvas mode but if you need that mode, please disable rikaikun. + `, + }); + } + break; case 'xsearch': console.log('xsearch'); response(rcxMain.search(request.text, request.dictOption)); @@ -70,3 +83,5 @@ chrome.runtime.onMessage.addListener(async (request, sender, response) => { // Clear browser action badge text on first load // Chrome preserves last state which is usually 'On' chrome.browserAction.setBadgeText({ text: '' }); + +export { rcxMainPromise as TestOnlyRxcMainPromise }; diff --git a/extension/docs-html-fallback.ts b/extension/docs-html-fallback.ts new file mode 100644 index 000000000..9881838aa --- /dev/null +++ b/extension/docs-html-fallback.ts @@ -0,0 +1,23 @@ +function forceHtml(force: boolean) { + if (!force) { + return; + } + console.log( + 'rikaikun is forcing Docs to use HTML instead of canvas for rendering.' + ); + const injectedCode = `(function() {window['_docs_force_html_by_ext'] = '${chrome.runtime.id}';})();`; + + const script = document.createElement('script'); + + script.textContent = injectedCode; + + // Usually, `document.head` isn't guaranteed to be present when content_scripts run but in this case + // we're running inside a callback so it should be 100% safe. + document.head.appendChild(script); +} + +// This check allows the user to get newer Docs Canvas without disabling rikaikun. +// This delays when the forcing code is injected but it seems to be early enough in practice. +chrome.runtime.sendMessage({ type: 'forceDocsHtml?' }, forceHtml); + +export { forceHtml as TestOnlyForceHtml }; diff --git a/extension/manifest.json b/extension/manifest.json index 6f9852a89..39885c532 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -26,6 +26,12 @@ "match_about_blank": true, "js": ["rikaicontent.js"], "all_frames": true + }, + { + "matches": ["https://docs.google.com/*"], + "js": ["docs-html-fallback.js"], + "run_at": "document_start", + "all_frames": true } ], "web_accessible_resources": [ diff --git a/extension/test/background_test.ts b/extension/test/background_test.ts new file mode 100644 index 000000000..7ea81784e --- /dev/null +++ b/extension/test/background_test.ts @@ -0,0 +1,88 @@ +import { RcxMain } from '../rikaichan'; +import { expect, use } from '@esm-bundle/chai'; +import chrome from 'sinon-chrome'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; + +use(sinonChai); + +let rcxMain: RcxMain; + +describe('background.ts', () => { + before(async () => { + // Resolve config fetch with minimal config object. + chrome.storage.sync.get.yields({ kanjiInfo: [] }); + // Imports only run once so run in `before` to make it deterministic. + rcxMain = await (await import('../background')).TestOnlyRxcMainPromise; + }); + + beforeEach(() => { + // Only reset the spies we're using since we need to preserve + // the state of `chrome.runtime.onMessage.addListener` for invoking + // the core functionality of background.ts. + chrome.tabs.sendMessage.reset(); + }); + + describe('when sent "forceDocsHtml?" message', () => { + it('should not call response callback when rikaikun disabled', async () => { + rcxMain.enabled = 0; + const responseCallback = sinon.spy(); + + await sendMessageToBackground({ + type: 'forceDocsHtml?', + responseCallback: responseCallback, + }); + + expect(responseCallback).to.have.not.been.called; + }); + + it('should not send "showPopup" message when rikaikun disabled', async () => { + rcxMain.enabled = 0; + + await sendMessageToBackground({ + type: 'forceDocsHtml?', + }); + + expect(chrome.tabs.sendMessage).to.have.not.been.called; + }); + + it('should pass true to response callback when rikaikun enabled', async () => { + rcxMain.enabled = 1; + const responseCallback = sinon.spy(); + + await sendMessageToBackground({ + type: 'forceDocsHtml?', + responseCallback: responseCallback, + }); + + expect(responseCallback).to.have.been.calledOnceWith(true); + }); + + it('should send "showPopup" message when rikaikun enabled', async () => { + rcxMain.enabled = 1; + + await sendMessageToBackground({ + type: 'forceDocsHtml?', + }); + + expect(chrome.tabs.sendMessage).to.have.been.calledWith( + /* tabId= */ sinon.match.any, + sinon.match({ type: 'showPopup' }) + ); + }); + }); +}); + +async function sendMessageToBackground({ + type, + responseCallback = () => {}, +}: { + type: string; + responseCallback?: Function; +}): Promise { + return await chrome.runtime.onMessage.addListener.yield( + { type: type }, + { tab: { id: 0 } }, + responseCallback + ); +} diff --git a/extension/test/docs-html-fallback_test.ts b/extension/test/docs-html-fallback_test.ts new file mode 100644 index 000000000..75987a54a --- /dev/null +++ b/extension/test/docs-html-fallback_test.ts @@ -0,0 +1,41 @@ +//import '../background'; +import { expect, use } from '@esm-bundle/chai'; +import chrome from 'sinon-chrome'; + +declare global { + interface Window { + _docs_force_html_by_ext?: string; + } +} + +let forceHtmlCallback: (force: boolean) => void; + +describe('docs-html-fallback.ts after sending `forceDocsHtml?` message', () => { + before(async () => { + await import('../docs-html-fallback'); + forceHtmlCallback = chrome.runtime.sendMessage.args[0][1]; + }); + + beforeEach(() => { + chrome.reset(); + window._docs_force_html_by_ext = undefined; + }); + + describe('when `forceHtml` callback is called with `false`', () => { + it('should not add special property to window object', async () => { + forceHtmlCallback(false); + + expect(window._docs_force_html_by_ext).to.be.undefined; + }); + }); + + describe('when `forceHtml` callback is called with `true`', () => { + it('should set special property to rikaikun extension ID', async () => { + chrome.runtime.id = 'test_special_id'; + + forceHtmlCallback(true); + + expect(window._docs_force_html_by_ext).to.equal(chrome.runtime.id); + }); + }); +}); diff --git a/snowpack.config.js b/snowpack.config.js index 6a4beaf5c..3997996c9 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -15,7 +15,7 @@ const config = { list: [ //Remove test only export from rikaicontent { - from: /export.*TestOnlyRcxContent.*\n/, + from: /export.*TestOnly.*\n/, to: '', }, ],