diff --git a/extension/rikaicontent.ts b/extension/rikaicontent.ts index b09922d0f..ca670c801 100644 --- a/extension/rikaicontent.ts +++ b/extension/rikaicontent.ts @@ -1094,7 +1094,14 @@ class RcxContent { fake.scrollLeft = eventTarget.scrollLeft; } // Calculate range and friends here after we've made our fake textarea/input divs. - range = document.caretRangeFromPoint(ev.clientX, ev.clientY); + range = document.caretRangeFromPoint( + ev.clientX, + ev.clientY + ) as Range | null; + // If we don't have a valid range, don't do any more work + if (range === null) { + return; + } const startNode = range.startContainer; ro = range.startOffset; diff --git a/extension/test/rikaicontent_test.ts b/extension/test/rikaicontent_test.ts index e0fd45250..e2a29c5a3 100644 --- a/extension/test/rikaicontent_test.ts +++ b/extension/test/rikaicontent_test.ts @@ -1,61 +1,80 @@ +import { Config } from '../configuration'; import { TestOnlyRcxContent } from '../rikaicontent'; import { expect, use } from '@esm-bundle/chai'; import chrome from 'sinon-chrome'; +import simulant from 'simulant'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; use(sinonChai); -describe('RcxContent.show', () => { +let rcxContent = new TestOnlyRcxContent(); + +describe('RcxContent', () => { beforeEach(() => { chrome.reset(); + rcxContent = new TestOnlyRcxContent(); + // Default enable rcxContent since no tests care about that now. + rcxContent.enableTab({ showOnKey: '' } as Config); }); + describe('.show', () => { + describe('when given Japanese word interrupted with text wrapped by `display: none`', () => { + it('sends "xsearch" message with invisible text omitted', () => { + const span = insertHtmlIntoDomAndReturnFirstTextNode( + 'test' + ); + + executeShowForGivenNode(rcxContent, span); - describe('when given Japanese word interrupted with text wrapped by `display: none`', () => { - it('sends "xsearch" message with invisible text omitted', () => { - const rcxContent = new TestOnlyRcxContent(); - const span = insertHtmlIntoDomAndReturnFirstTextNode( - 'test' - ); + expect(chrome.runtime.sendMessage).to.have.been.calledWith( + sinon.match({ type: 'xsearch', text: '試す' }), + sinon.match.any + ); + }); + }); - executeShowForGivenNode(rcxContent, span); + describe('when given Japanese word interrupted with text wrapped by `visibility: hidden`', () => { + it('sends "xsearch" message with invisible text omitted', () => { + const span = insertHtmlIntoDomAndReturnFirstTextNode( + 'test' + ); - expect(chrome.runtime.sendMessage).to.have.been.calledWith( - sinon.match({ type: 'xsearch', text: '試す' }), - sinon.match.any - ); + executeShowForGivenNode(rcxContent, span); + + expect(chrome.runtime.sendMessage).to.have.been.calledWith( + sinon.match({ type: 'xsearch', text: '試す' }), + sinon.match.any + ); + }); }); - }); - describe('when given Japanese word interrupted with text wrapped by `visibility: hidden`', () => { - it('sends "xsearch" message with invisible text omitted', () => { - const rcxContent = new TestOnlyRcxContent(); - const span = insertHtmlIntoDomAndReturnFirstTextNode( - 'test' - ); + describe('when given Japanese word is interrupted with text wrapped by visible span', () => { + it('sends "xsearch" message with all text included', () => { + const rcxContent = new TestOnlyRcxContent(); + const span = insertHtmlIntoDomAndReturnFirstTextNode( + 'test' + ); - executeShowForGivenNode(rcxContent, span); + executeShowForGivenNode(rcxContent, span); - expect(chrome.runtime.sendMessage).to.have.been.calledWith( - sinon.match({ type: 'xsearch', text: '試す' }), - sinon.match.any - ); + expect(chrome.runtime.sendMessage).to.have.been.calledWith( + sinon.match({ type: 'xsearch', text: '試testす' }), + sinon.match.any + ); + }); }); }); - describe('when given Japanese word is interrupted with text wrapped by visible span', () => { - it('sends "xsearch" message with all text included', () => { - const rcxContent = new TestOnlyRcxContent(); - const span = insertHtmlIntoDomAndReturnFirstTextNode( - 'test' - ); + describe('mousemove', () => { + it('handled without logging errors if `caretRangeFromPoint` returns null', () => { + sinon + .stub(document, 'caretRangeFromPoint') + .returns(null as unknown as Range); + sinon.spy(console, 'log'); - executeShowForGivenNode(rcxContent, span); + simulant.fire(document, 'mousemove'); - expect(chrome.runtime.sendMessage).to.have.been.calledWith( - sinon.match({ type: 'xsearch', text: '試testす' }), - sinon.match.any - ); + expect(console.log).to.not.have.been.called; }); }); }); diff --git a/package-lock.json b/package-lock.json index f6fb48c40..eb3ac045b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1785,6 +1785,12 @@ "@types/node": "*" } }, + "@types/simulant": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@types/simulant/-/simulant-0.2.0.tgz", + "integrity": "sha512-pQcnO5/JMR9KEnQGuYkDNQ9IDFAp0nrCfCjxqZ03WY2QDcuMPR6w0VpL6MO5VQEn93YkNCW9nTuRl/q0+iasVg==", + "dev": true + }, "@types/sinon": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.2.tgz", @@ -12329,6 +12335,12 @@ } } }, + "simulant": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simulant/-/simulant-0.2.2.tgz", + "integrity": "sha1-8bzlJxK2p6DaON392n6DsgsdoB4=", + "dev": true + }, "sinon": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", diff --git a/package.json b/package.json index 7a972b357..6c4715f50 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@types/chrome": "0.0.147", "@types/mocha": "^8.2.2", "@types/node": "^16.3.3", + "@types/simulant": "^0.2.0", "@types/sinon-chai": "^3.2.5", "@types/sinon-chrome": "^2.2.10", "@web/test-runner": "^0.13.13", @@ -78,6 +79,7 @@ "prettier-plugin-jsdoc": "^0.3.23", "semantic-release": "^17.4.4", "semantic-release-chrome": "^1.1.3", + "simulant": "^0.2.2", "sinon": "^7.5.0", "sinon-chai": "^3.7.0", "sinon-chrome": "^3.0.1",