diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 5ff0e7c3..21883430 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -24,3 +24,5 @@ jobs: run: yarn test env: CI: true + # for jest + FORCE_COLOR: true diff --git a/__tests__/ButtonSetter.test.js b/__tests__/ButtonSetter.test.js index 0f41ec77..b63677e2 100644 --- a/__tests__/ButtonSetter.test.js +++ b/__tests__/ButtonSetter.test.js @@ -1,14 +1,14 @@ // import * as main from '../src/main'; -import { SHOW_ON_TIMELINE, isFalse, SHOW_ON_TWEET_DETAIL } from '../src/constants'; +import { SHOW_ON_TIMELINE, isFalse, SHOW_ON_TWEET_DETAIL, isTrue } from '../src/constants'; import { ButtonSetter } from '../src/ButtonSetter'; function makeAllEnabledOptions() { return { - SHOW_ON_TIMELINE: 'istrue', - SHOW_ON_TWEET_DETAIL: 'istrue', - SHOW_ON_TWEETDECK_TIMELINE: 'istrue', - SHOW_ON_TWEETDECK_TWEET_DETAIL: 'istrue', - STRIP_IMAGE_SUFFIX: 'istrue', + SHOW_ON_TIMELINE: isTrue, + SHOW_ON_TWEET_DETAIL: isTrue, + SHOW_ON_TWEETDECK_TIMELINE: isTrue, + SHOW_ON_TWEETDECK_TWEET_DETAIL: isTrue, + STRIP_IMAGE_SUFFIX: isTrue, }; } diff --git a/__tests__/ButtonSetterTweetDeck.test.js b/__tests__/ButtonSetterTweetDeck.test.js index 8097e6e6..cc2efcaf 100644 --- a/__tests__/ButtonSetterTweetDeck.test.js +++ b/__tests__/ButtonSetterTweetDeck.test.js @@ -1,13 +1,13 @@ -import { SHOW_ON_TWEETDECK_TIMELINE, isFalse, SHOW_ON_TWEETDECK_TWEET_DETAIL } from '../src/constants'; +import { SHOW_ON_TWEETDECK_TIMELINE, isFalse, SHOW_ON_TWEETDECK_TWEET_DETAIL, isTrue } from '../src/constants'; import { ButtonSetterTweetDeck } from '../src/ButtonSetterTweetDeck'; function makeAllEnabledOptions() { return { - SHOW_ON_TIMELINE: 'istrue', - SHOW_ON_TWEET_DETAIL: 'istrue', - SHOW_ON_TWEETDECK_TIMELINE: 'istrue', - SHOW_ON_TWEETDECK_TWEET_DETAIL: 'istrue', - STRIP_IMAGE_SUFFIX: 'istrue', + SHOW_ON_TIMELINE: isTrue, + SHOW_ON_TWEET_DETAIL: isTrue, + SHOW_ON_TWEETDECK_TIMELINE: isTrue, + SHOW_ON_TWEETDECK_TWEET_DETAIL: isTrue, + STRIP_IMAGE_SUFFIX: isTrue, }; } diff --git a/__tests__/Constants.test.js b/__tests__/Constants.test.js index de41865f..9e6d947d 100644 --- a/__tests__/Constants.test.js +++ b/__tests__/Constants.test.js @@ -13,7 +13,7 @@ import { isFalse, OPTION_KEYS, OPTIONS_TEXT, - userjsOptions, + initialOptions, isTwitter, isTweetdeck, isImageTab, @@ -49,15 +49,15 @@ describe('定数', () => { }); it('userjs用の設定項目の初期値は全部真', () => { - expect(userjsOptions).toStrictEqual({ + expect(initialOptions).toStrictEqual({ // 公式Web - SHOW_ON_TIMELINE: 'istrue', - SHOW_ON_TWEET_DETAIL: 'istrue', + SHOW_ON_TIMELINE: isTrue, + SHOW_ON_TWEET_DETAIL: isTrue, // TweetDeck - SHOW_ON_TWEETDECK_TIMELINE: 'istrue', - SHOW_ON_TWEETDECK_TWEET_DETAIL: 'istrue', + SHOW_ON_TWEETDECK_TIMELINE: isTrue, + SHOW_ON_TWEETDECK_TWEET_DETAIL: isTrue, // 画像ページ - STRIP_IMAGE_SUFFIX: 'istrue', + STRIP_IMAGE_SUFFIX: isTrue, }); expect(OPTION_KEYS).toStrictEqual([ diff --git a/__tests__/Utils.test.js b/__tests__/Utils.test.js index a0a0debe..5b957251 100644 --- a/__tests__/Utils.test.js +++ b/__tests__/Utils.test.js @@ -1,5 +1,8 @@ // import * as main from '../src/main'; -import { isTrue, isFalse, OPTION_KEYS } from '../src/constants'; + +import { chrome } from 'jest-chrome'; + +import { OPTION_KEYS, initialOptionsBool } from '../src/constants'; import { printException, collectUrlParams, @@ -244,40 +247,25 @@ describe('Utils', () => { describe('updateOptions', () => { describe('Chrome拡張機能のとき', () => { - const originalChrome = window.chrome; - beforeAll(() => { - delete window.chrome; - window.chrome = { runtime: { id: 'id' } }; - }); - afterAll(() => { - window.chrome = originalChrome; - }); + chrome.runtime.id = 'mock'; it('初期設定を取得できる', async () => { - const expected = {}; - OPTION_KEYS.forEach((key) => { - expected[key] = isTrue; - }); - window.chrome.runtime.sendMessage = jest.fn((_, callback) => callback({ data: {} })); - await expect(updateOptions()).resolves.toStrictEqual(expected); + chrome.runtime.sendMessage.mockImplementation((_, callback) => callback({ data: initialOptionsBool })); + await expect(updateOptions()).resolves.toStrictEqual(initialOptionsBool); }); it('設定した値を取得できる', async () => { - const expected = {}; + const expected = { ...initialOptionsBool }; OPTION_KEYS.forEach((key, i) => { - expected[key] = i % 2 === 0 ? isTrue : isFalse; + expected[key] = i % 2 === 0; }); - window.chrome.runtime.sendMessage = jest.fn((_, callback) => callback({ data: { ...expected } })); + chrome.runtime.sendMessage.mockImplementation((_, callback) => callback({ data: { ...expected } })); await expect(updateOptions()).resolves.toStrictEqual(expected); }); it('設定が取得できなかったら初期設定', async () => { - const expected = {}; - OPTION_KEYS.forEach((key) => { - expected[key] = isTrue; - }); - window.chrome.runtime.sendMessage = jest.fn((_, callback) => callback({})); - await expect(updateOptions()).resolves.toStrictEqual(expected); + chrome.runtime.sendMessage.mockImplementation((_, callback) => callback({})); + await expect(updateOptions()).resolves.toStrictEqual(initialOptionsBool); }); }); @@ -292,11 +280,7 @@ describe('Utils', () => { }); it('初期設定を取得できる', async () => { - const expected = {}; - OPTION_KEYS.forEach((key) => { - expected[key] = isTrue; - }); - await expect(updateOptions()).resolves.toStrictEqual(expected); + await expect(updateOptions()).resolves.toStrictEqual(initialOptionsBool); }); }); }); diff --git a/__tests__/options.test.js b/__tests__/options.test.js new file mode 100644 index 00000000..6f3320fa --- /dev/null +++ b/__tests__/options.test.js @@ -0,0 +1,73 @@ +import { chrome } from 'jest-chrome'; +import { + initialOptions, + initialOptionsBool, + isFalse, + MIGRATED_TO_CHROME_STORAGE, + SHOW_ON_TWEETDECK_TIMELINE, + SHOW_ON_TIMELINE, + SHOW_ON_TWEETDECK_TWEET_DETAIL, +} from '../src/constants'; + +import { getOptions, setOptions } from '../src/extension-contexts/options'; + +let chromeStorage = {}; +chrome.storage.sync.set.mockImplementation((items) => { + chromeStorage = { ...chromeStorage, ...items }; +}); +chrome.storage.sync.get.mockImplementation((keys, callback) => { + if (typeof keys === 'string') { + callback({ [keys]: chromeStorage[keys] }); + } else { + callback(Object.fromEntries(Object.entries(chromeStorage).filter(([k, _]) => keys.find((key) => k === key)))); + } +}); +beforeEach(() => { + chromeStorage = {}; + localStorage.clear(); +}); + +describe('options', () => { + describe('getOptions', () => { + it('何もない状態で呼んだら, 初期値が返って, 初期値が保存されて, 移行済みになる', () => { + expect(getOptions()).resolves.toMatchObject(initialOptionsBool); + expect(chromeStorage).toMatchObject({ + ...initialOptionsBool, + [MIGRATED_TO_CHROME_STORAGE]: true, + }); + }); + it('未移行で, localStorageに設定があったら, localStorageの内容が移行されつつ返って, 移行済みになる', () => { + Object.entries({ ...initialOptions, [SHOW_ON_TWEETDECK_TIMELINE]: isFalse }).map(([k, v]) => { + localStorage.setItem(k, v); + }); + const expected = { ...initialOptionsBool, [SHOW_ON_TWEETDECK_TIMELINE]: false }; + expect(getOptions()).resolves.toMatchObject(expected); + expect(chromeStorage).toMatchObject({ + ...expected, + [MIGRATED_TO_CHROME_STORAGE]: true, + }); + }); + it('移行済みなら, 保存された設定が返る', () => { + Object.entries({ ...initialOptions, [SHOW_ON_TWEETDECK_TIMELINE]: isFalse }).map(([k, v]) => { + localStorage.setItem(k, v); + }); + chromeStorage = { + ...initialOptionsBool, + [SHOW_ON_TIMELINE]: false, + [MIGRATED_TO_CHROME_STORAGE]: true, + }; + const expected = { ...initialOptionsBool, [SHOW_ON_TIMELINE]: false }; + expect(getOptions()).resolves.toMatchObject(expected); + expect(chromeStorage).toMatchObject({ + ...expected, + [MIGRATED_TO_CHROME_STORAGE]: true, + }); + }); + }); + describe('setOptions', () => { + const expected = { ...initialOptionsBool, [SHOW_ON_TWEETDECK_TWEET_DETAIL]: false }; + setOptions(expected); + expect(chrome.storage.sync.set.mock.calls.length).toBe(1); + expect(chrome.storage.sync.set.mock.lastCall[0]).toBe(expected); + }); +}); diff --git a/__tests__/popup.test.js b/__tests__/popup.test.js index aeceb58e..85536c3d 100644 --- a/__tests__/popup.test.js +++ b/__tests__/popup.test.js @@ -3,25 +3,28 @@ import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }); const { shallow } = Enzyme; +import { chrome } from 'jest-chrome'; import { OPTIONS_TEXT, SHOW_ON_TIMELINE, - isFalse, SHOW_ON_TWEETDECK_TIMELINE, - isTrue, OPTION_KEYS, + initialOptionsBool, } from '../src/constants'; -import { Popup } from '../src/popup'; +import { Popup } from '../src/extension-contexts/popup'; + +let mockOptions = initialOptionsBool; +chrome.storage.sync.set.mockImplementation((newOptions) => { + mockOptions = { ...newOptions }; +}); +chrome.storage.sync.get.mockImplementation(() => mockOptions); describe('Popup', () => { it('render', () => { const optionsText = OPTIONS_TEXT; const optionKeys = OPTION_KEYS; - const optionsEnabled = {}; - optionKeys.forEach((key) => { - optionsEnabled[key] = true; - }); + const optionsEnabled = { ...initialOptionsBool }; const props = { optionsText, @@ -33,22 +36,13 @@ describe('Popup', () => { }); describe('保存ボタン押すと設定が保存される', () => { - window.localStorage = {}; - it('最初は空', () => { - expect(window.localStorage).toMatchObject({}); - }); + mockOptions = initialOptionsBool; const optionsText = OPTIONS_TEXT; const optionKeys = OPTION_KEYS; - const optionsEnabled = {}; - const expectOptions = {}; - optionKeys.forEach((key) => { - optionsEnabled[key] = true; - expectOptions[key] = isTrue; - }); + const optionsEnabled = { ...initialOptionsBool }; // 初期設定いっこOFFにしてみる optionsEnabled[SHOW_ON_TIMELINE] = false; - expectOptions[SHOW_ON_TIMELINE] = isFalse; const props = { optionsText, @@ -56,35 +50,31 @@ describe('Popup', () => { optionsEnabled, }; - window.chrome = { - tabs: { - query: jest.fn((_, callback) => { - callback([ - { - // 対象タブ - id: 1, - url: 'http://twitter.com', - }, - { - // 対象ではないタブ - id: 1, - url: 'http://google.com', - }, - { - // 対象ではないタブ - id: 1, - }, - { - // 対象ではないタブ - url: 'http://twitter.com', - }, - ]); - }), - sendMessage: jest.fn((id, option, callback) => { - callback('mock ok'); - }), - }, - }; + chrome.tabs.query.mockImplementation((_, callback) => { + callback([ + { + // 対象タブ + id: 1, + url: 'http://twitter.com', + }, + { + // 対象ではないタブ + id: 1, + url: 'http://google.com', + }, + { + // 対象ではないタブ + id: 1, + }, + { + // 対象ではないタブ + url: 'http://twitter.com', + }, + ]); + }); + chrome.tabs.sendMessage.mockImplementation((id, option, callback) => { + callback('mock ok'); + }); const wrapper = shallow(); it('渡した設定がそのまま保存される', () => { @@ -92,25 +82,30 @@ describe('Popup', () => { // 送りたいタブは正しい形式かつ対象ホストなタブのみ expect(window.chrome.tabs.query.mock.calls.length).toBe(1); - expect(window.localStorage).toMatchObject(expectOptions); + expect(mockOptions).toMatchObject(optionsEnabled); }); it('チェックボックスをクリックして保存すると設定変えられる', () => { wrapper.find(`.${SHOW_ON_TIMELINE}`).simulate('click'); wrapper.find(`.${SHOW_ON_TWEETDECK_TIMELINE}`).simulate('click'); - expectOptions[SHOW_ON_TIMELINE] = isTrue; - expectOptions[SHOW_ON_TWEETDECK_TIMELINE] = isFalse; wrapper.find('.saveSettingButton').simulate('click'); - expect(window.localStorage).toMatchObject(expectOptions); + expect(mockOptions).toMatchObject({ + ...optionsEnabled, + [SHOW_ON_TIMELINE]: true, + [SHOW_ON_TWEETDECK_TIMELINE]: false, + }); }); it('何度も設定変えられる', () => { wrapper.find(`.${SHOW_ON_TIMELINE}`).simulate('click'); - expectOptions[SHOW_ON_TIMELINE] = isFalse; wrapper.find('.saveSettingButton').simulate('click'); - expect(window.localStorage).toMatchObject(expectOptions); + expect(mockOptions).toMatchObject({ + ...optionsEnabled, + [SHOW_ON_TIMELINE]: false, + [SHOW_ON_TWEETDECK_TIMELINE]: false, + }); }); }); }); diff --git a/coverage/badge.svg b/coverage/badge.svg index 05828c95..fdcac574 100644 --- a/coverage/badge.svg +++ b/coverage/badge.svg @@ -1 +1 @@ -Coverage: 87.07%Coverage87.07% \ No newline at end of file +Coverage: 82.58%Coverage82.58% \ No newline at end of file diff --git a/dist/manifest.json b/dist/manifest.json index 497d889e..01c4e1f8 100644 --- a/dist/manifest.json +++ b/dist/manifest.json @@ -4,7 +4,7 @@ "version": "4.1.0", "description": "twitterの画像ツイートにボタンを追加する拡張機能。追加されたボタンを押すとツイートの画像を原寸で新しいタブに表示する。連絡先: @hogextend", "author": "hogashi", - "permissions": ["tabs"], + "permissions": ["tabs", "storage"], "icons": { "16": "icons/icon.png", "48": "icons/icon.png", diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..77067c71 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + coverageReporters: ['json-summary', 'lcov', 'text'], + setupFilesAfterEnv: ['./jest.setup.js'], +}; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..4acf7987 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1 @@ +Object.assign(global, require('jest-chrome')); diff --git a/package.json b/package.json index 76b76fca..1b94fd42 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.7", "jest": "29.3.1", + "jest-chrome": "^0.8.0", "jest-environment-jsdom": "^29.3.1", "make-coverage-badge": "1.2.0", "npm-run-all": "4.1.5", @@ -54,12 +55,5 @@ "react": "17.0.2", "react-dom": "17.0.2", "rome": "^11.0.0" - }, - "jest": { - "coverageReporters": [ - "json-summary", - "lcov", - "text" - ] } } diff --git a/src/ButtonSetter.ts b/src/ButtonSetter.ts index e11b2c86..474ecdca 100644 --- a/src/ButtonSetter.ts +++ b/src/ButtonSetter.ts @@ -1,9 +1,9 @@ -import { isFalse, Options, SHOW_ON_TIMELINE, SHOW_ON_TWEET_DETAIL } from './constants'; +import { OptionsBool, SHOW_ON_TIMELINE, SHOW_ON_TWEET_DETAIL } from './constants'; import { onOriginalButtonClick, printException, setStyle } from './utils'; export interface ButtonSetterType { - setButtonOnTimeline: (currentOptions: Options) => void; - setButtonOnTweetDetail: (currentOptions: Options) => void; + setButtonOnTimeline: (currentOptions: OptionsBool) => void; + setButtonOnTweetDetail: (currentOptions: OptionsBool) => void; } /** @@ -11,7 +11,7 @@ export interface ButtonSetterType { */ export class ButtonSetter implements ButtonSetterType { // タイムラインにボタン表示 - public setButtonOnTimeline(currentOptions: Options): void { + public setButtonOnTimeline(currentOptions: OptionsBool): void { // 昔のビューの処理はしばらく残す // ref: https://github.com/hogashi/twitterOpenOriginalImage/issues/32#issuecomment-578510155 if (document.querySelector('#react-root')) { @@ -22,7 +22,7 @@ export class ButtonSetter implements ButtonSetterType { } // ツイート詳細にボタン表示 - public setButtonOnTweetDetail(currentOptions: Options): void { + public setButtonOnTweetDetail(currentOptions: OptionsBool): void { // 昔のビューの処理はしばらく残す // TODO: Reactレイアウトでも実装する必要がある? // ref: https://github.com/hogashi/twitterOpenOriginalImage/issues/32#issuecomment-578510155 @@ -116,11 +116,9 @@ export class ButtonSetter implements ButtonSetterType { container.appendChild(button); } - private _setButtonOnTimeline(currentOptions: Options): void { + private _setButtonOnTimeline(currentOptions: OptionsBool): void { // タイムラインにボタン表示する設定がされているときだけ実行する - // - isTrue か 設定なし のとき ON - // - isFalse のとき OFF - if (!(currentOptions[SHOW_ON_TIMELINE] !== isFalse)) { + if (!currentOptions[SHOW_ON_TIMELINE]) { return; } const tweets = document.getElementsByClassName('js-stream-tweet'); @@ -161,11 +159,9 @@ export class ButtonSetter implements ButtonSetterType { }); } - private _setButtonOnTweetDetail(currentOptions: Options): void { + private _setButtonOnTweetDetail(currentOptions: OptionsBool): void { // ツイート詳細にボタン表示する設定がされているときだけ実行する - // - isTrue か 設定なし のとき ON - // - isFalse のとき OFF - if (!(currentOptions[SHOW_ON_TWEET_DETAIL] !== isFalse)) { + if (!currentOptions[SHOW_ON_TWEET_DETAIL]) { return; } const className = 'tooi-button-container-detail'; @@ -201,11 +197,9 @@ export class ButtonSetter implements ButtonSetterType { }); } - private _setButtonOnReactLayoutTimeline(currentOptions: Options): void { + private _setButtonOnReactLayoutTimeline(currentOptions: OptionsBool): void { // ツイート詳細にボタン表示する設定がされているときだけ実行する - // - isTrue か 設定なし のとき ON - // - isFalse のとき OFF - if (!(currentOptions[SHOW_ON_TIMELINE] !== isFalse)) { + if (!currentOptions[SHOW_ON_TIMELINE]) { return; } const className = 'tooi-button-container-react-timeline'; diff --git a/src/ButtonSetterTweetDeck.ts b/src/ButtonSetterTweetDeck.ts index 4ef6c1b4..201c744c 100644 --- a/src/ButtonSetterTweetDeck.ts +++ b/src/ButtonSetterTweetDeck.ts @@ -1,5 +1,5 @@ import { ButtonSetterType } from './ButtonSetter'; -import { SHOW_ON_TWEETDECK_TIMELINE, isFalse, SHOW_ON_TWEETDECK_TWEET_DETAIL, Options } from './constants'; +import { SHOW_ON_TWEETDECK_TIMELINE, SHOW_ON_TWEETDECK_TWEET_DETAIL, OptionsBool } from './constants'; import { printException, setStyle, onOriginalButtonClick } from './utils'; /** @@ -7,11 +7,9 @@ import { printException, setStyle, onOriginalButtonClick } from './utils'; */ export class ButtonSetterTweetDeck implements ButtonSetterType { // タイムラインにボタン表示 - public setButtonOnTimeline(currentOptions: Options): void { + public setButtonOnTimeline(currentOptions: OptionsBool): void { // タイムラインにボタン表示する設定がされているときだけ実行する - // - isTrue か 設定なし のとき ON - // - isFalse のとき OFF - if (!(currentOptions[SHOW_ON_TWEETDECK_TIMELINE] !== isFalse)) { + if (!currentOptions[SHOW_ON_TWEETDECK_TIMELINE]) { return; } // if タイムラインのツイートを取得できたら @@ -62,11 +60,9 @@ export class ButtonSetterTweetDeck implements ButtonSetterType { } // ツイート詳細にボタン表示 - public setButtonOnTweetDetail(currentOptions: Options): void { + public setButtonOnTweetDetail(currentOptions: OptionsBool): void { // ツイート詳細にボタン表示する設定がされているときだけ実行する - // - isTrue か 設定なし のとき ON - // - isFalse のとき OFF - if (!(currentOptions[SHOW_ON_TWEETDECK_TWEET_DETAIL] !== isFalse)) { + if (!currentOptions[SHOW_ON_TWEETDECK_TWEET_DETAIL]) { return; } // if ツイート詳細を取得できたら diff --git a/src/background.ts b/src/background.ts deleted file mode 100644 index 1155ff29..00000000 --- a/src/background.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { GET_LOCAL_STORAGE } from './constants'; -import { MessageRequest, MessageResponse } from './utils'; - -// バックグラウンドで実行される - -window.chrome.runtime.onMessage.addListener( - (request: MessageRequest, _, sendResponse: (res: MessageResponse) => void) => { - // console.log(chrome.runtime.lastError); - if (request.method === GET_LOCAL_STORAGE) { - sendResponse({ data: localStorage }); - } else { - sendResponse({ data: null }); - } - return true; - }, -); diff --git a/src/constants.ts b/src/constants.ts index 5c1dbaf1..72b341fc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,14 @@ +export interface OptionsBool { + // 公式Web + SHOW_ON_TIMELINE: boolean; + SHOW_ON_TWEET_DETAIL: boolean; + // TweetDeck + SHOW_ON_TWEETDECK_TIMELINE: boolean; + SHOW_ON_TWEETDECK_TWEET_DETAIL: boolean; + // 画像ページ + STRIP_IMAGE_SUFFIX: boolean; +} + export interface Options { // 公式Web SHOW_ON_TIMELINE: TooiBoolean; @@ -12,10 +23,24 @@ export interface Options { export type OptionsMaybe = { [key in keyof Options]?: TooiBoolean }; /** - * userjs 用の設定項目 + * 設定項目 + */ +export const initialOptionsBool: OptionsBool = { + // 公式Web + SHOW_ON_TIMELINE: true, + SHOW_ON_TWEET_DETAIL: true, + // TweetDeck + SHOW_ON_TWEETDECK_TIMELINE: true, + SHOW_ON_TWEETDECK_TWEET_DETAIL: true, + // 画像ページ + STRIP_IMAGE_SUFFIX: true, +}; + +/** + * 設定項目 * 'isfalse' とすると、その設定がオフになる */ -export const userjsOptions: Options = { +export const initialOptions: Options = { // 公式Web SHOW_ON_TIMELINE: 'istrue', SHOW_ON_TWEET_DETAIL: 'istrue', @@ -52,7 +77,7 @@ export const isTweetdeck = (): boolean => window.location.hostname === HOST_TWEE export const isImageTab = (): boolean => window.location.hostname === HOST_PBS_TWIMG_COM; /** これ自体がChrome拡張機能かどうか */ -export const isNativeChromeExtension = (): boolean => window.chrome?.runtime?.id !== undefined; +export const isNativeChromeExtension = (): boolean => chrome?.runtime?.id !== undefined; // 設定 @@ -78,3 +103,6 @@ export const OPTIONS_TEXT: { [key in keyof Options]: string } = { // 画像ページ STRIP_IMAGE_SUFFIX: '[Ctrl]+[s]で拡張子を校正', }; + +// chrome.storateへの移行が済んだかどうかのキー +export const MIGRATED_TO_CHROME_STORAGE = 'MIGRATED_TO_CHROME_STORAGE'; diff --git a/src/extension-contexts/background.ts b/src/extension-contexts/background.ts new file mode 100644 index 00000000..31456403 --- /dev/null +++ b/src/extension-contexts/background.ts @@ -0,0 +1,17 @@ +import { GET_LOCAL_STORAGE } from '../constants'; +import { getOptions } from './options'; +import { MessageRequest, MessageResponseBool } from '../utils'; + +// バックグラウンドで実行される + +chrome.runtime.onMessage.addListener((request: MessageRequest, _, sendResponse: (res: MessageResponseBool) => void) => { + // console.log(chrome.runtime.lastError); + if (request.method === GET_LOCAL_STORAGE) { + getOptions().then((options) => { + sendResponse({ data: options }); + }); + } else { + sendResponse({ data: null }); + } + return true; +}); diff --git a/src/extension-contexts/options.ts b/src/extension-contexts/options.ts new file mode 100644 index 00000000..6a967228 --- /dev/null +++ b/src/extension-contexts/options.ts @@ -0,0 +1,46 @@ +import { + OptionsBool, + initialOptions, + initialOptionsBool, + OPTION_KEYS, + isTrue, + MIGRATED_TO_CHROME_STORAGE, +} from '../constants'; + +export const setOptions = (options: OptionsBool): void => { + chrome.storage.sync.set(options, () => { + console.log('options set'); + }); +}; + +export const getOptions = (): Promise => { + return new Promise((resolve) => { + // chrome.storageから取ってきつつ, + // まだ移行してないときはlocalStorageあるいは初期値を移行 + chrome.storage.sync.get(MIGRATED_TO_CHROME_STORAGE, (isMigrated) => { + if (!isMigrated[MIGRATED_TO_CHROME_STORAGE]) { + const newOptions = { ...initialOptions, ...localStorage }; + // 真偽値にして移行する + const newOptionsBool = { ...initialOptionsBool }; + OPTION_KEYS.forEach((key) => { + newOptionsBool[key] = newOptions[key] === isTrue; + }); + chrome.storage.sync.set( + { + ...newOptionsBool, + [MIGRATED_TO_CHROME_STORAGE]: true, + }, + () => { + // 移行できたら新しい値を返す + resolve(newOptionsBool); + }, + ); + } else { + chrome.storage.sync.get(OPTION_KEYS, (got) => { + // 初期値をフォールバックとしておく + resolve({ ...initialOptionsBool, ...got }); + }); + } + }); + }); +}; diff --git a/src/popup.tsx b/src/extension-contexts/popup.tsx similarity index 77% rename from src/popup.tsx rename to src/extension-contexts/popup.tsx index 0a407a62..4ae7bb87 100644 --- a/src/popup.tsx +++ b/src/extension-contexts/popup.tsx @@ -11,8 +11,6 @@ import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; import { OPTION_KEYS, - isTrue, - isFalse, HOST_TWITTER_COM, HOST_MOBILE_TWITTER_COM, HOST_TWEETDECK_TWITTER_COM, @@ -24,8 +22,10 @@ import { SHOW_ON_TWEETDECK_TWEET_DETAIL, STRIP_IMAGE_SUFFIX, OPTIONS_TEXT, -} from './constants'; -import { printException } from './utils'; + OptionsBool, +} from '../constants'; +import { printException } from '../utils'; +import { getOptions, setOptions } from './options'; /* popup.js */ // ツールバー右に表示される拡張機能のボタンをクリック、または @@ -34,7 +34,7 @@ import { printException } from './utils'; interface Props { optionsText: { [key: string]: string }; optionKeys: typeof OPTION_KEYS; - optionsEnabled: { [key: string]: boolean }; + optionsEnabled: OptionsBool; } export const Popup = (props: Props): JSX.Element => { @@ -42,10 +42,8 @@ export const Popup = (props: Props): JSX.Element => { const [enabled, setEnabled] = useState(optionsEnabled); const onSave = useCallback(() => { - optionKeys.forEach((key) => { - localStorage[key] = enabled[key] ? isTrue : isFalse; - }); - window.chrome.tabs.query({}, (result) => + setOptions(enabled); + chrome.tabs.query({}, (result) => result.forEach((tab) => { // console.log(tab); if (!(tab.url && tab.id)) { @@ -60,7 +58,7 @@ export const Popup = (props: Props): JSX.Element => { // 送り先タブが拡張機能が動作する対象ではないならメッセージを送らない return; } - window.chrome.tabs.sendMessage(tab.id, { method: OPTION_UPDATED }, (response) => { + chrome.tabs.sendMessage(tab.id, { method: OPTION_UPDATED }, (response) => { // eslint-disable-next-line no-console console.log('res:', response); }); @@ -139,30 +137,23 @@ export const Popup = (props: Props): JSX.Element => { ); }; -const optionsText = OPTIONS_TEXT; -const optionKeys = OPTION_KEYS; -const optionsEnabled: { [key: string]: boolean } = {}; -optionKeys.forEach((key) => { - // 最初はどっちも機能オンであってほしい - // 最初は値が入っていないので、「if isfalseでないなら機能オン」とする - optionsEnabled[key] = localStorage[key] !== isFalse; -}); - -const props = { - optionsText, - optionKeys, - optionsEnabled, -}; +getOptions().then((optionsEnabled) => { + const props = { + optionsText: OPTIONS_TEXT, + optionKeys: OPTION_KEYS, + optionsEnabled, + }; -let root = document.getElementById('root'); -if (!root) { - root = document.createElement('div'); - root.id = 'root'; - const body = document.querySelector('body'); - if (body) { - body.appendChild(root); - } else { - printException('cant find body'); + let root = document.getElementById('root'); + if (!root) { + root = document.createElement('div'); + root.id = 'root'; + const body = document.querySelector('body'); + if (body) { + body.appendChild(root); + } else { + printException('cant find body'); + } } -} -ReactDOM.render(, document.getElementById('root')); + ReactDOM.render(, document.getElementById('root')); +}); diff --git a/src/utils.ts b/src/utils.ts index ab15fc12..088df2a8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,20 +3,21 @@ import { ButtonSetterTweetDeck } from './ButtonSetterTweetDeck'; import { GET_LOCAL_STORAGE, isNativeChromeExtension, - isTrue, isTweetdeck, - Options, - OptionsMaybe, - OPTION_KEYS, OPTION_UPDATED, STRIP_IMAGE_SUFFIX, - userjsOptions, + OptionsBool, + initialOptionsBool, } from './constants'; /** chrome.runtime.sendMessage で送るメッセージ */ export interface MessageRequest { method: string; } +/** chrome.runtime.sendMessage で返るメッセージ(真偽値版) */ +export interface MessageResponseBool { + data: OptionsBool | null; +} /** chrome.runtime.sendMessage で返るメッセージ */ export interface MessageResponse { data: { [key: string]: string } | null; @@ -176,38 +177,28 @@ export const getButtonSetter = (): ButtonSetterType => /** * 設定項目更新 - * background script に問い合わせて返ってきた値で options をつくって返す + * background script に問い合わせて返ってきた値で options (真偽値) をつくって返す */ -export const updateOptions = (): Promise => { +export const updateOptions = (): Promise => { // これ自体はChrome拡張機能でない(UserScriptとして読み込まれている)とき // 設定は変わりようがないので何もしない if (!isNativeChromeExtension()) { - return Promise.resolve(userjsOptions); + return Promise.resolve(initialOptionsBool); } - return new Promise((resolve) => { + return new Promise((resolve) => { const request: MessageRequest = { method: GET_LOCAL_STORAGE, }; - const callback = (response: MessageResponse): void => { + const callback = (response: MessageResponseBool): void => { // 何かおかしくて設定内容取ってこれなかったらデフォルトということにする - resolve(response?.data ? response.data : {}); + resolve(response?.data ? response.data : initialOptionsBool); }; - window.chrome.runtime.sendMessage(request, callback); - }).then((data: OptionsMaybe) => { - const newOptions: OptionsMaybe = {}; - // ここで全部埋めるので newOptions は Options になる - OPTION_KEYS.forEach((key) => { - newOptions[key] = data[key] || isTrue; - }); - - // console.log('get options (then): ', newOptions); // debug - - return newOptions as Options; + chrome.runtime.sendMessage(request, callback); }); }; /** Originalボタンおく */ -export const setOriginalButton = (options: Options): void => { +export const setOriginalButton = (options: OptionsBool): void => { // 実行の間隔(ms) const INTERVAL = 300; @@ -215,7 +206,7 @@ export const setOriginalButton = (options: Options): void => { const buttonSetter = getButtonSetter(); // ボタンを設置 - const setButton = (currentOptions: Options): void => { + const setButton = (currentOptions: OptionsBool): void => { // console.log('setButton: ' + currentOptions['SHOW_ON_TIMELINE'] + ' ' + currentOptions['SHOW_ON_TWEET_DETAIL']) // debug buttonSetter.setButtonOnTimeline(currentOptions); buttonSetter.setButtonOnTweetDetail(currentOptions); @@ -223,7 +214,7 @@ export const setOriginalButton = (options: Options): void => { let isInterval = false; let deferred = false; - const setButtonWithInterval = (currentOptions: Options): void => { + const setButtonWithInterval = (currentOptions: OptionsBool): void => { // 短時間に何回も実行しないようインターバルを設ける if (isInterval) { deferred = true; @@ -254,12 +245,12 @@ export const setOriginalButton = (options: Options): void => { // これ自体がChrome拡張機能のときだけ設置する // (Chrome拡張機能でないときは設定反映できる機構ないので) if (isNativeChromeExtension()) { - window.chrome.runtime.onMessage.addListener((request, _, sendResponse) => { + chrome.runtime.onMessage.addListener((request, _, sendResponse) => { // Unchecked runtime.lastError みたいなエラーが出ることがあるので, // ひとまず console.log で出すようにしてみている - if (window.chrome.runtime.lastError !== undefined) { + if (chrome.runtime.lastError !== undefined) { // eslint-disable-next-line no-console - console.log(window.chrome.runtime.lastError); + console.log(chrome.runtime.lastError); } if (request.method === OPTION_UPDATED) { updateOptions().then((options) => { @@ -279,11 +270,11 @@ export const setOriginalButton = (options: Options): void => { * twitterの画像を表示したときのC-sを拡張 * 画像のファイル名を「~.jpg-orig」「~.png-orig」ではなく「~-orig.jpg」「~-orig.png」にする */ -export const fixFileNameOnSaveCommand = (options: Options): void => { +export const fixFileNameOnSaveCommand = (options: OptionsBool): void => { // キーを押したとき document.addEventListener('keydown', (e) => { // 設定が有効なら - if (options[STRIP_IMAGE_SUFFIX] !== 'isfalse') { + if (options[STRIP_IMAGE_SUFFIX]) { downloadImage(e); } }); diff --git a/webpack.config.js b/webpack.config.js index 2fa143cb..29949826 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,8 @@ module.exports = { entry: { main: './src/main.ts', - background: './src/background.ts', - popup: './src/popup.tsx', + background: './src/extension-contexts/background.ts', + popup: './src/extension-contexts/popup.tsx', }, output: { filename: '[name].bundle.js', diff --git a/yarn.lock b/yarn.lock index 81673508..ca5d58c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1485,6 +1485,14 @@ "@types/filesystem" "*" "@types/har-format" "*" +"@types/chrome@^0.0.114": + version "0.0.114" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.114.tgz#8ceb33fa261f4b9e307fa7344ba8182d8d410d4e" + integrity sha512-i7qRr74IrxHtbnrZSKUuP5Uvd5EOKwlwJq/yp7+yTPihOXnPhNQO4Z5bqb1XTnrjdbUKEJicaVVbhcgtRijmLA== + dependencies: + "@types/filesystem" "*" + "@types/har-format" "*" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -3463,6 +3471,13 @@ jest-changed-files@^29.2.0: execa "^5.0.0" p-limit "^3.1.0" +jest-chrome@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/jest-chrome/-/jest-chrome-0.8.0.tgz#f741f5cf49292326eb9a2507111b9a77a01a495d" + integrity sha512-39RR1GT9nI4e4jsuH1vIf4l5ApxxkcstjGJr+GsOURL8f4Db0UlbRnsZaM+ZRniaGtokqklUH5VFKGZZ6YztUg== + dependencies: + "@types/chrome" "^0.0.114" + jest-circus@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.3.1.tgz#177d07c5c0beae8ef2937a67de68f1e17bbf1b4a"