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: '',
},
],