From fc64b242d8f0274f4983425404ed79176507d25e Mon Sep 17 00:00:00 2001 From: Alina Date: Wed, 24 Apr 2024 14:42:26 +0400 Subject: [PATCH 01/12] feat: add editing latest note fields --- public/_locales/en/messages.json | 15 +++++ src/background/app/index.ts | 3 + .../features/editLatestNote/index.ts | 1 + src/background/features/editLatestNote/ui.tsx | 55 +++++++++++++++++++ .../contextMenus/chromeContextMenus.ts | 4 ++ .../shared/browser/contextMenus/types.ts | 7 +++ src/content/dictionary/index.ts | 4 +- 7 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/background/features/editLatestNote/index.ts create mode 100644 src/background/features/editLatestNote/ui.tsx diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 52a81a8..eabaca2 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -112,6 +112,15 @@ "CONTEXT_MENU_ADD_NOTE_AUTOMATICALLY": { "message": "Add note" }, + "CONTEXT_MENU_EDIT_LATEST_NOTE_TRANSLATION": { + "message": "Edit latest note translation" + }, + "CONTEXT_MENU_EDIT_LATEST_NOTE_CONTEXT": { + "message": "Edit latest note context" + }, + "CONTEXT_MENU_EDIT_LATEST_NOTE_TRANSCRIPTION": { + "message": "Edit latest note transcription" + }, "ERROR_CAN_NOT_IDENTIFY_DICTIONARY_ADD_NOTE_AUTOMATICALLY": { "message": "Adding note automatically failed, can't identify dictionary" }, @@ -180,5 +189,11 @@ }, "CLEAR": { "message": "Clear" + }, + "ERROR_CAN_NOT_GET_ACTIVE_TAB": { + "message": "Can't get active tab" + }, + "ERROR_CAN_NOT_GET_ACTIVE_TAB_ON_ACTIVATED": { + "message": "Can't get active tab while activating" } } \ No newline at end of file diff --git a/src/background/app/index.ts b/src/background/app/index.ts index c99e101..24c879b 100644 --- a/src/background/app/index.ts +++ b/src/background/app/index.ts @@ -1,5 +1,6 @@ import { addNewWord } from '@background/features/addNewWord' import { autoAddNewNote } from '@background/features/autoAddNewNote' +import { editLatestNote } from '@background/features/editLatestNote' import { browser } from '@background/shared/browser' @@ -18,6 +19,8 @@ browser.contextMenus.removeAll(() => { addNewWord(PARENT_ID) autoAddNewNote(PARENT_ID) + + editLatestNote(PARENT_ID) }) $notes.listen(() => {}) diff --git a/src/background/features/editLatestNote/index.ts b/src/background/features/editLatestNote/index.ts new file mode 100644 index 0000000..ed58495 --- /dev/null +++ b/src/background/features/editLatestNote/index.ts @@ -0,0 +1 @@ +export * from './ui' diff --git a/src/background/features/editLatestNote/ui.tsx b/src/background/features/editLatestNote/ui.tsx new file mode 100644 index 0000000..3416a88 --- /dev/null +++ b/src/background/features/editLatestNote/ui.tsx @@ -0,0 +1,55 @@ +import { browser } from '@background/shared/browser' +import { + TOnClickContextMenuInfoProps, + TOnClickContextMenuTabProps, +} from '@background/shared/browser/contextMenus' +import { currentPageLanguageStore } from '@background/shared/entities/language' + +import { INoteSelectors } from '@shared/entities/note' +import { editNote } from '@shared/entities/note/model/store' + +export const editLatestNote = (parentId: string) => { + currentPageLanguageStore.$currentPageLanguage.listen(() => {}) + + const fields: Array> = [ + 'translation', + 'context', + 'transcription', + ] + fields.forEach((field) => { + const handleClick = async ( + info: TOnClickContextMenuInfoProps, + tab?: TOnClickContextMenuTabProps, + ) => { + if (info.menuItemId === `edit_latest_note_${field}`) { + if (!tab?.id) return + + editNote(currentPageLanguageStore.$currentPageLanguage.get(), { + ...currentPageLanguageStore.$latestNote.get(), + translation: info.selectionText, + }) + } + } + + currentPageLanguageStore.$latestNote.listen((latestNote) => { + if (latestNote) { + browser.contextMenus.update(`edit_latest_note_${field}`, { + title: `${browser.i18n.getMessage(`CONTEXT_MENU_EDIT_LATEST_NOTE_${field.toUpperCase()}`)} ${latestNote.text}`, + }) + } else { + browser.contextMenus.remove(`edit_latest_note_${field}`) + } + }) + + browser.contextMenus.create({ + title: browser.i18n.getMessage( + `CONTEXT_MENU_EDIT_LATEST_NOTE_${field.toUpperCase()}`, + ), + id: `edit_latest_note_${field}`, + parentId: parentId, + contexts: ['selection'], + }) + + browser.contextMenus.onClicked.addListener(handleClick) + }) +} diff --git a/src/background/shared/browser/contextMenus/chromeContextMenus.ts b/src/background/shared/browser/contextMenus/chromeContextMenus.ts index 714cfd9..38deff0 100644 --- a/src/background/shared/browser/contextMenus/chromeContextMenus.ts +++ b/src/background/shared/browser/contextMenus/chromeContextMenus.ts @@ -3,7 +3,11 @@ import { IContextMenus } from './types' export const chromeContextMenus: IContextMenus = { create: chrome.contextMenus.create, + update: chrome.contextMenus.update, + onClicked: chrome.contextMenus.onClicked, removeAll: chrome.contextMenus.removeAll, + + remove: chrome.contextMenus.remove, } diff --git a/src/background/shared/browser/contextMenus/types.ts b/src/background/shared/browser/contextMenus/types.ts index 4fcb509..05c9fc8 100644 --- a/src/background/shared/browser/contextMenus/types.ts +++ b/src/background/shared/browser/contextMenus/types.ts @@ -3,6 +3,13 @@ import { TTab } from '@shared/shared/browser/tabs' export interface IContextMenus { create: (menuConfig: IMenuConfig) => string | number + update: ( + menuItemId: string | number, + menuConfig: Partial, + ) => void + + remove: (menuItemId: string | number) => void + onClicked: { addListener: ( callback: ( diff --git a/src/content/dictionary/index.ts b/src/content/dictionary/index.ts index 8c5e7d2..224e0f6 100644 --- a/src/content/dictionary/index.ts +++ b/src/content/dictionary/index.ts @@ -15,9 +15,9 @@ import { $languages } from '@shared/entities/language/model/store' import { getNotesFields } from './helpers/getNotesFields' -$languages.listen(() => {}) - browser.runtime.onConnect.addListener(async function (port) { + $languages.listen(() => {}) + if (port.name === 'CurrentPage') { getLanguageFromPageHandler(port, () => { let pageLanguage: TLanguageCode = 'other' From a512043f0b578eacdce102571364a6190d4fb984 Mon Sep 17 00:00:00 2001 From: Alina Date: Wed, 24 Apr 2024 14:48:33 +0400 Subject: [PATCH 02/12] chore: change portuguese to british portuguese --- src/shared/entities/dictionary/model/dictionaries.ts | 8 ++++---- src/shared/entities/dictionary/model/helpers.ts | 3 ++- src/shared/entities/language/model/languages.ts | 4 ++-- src/shared/entities/note/model/store.ts | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/shared/entities/dictionary/model/dictionaries.ts b/src/shared/entities/dictionary/model/dictionaries.ts index ab3ba0a..9451fbb 100644 --- a/src/shared/entities/dictionary/model/dictionaries.ts +++ b/src/shared/entities/dictionary/model/dictionaries.ts @@ -121,14 +121,14 @@ export const DICTIONARIES: TDictionaries = { }, ], }, - pt: { + 'pt-BR': { label: 'Portuguese', - value: 'pt', + value: 'pt-BR', dictionaries: [ { - id: 'pt_Wordreference', + id: 'pt-BR_Wordreference', name: 'Wordreference', - url: 'https://www.wordreference.com/pten/', + url: 'https://www.wordreference.com/enpt/', selectors: { text: '.even.FrWrd', transcription: '', diff --git a/src/shared/entities/dictionary/model/helpers.ts b/src/shared/entities/dictionary/model/helpers.ts index b6ffae5..377120d 100644 --- a/src/shared/entities/dictionary/model/helpers.ts +++ b/src/shared/entities/dictionary/model/helpers.ts @@ -1,7 +1,7 @@ import { TLanguageCode } from '@shared/entities/language' import { INoteSelectors } from '@shared/entities/note' -import { DICTIONARIES, GOOGLE_TRANSLATE } from '.' +import { DICTIONARIES, GOOGLE_TRANSLATE } from './dictionaries' import { IDictionary } from './types' export const getDictionaryByPageUrl = ( @@ -16,6 +16,7 @@ export const getDictionaryByPageUrl = ( let dictionaryIndex = 0 while (dictionaryIndex < languageDictionaries.length) { const dictionary = languageDictionaries[dictionaryIndex] + if (pageUrl.includes(dictionary.url)) { return { languageCode: languages[languageIndex].value, diff --git a/src/shared/entities/language/model/languages.ts b/src/shared/entities/language/model/languages.ts index 24c5aee..5b8394a 100644 --- a/src/shared/entities/language/model/languages.ts +++ b/src/shared/entities/language/model/languages.ts @@ -1,11 +1,11 @@ import { ILanguage } from '.' -export const LANGUAGE_CODES = ['en', 'ja', 'pt', 'ko', 'other'] as const +export const LANGUAGE_CODES = ['en', 'ja', 'pt-BR', 'ko', 'other'] as const export const LANGUAGES: ILanguage[] = [ { label: 'English', value: 'en' }, { label: 'Japanese', value: 'ja' }, - { label: 'Portuguese', value: 'pt' }, + { label: 'Portuguese', value: 'pt-BR' }, { label: 'Korean', value: 'ko' }, { label: 'Other', value: 'other' }, ] diff --git a/src/shared/entities/note/model/store.ts b/src/shared/entities/note/model/store.ts index cd0ac77..dd93c55 100644 --- a/src/shared/entities/note/model/store.ts +++ b/src/shared/entities/note/model/store.ts @@ -15,7 +15,7 @@ export type StorageValue = Record export const defaultNotes: StorageValue = { en: [], ja: [], - pt: [], + 'pt-BR': [], ko: [], other: [], } From 0944238e4d3f1c622f09aab014e5a76d71822398 Mon Sep 17 00:00:00 2001 From: Alina Date: Wed, 24 Apr 2024 18:01:20 +0400 Subject: [PATCH 03/12] chore: add retry to get tab --- src/background/shared/browser/index.ts | 2 + src/shared/shared/browser/tabs/chromeTabs.ts | 63 ++++++++++++-------- src/shared/shared/browser/tabs/types.ts | 5 ++ 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/background/shared/browser/index.ts b/src/background/shared/browser/index.ts index 55187e0..c3274da 100644 --- a/src/background/shared/browser/index.ts +++ b/src/background/shared/browser/index.ts @@ -1,8 +1,10 @@ import { i18n } from '@shared/shared/browser/i18n' +import { tabs } from '@shared/shared/browser/tabs' import { contextMenus } from './contextMenus' export const browser = { contextMenus, i18n, + tabs, } diff --git a/src/shared/shared/browser/tabs/chromeTabs.ts b/src/shared/shared/browser/tabs/chromeTabs.ts index 8ca9cd3..16a7758 100644 --- a/src/shared/shared/browser/tabs/chromeTabs.ts +++ b/src/shared/shared/browser/tabs/chromeTabs.ts @@ -1,47 +1,62 @@ import { Result } from '@shared/shared/libs/operationResult' +import { i18n } from '../i18n' import { ITabs } from './types' export const chromeTabs: ITabs = { - getActiveTab: () => { - return new Promise((resolve) => { + _getActiveTabWithRetry: (onSuccess, onError) => { + let tryCount = 8 + let delay = 300 + + const interval = setInterval(() => { chrome.tabs.query( { active: true, currentWindow: true, status: 'complete' }, function (tabs) { const tab = tabs[0] if (tab?.id && tab?.url) { - resolve(Result.Success({ id: tab.id, url: tab.url })) + clearInterval(interval) + onSuccess({ id: tab.id, url: tab.url }) + } else if (tryCount === 0) { + clearInterval(interval) + onError() } else { - resolve( - Result.Error({ - type: 'ERROR_CAN_NOT_GET_ACTIVE_TAB', - error: null, - }), - ) + tryCount-- + delay += 200 } }, ) + }, delay) + }, + + getActiveTab: () => { + return new Promise((resolve) => { + chromeTabs._getActiveTabWithRetry( + (tab) => resolve(Result.Success(tab)), + () => + resolve( + Result.Error({ + type: i18n.getMessage('ERROR_CAN_NOT_GET_ACTIVE_TAB'), + error: null, + }), + ), + ) }) }, onActivated: { addListener: (callback) => { chrome.tabs.onActivated.addListener(async () => { - chrome.tabs.query( - { active: true, currentWindow: true, status: 'complete' }, - function (tabs) { - const tab = tabs[0] - if (tab?.id && tab?.url) { - callback(Result.Success({ id: tab.id, url: tab.url })) - } else { - callback( - Result.Error({ - type: 'ERROR_CAN_NOT_GET_ACTIVE_TAB_ON_ACTIVATED', - error: null, - }), - ) - } - }, + chromeTabs._getActiveTabWithRetry( + (tab) => callback(Result.Success(tab)), + () => + callback( + Result.Error({ + type: i18n.getMessage( + 'ERROR_CAN_NOT_GET_ACTIVE_TAB_ON_ACTIVATED', + ), + error: null, + }), + ), ) }) }, diff --git a/src/shared/shared/browser/tabs/types.ts b/src/shared/shared/browser/tabs/types.ts index 7e400af..21018c9 100644 --- a/src/shared/shared/browser/tabs/types.ts +++ b/src/shared/shared/browser/tabs/types.ts @@ -1,6 +1,11 @@ import { TResult } from '@shared/shared/libs/operationResult' export interface ITabs { + _getActiveTabWithRetry: ( + onSuccess: (tab: TTab) => void, + onError: () => void, + ) => void + getActiveTab: () => Promise> onActivated: { From 3848f995b7f8ff8fbe597890c574a783a4f7e4e2 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 7 May 2024 17:04:48 +0400 Subject: [PATCH 04/12] fix: remove other language from dictionary and language selection, remove error alert --- src/popup/app/Popup.tsx | 3 -- src/popup/app/styles.scss | 1 - .../ui/SelectDictionaryList.tsx | 22 +++++++------ .../language/SelectLanguages/model/store.ts | 25 +++++++++------ .../SelectLanguages/ui/SelectLanguages.tsx | 32 ++++++++++--------- 5 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/popup/app/Popup.tsx b/src/popup/app/Popup.tsx index c9c79cc..58fb1a6 100644 --- a/src/popup/app/Popup.tsx +++ b/src/popup/app/Popup.tsx @@ -1,12 +1,9 @@ import { NoteList } from '@popup/widgets/note/NoteList' import { Settings } from '@popup/widgets/settings' -import { ErrorAlert } from '@popup/entities/error' - export const Popup = () => { return (
-
diff --git a/src/popup/app/styles.scss b/src/popup/app/styles.scss index af73885..974f2b8 100644 --- a/src/popup/app/styles.scss +++ b/src/popup/app/styles.scss @@ -98,7 +98,6 @@ main { height: 480px; padding: var(--spacing-5) var(--spacing-2); overflow-y: auto; - scrollbar-gutter: stable both-edges; } ul { diff --git a/src/popup/features/dictionary/SelectDictionaryVariant/ui/SelectDictionaryList.tsx b/src/popup/features/dictionary/SelectDictionaryVariant/ui/SelectDictionaryList.tsx index f50c7ee..1995d68 100644 --- a/src/popup/features/dictionary/SelectDictionaryVariant/ui/SelectDictionaryList.tsx +++ b/src/popup/features/dictionary/SelectDictionaryVariant/ui/SelectDictionaryList.tsx @@ -26,18 +26,20 @@ export const SelectDictionaryList = () => { <> - {languages.map((language) => ( - - {language.label} - - ))} + {languages.map((language) => + language.value === 'other' ? null : ( + + {language.label} + + ), + )} {languages.map((language) => { - return ( + return language.value === 'other' ? null : ( ([]), - islanguageCodesDirty: atom(true), + defaultValue: atom([]), + languageCodes: atom( + LANGUAGES.map((language) => language.value), + ), + islanguageCodesDirty: atom(false), } languageStore.$languages.listen((newValue) => { - if (localStore.islanguageCodesDirty.get()) { - const languageCodes = newValue.map((language) => language.value) - defaultValue = languageCodes + const islanguageCodesDirty = localStore.islanguageCodesDirty.get() + const languageCodes = newValue.map((language) => language.value) + if (islanguageCodesDirty) { + localStore.defaultValue.set(languageCodes) + } else { localStore.languageCodes.set(languageCodes) } }) @@ -37,7 +44,7 @@ export const checkIsSelected = (languageCode: TLanguageCode): boolean => { } export const reset = () => { - localStore.languageCodes.set(defaultValue) + localStore.languageCodes.set(localStore.defaultValue.get()) } export const syncLocalStoreWithLanguageStore = (): Promise => { diff --git a/src/popup/features/language/SelectLanguages/ui/SelectLanguages.tsx b/src/popup/features/language/SelectLanguages/ui/SelectLanguages.tsx index 32766e6..9f52266 100644 --- a/src/popup/features/language/SelectLanguages/ui/SelectLanguages.tsx +++ b/src/popup/features/language/SelectLanguages/ui/SelectLanguages.tsx @@ -46,21 +46,23 @@ export const SelectLanguages = ({ languages }: Props) => {
    - {languages.map((language) => ( -
  • - -
  • - ))} + {languages.map((language) => + language.value === 'other' ? null : ( +
  • + +
  • + ), + )}
From 3616f96593b0d8ed36bf60d5e2fba0e0c853bd57 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 7 May 2024 17:16:06 +0400 Subject: [PATCH 05/12] fix: remove logging fallback when getting from chrome storage --- src/shared/shared/browser/storage/chromeStorage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shared/shared/browser/storage/chromeStorage.ts b/src/shared/shared/browser/storage/chromeStorage.ts index 350bc04..a64fa38 100644 --- a/src/shared/shared/browser/storage/chromeStorage.ts +++ b/src/shared/shared/browser/storage/chromeStorage.ts @@ -7,7 +7,10 @@ export const chromeStorage: IStorage = { get: (key, defaultValue, enableLogging = true) => { return new Promise((resolve) => { chrome.storage.local.get(key, (storage) => { - if (!storage[key]) resolve(Result.Success(defaultValue)) + if (!storage[key]) { + resolve(Result.Success(defaultValue)) + return + } try { resolve(Result.Success(JSON.parse(storage[key]))) } catch (error) { From 5896c5908f604187919149649e748032078d1887 Mon Sep 17 00:00:00 2001 From: Alina Date: Wed, 8 May 2024 00:00:01 +0400 Subject: [PATCH 06/12] fix: remove including other in select languaage functionality --- src/popup/features/language/SelectLanguages/model/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup/features/language/SelectLanguages/model/store.ts b/src/popup/features/language/SelectLanguages/model/store.ts index 6b34c08..348d34a 100644 --- a/src/popup/features/language/SelectLanguages/model/store.ts +++ b/src/popup/features/language/SelectLanguages/model/store.ts @@ -36,7 +36,7 @@ export const toggle = async (languageCode: TLanguageCode) => { ? languageCodes.filter((lang) => lang !== languageCode) : [...languageCodes, languageCode] - localStore.languageCodes.set(newSelectedLanguages) + localStore.languageCodes.set([...newSelectedLanguages, 'other']) } export const checkIsSelected = (languageCode: TLanguageCode): boolean => { From 66bed8fac5800da2aee8e7ac0f7b3143b069fa20 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 21 May 2024 18:00:36 +0400 Subject: [PATCH 07/12] chore: remove circle dependencies --- .gitignore | 2 ++ package.json | 1 + pnpm-lock.yaml | 26 ++++++++++++++++ src/content/dictionary/index.ts | 8 +++-- .../entities/dictionary/model/helpers.ts | 7 +++++ src/shared/entities/dictionary/model/store.ts | 2 ++ src/shared/entities/error/model/store.ts | 2 +- src/shared/entities/language/model/helpers.ts | 11 ++----- src/shared/entities/language/model/index.ts | 4 +-- .../entities/language/model/languages.ts | 4 +-- src/shared/entities/language/model/store.ts | 7 ++++- src/shared/entities/language/model/types.ts | 3 +- .../note/model/__tests__/store.test.ts | 3 +- src/shared/entities/note/model/store.ts | 7 ++++- src/shared/entities/storage/model/Storage.ts | 15 +++++----- .../shared/browser/storage/chromeStorage.ts | 23 ++++++++------ src/shared/shared/browser/storage/index.ts | 1 + src/shared/shared/browser/storage/types.ts | 30 +++++++++---------- .../shared/libs/operationResult/index.ts | 6 ++-- vite.config.background.ts | 6 ++++ vite.config.dictionary.ts | 6 ++++ vite.config.popup.ts | 8 ++++- 22 files changed, 123 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index 8141ff5..cd00b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ coverage /playwright-report/ /blob-report/ /playwright/.cache/ + +*.circleDep.txt \ No newline at end of file diff --git a/package.json b/package.json index 6befb63..6ce27ba 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "stylelint-prettier": "5.0.0", "typescript": "5.4.2", "vite": "5.1.5", + "vite-plugin-circular-dependency": "0.4.1", "vitest": "1.3.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50afb5b..c19396d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ devDependencies: vite: specifier: 5.1.5 version: 5.1.5(@types/node@20.11.25)(sass@1.71.1) + vite-plugin-circular-dependency: + specifier: 0.4.1 + version: 0.4.1 vitest: specifier: 1.3.1 version: 1.3.1(@types/node@20.11.25)(jsdom@24.0.0)(sass@1.71.1) @@ -1009,6 +1012,20 @@ packages: picomatch: 2.3.1 dev: true + /@rollup/pluginutils@5.1.0: + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + /@rollup/rollup-android-arm-eabi@4.12.1: resolution: {integrity: sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==} cpu: [arm] @@ -4411,6 +4428,15 @@ packages: - terser dev: true + /vite-plugin-circular-dependency@0.4.1: + resolution: {integrity: sha512-xMvrFuadDXrUYdQ8acYmYDR0hnNTTBY5y4is4AnFW04DM0kWyLxujV6omrMYOGwZDHaAjLVfbZiwUMxuhqHM3w==} + dependencies: + '@rollup/pluginutils': 5.1.0 + chalk: 4.1.2 + transitivePeerDependencies: + - rollup + dev: true + /vite@5.1.5(@types/node@20.11.25)(sass@1.71.1): resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/src/content/dictionary/index.ts b/src/content/dictionary/index.ts index 224e0f6..d223764 100644 --- a/src/content/dictionary/index.ts +++ b/src/content/dictionary/index.ts @@ -5,11 +5,13 @@ import { getNoteFromDictionaryPageHandler, } from '@shared/features/note/AutoAddNewNote' -import { GOOGLE_TRANSLATE } from '@shared/entities/dictionary' +import { + GOOGLE_TRANSLATE, + getDictionaryLanguageCodeByPageUrl, +} from '@shared/entities/dictionary' import { TLanguageCode, getLanguageCodeByPageMetaData, - getLanguageCodeByPageUrl, } from '@shared/entities/language' import { $languages } from '@shared/entities/language/model/store' @@ -27,7 +29,7 @@ browser.runtime.onConnect.addListener(async function (port) { if (isTranslatorPage) { pageLanguage = GOOGLE_TRANSLATE.getLanguage() } else { - pageLanguage = getLanguageCodeByPageUrl(window.location.href) + pageLanguage = getDictionaryLanguageCodeByPageUrl(window.location.href) if (pageLanguage === 'other') { pageLanguage = getLanguageCodeByPageMetaData() } diff --git a/src/shared/entities/dictionary/model/helpers.ts b/src/shared/entities/dictionary/model/helpers.ts index 377120d..3d1dfb5 100644 --- a/src/shared/entities/dictionary/model/helpers.ts +++ b/src/shared/entities/dictionary/model/helpers.ts @@ -67,3 +67,10 @@ export const getDictionariesUrls = (): string[] => { return urls } + +export const getDictionaryLanguageCodeByPageUrl = ( + url: string, +): TLanguageCode => { + const dictionary = getDictionaryByPageUrl(url) + return dictionary ? dictionary.languageCode : 'other' +} diff --git a/src/shared/entities/dictionary/model/store.ts b/src/shared/entities/dictionary/model/store.ts index 223be29..033aa30 100644 --- a/src/shared/entities/dictionary/model/store.ts +++ b/src/shared/entities/dictionary/model/store.ts @@ -2,6 +2,7 @@ import { atom, onMount, task } from 'nanostores' import { browser } from '@popup/shared/browser' +import { addError } from '@shared/entities/error' import { TLanguageCode } from '@shared/entities/language' import { getStorage } from '@shared/entities/storage' @@ -15,6 +16,7 @@ type StorageValue = TDictionaries export const DictionaryStorage = getStorage( 'dictionaries', DICTIONARIES, + addError, ) export const $dictionaries = atom(DICTIONARIES) diff --git a/src/shared/entities/error/model/store.ts b/src/shared/entities/error/model/store.ts index 8443da3..61b9321 100644 --- a/src/shared/entities/error/model/store.ts +++ b/src/shared/entities/error/model/store.ts @@ -4,7 +4,7 @@ import { getStorage } from '@shared/entities/storage' import { TBaseError } from '@shared/shared/libs/operationResult' -export const ErrorStorage = getStorage('errors', [], false) +export const ErrorStorage = getStorage('errors', []) export const $errors = atom([]) diff --git a/src/shared/entities/language/model/helpers.ts b/src/shared/entities/language/model/helpers.ts index 4e52074..5c78821 100644 --- a/src/shared/entities/language/model/helpers.ts +++ b/src/shared/entities/language/model/helpers.ts @@ -1,19 +1,12 @@ -import { getDictionaryByPageUrl } from '@shared/entities/dictionary' - -import { languageStore } from '..' +import { $languageCodes } from './store' import { TLanguageCode } from './types' -export const getLanguageCodeByPageUrl = (url: string): TLanguageCode => { - const dictionary = getDictionaryByPageUrl(url) - return dictionary ? dictionary.languageCode : 'other' -} - export const getLanguageCodeByPageMetaData = () => { const pageLanguageCode = document .getElementsByTagName('html')[0] .getAttribute('lang') as TLanguageCode - return languageStore.$languageCodes.get().includes(pageLanguageCode) + return $languageCodes.get().includes(pageLanguageCode) ? pageLanguageCode : 'other' } diff --git a/src/shared/entities/language/model/index.ts b/src/shared/entities/language/model/index.ts index 72699a8..a274bbf 100644 --- a/src/shared/entities/language/model/index.ts +++ b/src/shared/entities/language/model/index.ts @@ -1,4 +1,4 @@ -export type { TLanguageCode, ILanguage } from './types' -export { LANGUAGES, LANGUAGE_CODES } from './languages' +export type { TLanguageCode, ILanguage, LANGUAGE_CODES } from './types' +export { LANGUAGES } from './languages' export * as languageStore from './store' export * from './helpers' diff --git a/src/shared/entities/language/model/languages.ts b/src/shared/entities/language/model/languages.ts index 5b8394a..93389c0 100644 --- a/src/shared/entities/language/model/languages.ts +++ b/src/shared/entities/language/model/languages.ts @@ -1,6 +1,4 @@ -import { ILanguage } from '.' - -export const LANGUAGE_CODES = ['en', 'ja', 'pt-BR', 'ko', 'other'] as const +import { ILanguage } from './types' export const LANGUAGES: ILanguage[] = [ { label: 'English', value: 'en' }, diff --git a/src/shared/entities/language/model/store.ts b/src/shared/entities/language/model/store.ts index 2b31332..b590e66 100644 --- a/src/shared/entities/language/model/store.ts +++ b/src/shared/entities/language/model/store.ts @@ -2,6 +2,7 @@ import { atom, computed, onMount, task } from 'nanostores' import { browser } from '@popup/shared/browser' +import { addError } from '@shared/entities/error' import { ILanguage, TLanguageCode } from '@shared/entities/language' import { getStorage } from '@shared/entities/storage' @@ -11,7 +12,11 @@ import { LANGUAGES } from './languages' type StorageValue = ILanguage[] -export const LanguageStorage = getStorage('languages', LANGUAGES) +export const LanguageStorage = getStorage( + 'languages', + LANGUAGES, + addError, +) export const $languages = atom(LANGUAGES) export const $languageCodes = computed($languages, (languages) => { diff --git a/src/shared/entities/language/model/types.ts b/src/shared/entities/language/model/types.ts index d4057a3..01e15b2 100644 --- a/src/shared/entities/language/model/types.ts +++ b/src/shared/entities/language/model/types.ts @@ -1,6 +1,7 @@ -import { LANGUAGE_CODES } from './languages' +export const LANGUAGE_CODES = ['en', 'ja', 'pt-BR', 'ko', 'other'] as const export type TLanguageCode = (typeof LANGUAGE_CODES)[number] + export interface ILanguage { label: string value: TLanguageCode diff --git a/src/shared/entities/note/model/__tests__/store.test.ts b/src/shared/entities/note/model/__tests__/store.test.ts index 41711cf..32036f6 100644 --- a/src/shared/entities/note/model/__tests__/store.test.ts +++ b/src/shared/entities/note/model/__tests__/store.test.ts @@ -2,6 +2,7 @@ import { waitFor } from '@tests/testUtils' import { allTasks, cleanStores, keepMount } from 'nanostores' import { afterEach, describe, expect, test, vi } from 'vitest' +import { storage } from '@shared/shared/browser/storage' import { Result } from '@shared/shared/libs/operationResult' import { @@ -45,7 +46,7 @@ describe('note store', () => { } afterEach(async () => { - await chrome.storage.local.clear() + await storage.clear() cleanStores($notes) $notes.set(notes) vi.clearAllMocks() diff --git a/src/shared/entities/note/model/store.ts b/src/shared/entities/note/model/store.ts index dd93c55..80e2683 100644 --- a/src/shared/entities/note/model/store.ts +++ b/src/shared/entities/note/model/store.ts @@ -3,6 +3,7 @@ import { atom, onMount, task } from 'nanostores' import { browser } from '@popup/shared/browser' +import { addError } from '@shared/entities/error' import { TLanguageCode } from '@shared/entities/language' import { getStorage } from '@shared/entities/storage' @@ -20,7 +21,11 @@ export const defaultNotes: StorageValue = { other: [], } -export const NoteStorage = getStorage(`notes`, defaultNotes) +export const NoteStorage = getStorage( + `notes`, + defaultNotes, + addError, +) export const $notes = atom(defaultNotes) diff --git a/src/shared/entities/storage/model/Storage.ts b/src/shared/entities/storage/model/Storage.ts index a2aeb74..2da893c 100644 --- a/src/shared/entities/storage/model/Storage.ts +++ b/src/shared/entities/storage/model/Storage.ts @@ -1,18 +1,19 @@ -import { TOnChangeListenerProps, storage } from '@shared/shared/browser/storage' -import { TResult } from '@shared/shared/libs/operationResult' +import { storage } from '@shared/shared/browser/storage' +import { TOnChangeListenerPlainProps } from '@shared/shared/browser/storage/types' +import { TBaseError, TResult } from '@shared/shared/libs/operationResult' export const getStorage = ( key: string, defaultValue: StorageValue, - enableLogging = true, + logError?: (error: TBaseError) => void, ) => { return { get() { - return storage.get(key, defaultValue, enableLogging) + return storage.get(key, defaultValue, logError) }, set(value: StorageValue) { - return storage.set(key, value, enableLogging) + return storage.set(key, value, logError) }, onChanged: { @@ -28,12 +29,12 @@ export const getStorage = ( key, callback, defaultValue, - enableLogging, + logError, ) }, removeListener( - listener: (changes: TOnChangeListenerProps) => void, + listener: (changes: TOnChangeListenerPlainProps) => void, ) { storage.onChanged.removeListener(listener) }, diff --git a/src/shared/shared/browser/storage/chromeStorage.ts b/src/shared/shared/browser/storage/chromeStorage.ts index a64fa38..83b5a9d 100644 --- a/src/shared/shared/browser/storage/chromeStorage.ts +++ b/src/shared/shared/browser/storage/chromeStorage.ts @@ -1,10 +1,10 @@ import { Result } from '@shared/shared/libs/operationResult' import { i18n } from '../i18n' -import { IStorage, TOnChangeListenerProps } from './types' +import { IStorage, TOnChangeListenerPlainProps } from './types' export const chromeStorage: IStorage = { - get: (key, defaultValue, enableLogging = true) => { + get: (key, defaultValue, logError) => { return new Promise((resolve) => { chrome.storage.local.get(key, (storage) => { if (!storage[key]) { @@ -20,7 +20,7 @@ export const chromeStorage: IStorage = { type: i18n.getMessage('ERROR_CAN_NOT_GET_DATA_FROM_STORAGE'), error: error instanceof Error ? error : null, }, - enableLogging, + logError, ), ) } @@ -28,7 +28,7 @@ export const chromeStorage: IStorage = { }) }, - set: (key, value, enableLogging = true) => { + set: (key, value, logError) => { return new Promise((resolve) => { try { const stringifiedValue = JSON.stringify(value) @@ -44,7 +44,7 @@ export const chromeStorage: IStorage = { type: i18n.getMessage('ERROR_CAN_NOT_UPDATE_DATA_IN_STORAGE'), error: error instanceof Error ? error : null, }, - enableLogging, + logError, ), ) } @@ -52,8 +52,8 @@ export const chromeStorage: IStorage = { }, onChanged: { - addListener: (key, callback, defaultValue, enableLogging = true) => { - const listener = (changes: TOnChangeListenerProps) => { + addListener: (key, callback, defaultValue, logError) => { + const listener = (changes: TOnChangeListenerPlainProps) => { if (changes[key]) { let oldValue = defaultValue try { @@ -67,7 +67,7 @@ export const chromeStorage: IStorage = { ), error: error instanceof Error ? error : null, }, - enableLogging, + logError, ), ) } @@ -89,7 +89,7 @@ export const chromeStorage: IStorage = { ), error: error instanceof Error ? error : null, }, - enableLogging, + logError, ), ) } @@ -103,4 +103,9 @@ export const chromeStorage: IStorage = { chrome.storage.local.onChanged.removeListener(listener) }, }, + + clear: async () => { + await chrome.storage.local.clear() + return Promise.resolve(Result.Success(true)) + }, } diff --git a/src/shared/shared/browser/storage/index.ts b/src/shared/shared/browser/storage/index.ts index 0e940f6..392507a 100644 --- a/src/shared/shared/browser/storage/index.ts +++ b/src/shared/shared/browser/storage/index.ts @@ -2,4 +2,5 @@ import { chromeStorage } from './chromeStorage' import { IStorage } from './types' export type { IStorage, TOnChangeListenerProps } from './types' + export const storage: IStorage = chromeStorage diff --git a/src/shared/shared/browser/storage/types.ts b/src/shared/shared/browser/storage/types.ts index e72fb2a..5824d6f 100644 --- a/src/shared/shared/browser/storage/types.ts +++ b/src/shared/shared/browser/storage/types.ts @@ -1,42 +1,40 @@ -import { TResult } from '@shared/shared/libs/operationResult' +import { TBaseError, TResult } from '@shared/shared/libs/operationResult' export interface IStorage { get: ( key: string, defaultValue: Data, - enableLogging?: boolean, + enableLogging?: (error: TBaseError) => void, ) => Promise> set: ( key: string, value: Data, - enableLogging?: boolean, + enableLogging?: (error: TBaseError) => void, ) => Promise> onChanged: { addListener: ( key: string, - callback: ( - changes: TResult<{ - newValue: Data - oldValue: Data - }>, - ) => void, + callback: (changes: TResult>) => void, defaultValue: Data, - enableLogging?: boolean, - ) => (changes: { - [key: string]: { newValue?: Data; oldValue?: Data } - }) => void + enableLogging?: (error: TBaseError) => void, + ) => (changes: TOnChangeListenerPlainProps) => void removeListener: ( - callback: (changes: { - [key: string]: { newValue?: Data; oldValue?: Data } - }) => void, + callback: (changes: TOnChangeListenerPlainProps) => void, ) => void } + + clear: () => Promise> } export type TOnChangeListenerProps = { + newValue: Value + oldValue: Value +} + +export type TOnChangeListenerPlainProps = { [key: string]: { newValue?: Value oldValue?: Value diff --git a/src/shared/shared/libs/operationResult/index.ts b/src/shared/shared/libs/operationResult/index.ts index d09e595..f714205 100644 --- a/src/shared/shared/libs/operationResult/index.ts +++ b/src/shared/shared/libs/operationResult/index.ts @@ -1,5 +1,3 @@ -import { addError } from '@shared/entities/error' - export type TBaseError = { type: string; error: Error | null } export type TResult = | { data: T; error: null } @@ -14,9 +12,9 @@ export const Result = Object.freeze({ }), Error: ( error: E, - enableLogging = true, + log?: (error: TBaseError) => void, ): TResult => { - enableLogging && addError(error) + log && log(error) return { data: null, error, diff --git a/vite.config.background.ts b/vite.config.background.ts index fe73f6b..147e879 100644 --- a/vite.config.background.ts +++ b/vite.config.background.ts @@ -1,4 +1,5 @@ import { defineConfig, mergeConfig } from 'vite' +import circleDependency from 'vite-plugin-circular-dependency' import commonViteConfig from './vite.config.common' @@ -6,6 +7,11 @@ import commonViteConfig from './vite.config.common' export default mergeConfig( commonViteConfig, defineConfig({ + plugins: [ + circleDependency({ + outputFilePath: './background.circleDep.txt', + }), + ], build: { outDir: 'dist/background', rollupOptions: { diff --git a/vite.config.dictionary.ts b/vite.config.dictionary.ts index a3efaa1..587380e 100644 --- a/vite.config.dictionary.ts +++ b/vite.config.dictionary.ts @@ -1,4 +1,5 @@ import { defineConfig, mergeConfig } from 'vite' +import circleDependency from 'vite-plugin-circular-dependency' import commonViteConfig from './vite.config.common' @@ -6,6 +7,11 @@ import commonViteConfig from './vite.config.common' export default mergeConfig( commonViteConfig, defineConfig({ + plugins: [ + circleDependency({ + outputFilePath: './dictionary.circleDep.txt', + }), + ], build: { outDir: 'dist/content/dictionary', rollupOptions: { diff --git a/vite.config.popup.ts b/vite.config.popup.ts index c6a03c3..0dce12e 100644 --- a/vite.config.popup.ts +++ b/vite.config.popup.ts @@ -1,5 +1,6 @@ import preact from '@preact/preset-vite' import { defineConfig, mergeConfig } from 'vite' +import circleDependency from 'vite-plugin-circular-dependency' import commonViteConfig from './vite.config.common' @@ -7,7 +8,12 @@ import commonViteConfig from './vite.config.common' export default mergeConfig( commonViteConfig, defineConfig({ - plugins: [preact()], + plugins: [ + preact(), + circleDependency({ + outputFilePath: './popup.circleDep.txt', + }), + ], build: { outDir: 'dist', rollupOptions: { From 0f58d3798977d2b156f67b3e43475537ec66960a Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 21 May 2024 18:02:25 +0400 Subject: [PATCH 08/12] test: add test for chrome storage --- .../storage/__mocks__/chromeStorageMock.ts | 56 ++++++ .../storage/__test__/chromeStorage.test.ts | 163 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 src/shared/shared/browser/storage/__mocks__/chromeStorageMock.ts create mode 100644 src/shared/shared/browser/storage/__test__/chromeStorage.test.ts diff --git a/src/shared/shared/browser/storage/__mocks__/chromeStorageMock.ts b/src/shared/shared/browser/storage/__mocks__/chromeStorageMock.ts new file mode 100644 index 0000000..352fd19 --- /dev/null +++ b/src/shared/shared/browser/storage/__mocks__/chromeStorageMock.ts @@ -0,0 +1,56 @@ +import { MockedObject, vi } from 'vitest' + +import { TOnChangeListenerPlainProps } from '../types' + +type TChrome = typeof chrome +type TStorageValue = Record + +let store: TStorageValue = {} +let listeners: Array<(changes: TOnChangeListenerPlainProps) => void> = [] + +function getStorageValueByKey(key: string) { + const result: TStorageValue = {} + result[key] = store[key] + return result +} + +export const chromeMockClearListeners = vi.fn(() => { + listeners = [] +}) + +export const chromeStorageMock = { + get: vi.fn((id, cb) => { + const result = id === null ? store : getStorageValueByKey(id) + return cb ? cb(result) : Promise.resolve(result) + }), + + set: vi.fn((payload, cb) => { + const changes: TOnChangeListenerPlainProps = {} + Object.keys(payload).forEach((key) => { + if (payload[key]) { + store[key] = payload[key] + changes[key] = { oldValue: store[key], newValue: payload[key] } + } + }) + + for (const listener of listeners) { + listener(changes) + } + + return cb ? cb() : Promise.resolve() + }), + + onChanged: { + addListener: vi.fn( + (listener: (changes: TOnChangeListenerPlainProps) => void) => { + listeners.push(listener) + }, + ), + removeListener: vi.fn(), + }, + + clear: vi.fn((cb) => { + store = {} + return cb ? cb() : Promise.resolve() + }), +} as unknown as MockedObject diff --git a/src/shared/shared/browser/storage/__test__/chromeStorage.test.ts b/src/shared/shared/browser/storage/__test__/chromeStorage.test.ts new file mode 100644 index 0000000..0b91aa3 --- /dev/null +++ b/src/shared/shared/browser/storage/__test__/chromeStorage.test.ts @@ -0,0 +1,163 @@ +import { CIRCULAR_VALUE, INVALID_JSON_STRING } from '@tests/testUtils' +import { MockedObject, beforeAll, describe, expect, test, vi } from 'vitest' + +import { Result } from '@shared/shared/libs/operationResult' + +import { storageMock } from '../__mocks__' +import { chromeStorageMock } from '../__mocks__/chromeStorageMock' +import { chromeStorage } from '../chromeStorage' + +describe('chromeStorage', () => { + const KEY = 'testKey' + const storedValue: TStorageValue = { someData: 'value' } + const defaultValue: TStorageValue = { defaultData: 'defaultValue' } + const oldValue = { oldValue: 'previousValue' } + const newValue = { newValue: 'updatedValue' } + + beforeAll(async () => { + global.chrome = { + storage: { + local: chromeStorageMock, + }, + } as unknown as MockedObject + await storageMock.clear() + vi.restoreAllMocks() + }) + + type TStorageValue = object + + describe('get/set', () => { + test('should resolve with error when data in storage is invalid', async () => { + vi.spyOn(chrome.storage.local, 'get').mockImplementationOnce( + (_key, callback) => { + callback({ + [KEY]: INVALID_JSON_STRING, + }) + }, + ) + + const getResult = await chromeStorage.get(KEY, defaultValue) + + expect(getResult.data).toBeNull() + expect(getResult.error).toMatchObject({ + type: 'Translated', + }) + }) + + test('should get default value if storage is empty', async () => { + const getResult = await chromeStorage.get(KEY, defaultValue) + + expect(getResult).toEqual(Result.Success(defaultValue)) + }) + + test('should get previously setted data', async () => { + const setResult = await chromeStorage.set(KEY, storedValue) + + expect(setResult).toEqual(Result.Success(true)) + + const getResult = await chromeStorage.get(KEY, defaultValue) + + expect(getResult).toEqual(Result.Success(storedValue)) + }) + + test('should not set data to the storage if data is not valid', async () => { + let setResult = await chromeStorage.set(KEY, storedValue) + + expect(setResult).toEqual(Result.Success(true)) + + setResult = await chromeStorage.set(KEY, CIRCULAR_VALUE) + + expect(setResult.data).toBeNull() + expect(setResult.error).toMatchObject({ + type: 'Translated', + }) + + const getResult = await chromeStorage.get(KEY, defaultValue) + + expect(getResult).toEqual(Result.Success(storedValue)) + }) + }) + + describe('onChanged', () => { + test('should invoke callback with parsed new and old values when storage changed', () => { + vi.spyOn( + chrome.storage.local.onChanged, + 'addListener', + ).mockImplementationOnce((listener) => { + listener({ + [KEY]: { + oldValue: JSON.stringify(oldValue), + newValue: JSON.stringify(newValue), + }, + }) + }) + + const callback = vi.fn() + + chromeStorage.onChanged.addListener(KEY, callback, defaultValue) + + chromeStorage.set(KEY, '') + expect(callback).toHaveBeenCalledWith( + Result.Success({ newValue, oldValue }), + ) + }) + + test('should invoke callback with default old value and parsed new value when storage changed with invalid old value', () => { + const callback = vi.fn() + + vi.spyOn( + chrome.storage.local.onChanged, + 'addListener', + ).mockImplementationOnce((listener) => { + listener({ + [KEY]: { + oldValue: INVALID_JSON_STRING, + newValue: JSON.stringify(newValue), + }, + }) + }) + + chromeStorage.onChanged.addListener(KEY, callback, defaultValue) + + expect(callback).toHaveBeenCalledTimes(2) + + const call1 = callback.mock.calls[0] + expect(call1[0].data).toBeNull() + expect(call1[0].error).toMatchObject({ + type: 'Translated', + }) + + const call2 = callback.mock.calls[1] + expect(call2[0]).toEqual( + Result.Success({ + newValue: newValue, + oldValue: defaultValue, + }), + ) + }) + + test('should invoke callback with error when storage changed with invalid new value', () => { + const callback = vi.fn() + + vi.spyOn( + chrome.storage.local.onChanged, + 'addListener', + ).mockImplementation((listener) => { + listener({ + [KEY]: { + oldValue: JSON.stringify(oldValue), + newValue: INVALID_JSON_STRING, + }, + }) + }) + + chromeStorage.onChanged.addListener(KEY, callback, defaultValue) + + const call = callback.mock.calls[0] + expect(call[0].data).toBeNull() + expect(call[0].error).toMatchObject({ + type: 'Translated', + }) + }) + }) +}) From 2b73af6712e1520c3a7777d4e683513f347df991 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 21 May 2024 18:06:53 +0400 Subject: [PATCH 09/12] test: add mock for browser --- .../shared/browser/storage/__mocks__/index.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/shared/shared/browser/storage/__mocks__/index.ts diff --git a/src/shared/shared/browser/storage/__mocks__/index.ts b/src/shared/shared/browser/storage/__mocks__/index.ts new file mode 100644 index 0000000..d073cf6 --- /dev/null +++ b/src/shared/shared/browser/storage/__mocks__/index.ts @@ -0,0 +1,71 @@ +import { vi } from 'vitest' + +import { Result } from '@shared/shared/libs/operationResult' + +import { IStorage, TOnChangeListenerPlainProps } from '../types' + +type TStorageValue = Record + +let store: TStorageValue = {} +let listeners: Array<(changes: TOnChangeListenerPlainProps) => void> = [] + +function getStorageValueByKey(key: string) { + const result: TStorageValue = {} + result[key] = store[key] + return result +} + +export const storageMockClearListeners = vi.fn(() => { + listeners = [] +}) + +export const storageMock: IStorage = { + get: vi.fn().mockImplementation((key, defaultValue) => { + const storage = key === null ? store : getStorageValueByKey(key) + + if (!storage[key]) { + return Promise.resolve(Result.Success(defaultValue)) + } + + return Promise.resolve(Result.Success(storage[key])) + }), + + set: vi.fn((key, value) => { + store[key] = value + const changes: TOnChangeListenerPlainProps = { + [key]: { + oldValue: store[key], + newValue: value, + }, + } + for (const listener of listeners) { + listener(changes) + } + + return Promise.resolve(Result.Success(true)) + }), + + onChanged: { + addListener: vi.fn((key, listener) => { + const plainListener = (changes: TOnChangeListenerPlainProps) => { + listener( + Result.Success({ + oldValue: changes[key].oldValue as never, + newValue: changes[key].newValue as never, + }), + ) + } + listeners.push(plainListener) + + return plainListener + }), + removeListener: vi.fn((listenerToRemove) => { + listeners = listeners.filter((listener) => listener !== listenerToRemove) + }), + }, + + clear: vi.fn(() => { + store = {} + return Promise.resolve(Result.Success(true)) + }), +} From 96db529a0c46c7c921aa4d7ebe77c25f4dceca54 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 21 May 2024 18:07:50 +0400 Subject: [PATCH 10/12] fix: fix reset in select language feature --- src/popup/features/language/SelectLanguages/model/store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/popup/features/language/SelectLanguages/model/store.ts b/src/popup/features/language/SelectLanguages/model/store.ts index 348d34a..b7d5774 100644 --- a/src/popup/features/language/SelectLanguages/model/store.ts +++ b/src/popup/features/language/SelectLanguages/model/store.ts @@ -22,6 +22,7 @@ languageStore.$languages.listen((newValue) => { if (islanguageCodesDirty) { localStore.defaultValue.set(languageCodes) } else { + localStore.defaultValue.set(languageCodes) localStore.languageCodes.set(languageCodes) } }) From 1d3e83279886e27cc5e66df33cb9c5a17bfe6aba Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 21 May 2024 18:08:58 +0400 Subject: [PATCH 11/12] test: fix test according to browser mock --- src/__tests__/setupTests.ts | 15 +- .../model/__tests__/store.test.ts | 14 +- .../dictionary/model/__tests__/store.test.ts | 9 +- .../language/model/__tests__/store.test.ts | 3 +- src/shared/shared/browser/__mocks__/chrome.ts | 64 ------- .../shared/browser/__test__/chrome.test.ts | 173 ------------------ .../shared/browser/i18n/__mocks__/index.ts | 9 + .../storage/__test__/chromeStorage.test.ts | 3 +- .../shared/browser/tabs/__mocks__/tabs.ts | 13 ++ 9 files changed, 54 insertions(+), 249 deletions(-) delete mode 100644 src/shared/shared/browser/__mocks__/chrome.ts delete mode 100644 src/shared/shared/browser/__test__/chrome.test.ts create mode 100644 src/shared/shared/browser/i18n/__mocks__/index.ts create mode 100644 src/shared/shared/browser/tabs/__mocks__/tabs.ts diff --git a/src/__tests__/setupTests.ts b/src/__tests__/setupTests.ts index 7cb7941..9fc989a 100644 --- a/src/__tests__/setupTests.ts +++ b/src/__tests__/setupTests.ts @@ -1,3 +1,14 @@ -import { chromeMock } from '@shared/shared/browser/__mocks__/chrome' +import { vi } from 'vitest' -global.chrome = chromeMock +import { i18nMock } from '@shared/shared/browser/i18n/__mocks__' +import { storageMock } from '@shared/shared/browser/storage/__mocks__/index.ts' + +vi.mock('@shared/shared/browser/storage', () => { + return { + storage: storageMock, + } +}) + +vi.mock('@shared/shared/browser/i18n', () => ({ + i18n: i18nMock, +})) diff --git a/src/popup/features/language/SelectLanguages/model/__tests__/store.test.ts b/src/popup/features/language/SelectLanguages/model/__tests__/store.test.ts index 3699641..8cfd32e 100644 --- a/src/popup/features/language/SelectLanguages/model/__tests__/store.test.ts +++ b/src/popup/features/language/SelectLanguages/model/__tests__/store.test.ts @@ -1,9 +1,11 @@ import { waitFor } from '@tests/testUtils' import { allTasks, cleanStores, keepMount } from 'nanostores' -import { afterEach, describe, expect, test } from 'vitest' +import { beforeEach, describe, expect, test } from 'vitest' import { languageStore } from '@shared/entities/language' +import { storage } from '@shared/shared/browser/storage' + import { checkIsSelected, localStore, @@ -13,10 +15,16 @@ import { } from '../store' describe('selectedLanguageCodes store', () => { - afterEach(async () => { - await chrome.storage.local.clear() + beforeEach(async () => { + await storage.clear() + cleanStores(languageStore.$languageCodes) cleanStores(localStore.languageCodes) + cleanStores(localStore.islanguageCodesDirty) + cleanStores(localStore.defaultValue) + languageStore.$languages.set([]) localStore.languageCodes.set([]) + localStore.islanguageCodesDirty.set(false) + localStore.defaultValue.set([]) }) describe('commmon', () => { diff --git a/src/shared/entities/dictionary/model/__tests__/store.test.ts b/src/shared/entities/dictionary/model/__tests__/store.test.ts index 99484ce..d75cc93 100644 --- a/src/shared/entities/dictionary/model/__tests__/store.test.ts +++ b/src/shared/entities/dictionary/model/__tests__/store.test.ts @@ -2,7 +2,8 @@ import { waitFor } from '@tests/testUtils' import { allTasks, cleanStores, keepMount } from 'nanostores' import { afterEach, describe, expect, test, vi } from 'vitest' -import { chromeMockClearListeners } from '@shared/shared/browser/__mocks__/chrome' +import { storage } from '@shared/shared/browser/storage' +import { storageMockClearListeners } from '@shared/shared/browser/storage/__mocks__' import { Result } from '@shared/shared/libs/operationResult' import { DICTIONARIES } from '../dictionaries' @@ -16,10 +17,10 @@ describe('dictionary store', () => { } afterEach(async () => { - await chrome.storage.local.clear() + await storage.clear() cleanStores($dictionaries) $dictionaries.set(DICTIONARIES) - chromeMockClearListeners() + storageMockClearListeners() vi.clearAllMocks() }) @@ -39,7 +40,7 @@ describe('dictionary store', () => { test('should set empty array and set error when can not get data from DictionaryStorage', async () => { vi.spyOn(DictionaryStorage, 'get').mockResolvedValueOnce( - Result.Error(error, false), + Result.Error(error), ) keepMount($dictionaries) diff --git a/src/shared/entities/language/model/__tests__/store.test.ts b/src/shared/entities/language/model/__tests__/store.test.ts index b223599..532419c 100644 --- a/src/shared/entities/language/model/__tests__/store.test.ts +++ b/src/shared/entities/language/model/__tests__/store.test.ts @@ -2,6 +2,7 @@ import { waitFor } from '@tests/testUtils' import { allTasks, cleanStores, keepMount } from 'nanostores' import { afterEach, describe, expect, test, vi } from 'vitest' +import { storage } from '@shared/shared/browser/storage' import { Result } from '@shared/shared/libs/operationResult' import { ILanguage } from '..' @@ -15,7 +16,7 @@ describe('languages store', () => { } afterEach(async () => { - await chrome.storage.local.clear() + await storage.clear() cleanStores($languages) $languages.set([]) }) diff --git a/src/shared/shared/browser/__mocks__/chrome.ts b/src/shared/shared/browser/__mocks__/chrome.ts deleted file mode 100644 index f21e1c1..0000000 --- a/src/shared/shared/browser/__mocks__/chrome.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MockedObject, vi } from 'vitest' - -import { TOnChangeListenerProps } from '../storage/types' - -type TChrome = typeof chrome -type TStorageValue = Record - -let store: TStorageValue = {} -let listeners: Array<(changes: TOnChangeListenerProps) => void> = [] - -function getStorageValueByKey(key: string) { - const result: TStorageValue = {} - result[key] = store[key] - return result -} - -export const chromeMockClearListeners = vi.fn(() => { - listeners = [] -}) - -export const chromeMock = { - storage: { - local: { - get: vi.fn((id, cb) => { - const result = id === null ? store : getStorageValueByKey(id) - return cb ? cb(result) : Promise.resolve(result) - }), - - set: vi.fn((payload, cb) => { - const changes: TOnChangeListenerProps = {} - Object.keys(payload).forEach((key) => { - if (payload[key]) { - store[key] = payload[key] - changes[key] = { oldValue: store[key], newValue: payload[key] } - } - }) - - for (const listener of listeners) { - listener(changes) - } - - return cb ? cb() : Promise.resolve() - }), - - onChanged: { - addListener: vi.fn( - (listener: (changes: TOnChangeListenerProps) => void) => { - listeners.push(listener) - }, - ), - removeListener: vi.fn(), - }, - - clear: vi.fn((cb) => { - store = {} - return cb ? cb() : Promise.resolve() - }), - }, - }, - - i18n: { - getMessage: vi.fn((key) => `Translated<${key}>`), - }, -} as unknown as MockedObject diff --git a/src/shared/shared/browser/__test__/chrome.test.ts b/src/shared/shared/browser/__test__/chrome.test.ts deleted file mode 100644 index 99a8413..0000000 --- a/src/shared/shared/browser/__test__/chrome.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { afterEach, describe, expect, test, vi } from 'vitest' - -import { Result } from '@shared/shared/libs/operationResult' - -import { i18n } from '../i18n' -import { storage } from '../storage' - -describe('browser', () => { - describe('storage', () => { - type TStorageValue = object - - const KEY = 'testKey' - const storedValue: TStorageValue = { someData: 'value' } - const defaultValue: TStorageValue = { defaultData: 'defaultValue' } - const circularValue = { prop: 'value', circularRef: {} } - circularValue.circularRef = circularValue - const invalidJSONString = `{"name": "Joe", "age": null]` - - describe('local', () => { - describe('get/set', () => { - afterEach(async () => { - await chrome.storage.local.clear() - vi.restoreAllMocks() - }) - - test('should resolve with error when data in storage is invalid', async () => { - vi.spyOn(chrome.storage.local, 'get').mockImplementation( - (_key, callback) => { - callback({ - [KEY]: invalidJSONString, - }) - }, - ) - - const getResult = await storage.get(KEY, defaultValue) - - expect(getResult.data).toBeNull() - expect(getResult.error).toMatchObject({ - type: 'Translated', - }) - }) - - test('should get default value if storage is empty', async () => { - const getResult = await storage.get(KEY, defaultValue) - - expect(getResult).toEqual(Result.Success(defaultValue)) - }) - - test('should get previously setted data', async () => { - const setResult = await storage.set(KEY, storedValue) - - expect(setResult).toEqual(Result.Success(true)) - - const getResult = await storage.get(KEY, defaultValue) - - expect(getResult).toEqual(Result.Success(storedValue)) - }) - - test('should not set data to the storage if data is not valid', async () => { - let setResult = await storage.set(KEY, storedValue) - - expect(setResult).toEqual(Result.Success(true)) - - setResult = await storage.set(KEY, circularValue) - - expect(setResult.data).toBeNull() - expect(setResult.error).toMatchObject({ - type: 'Translated', - }) - - const getResult = await storage.get(KEY, defaultValue) - - expect(getResult).toEqual(Result.Success(storedValue)) - }) - }) - - describe('onChanged', () => { - const oldValue = { oldValue: 'previousValue' } - const newValue = { newValue: 'updatedValue' } - - test('should invoke callback with parsed new and old values when storage changed', () => { - const callback = vi.fn() - - vi.spyOn( - chrome.storage.local.onChanged, - 'addListener', - ).mockImplementation((listener) => { - listener({ - [KEY]: { - oldValue: JSON.stringify(oldValue), - newValue: JSON.stringify(newValue), - }, - }) - }) - - storage.onChanged.addListener(KEY, callback, defaultValue) - - expect(callback).toHaveBeenCalledWith( - Result.Success({ newValue, oldValue }), - ) - }) - - test('should invoke callback with default old value and parsed new value when storage changed with invalid old value', () => { - const callback = vi.fn() - - vi.spyOn( - chrome.storage.local.onChanged, - 'addListener', - ).mockImplementation((listener) => { - listener({ - [KEY]: { - oldValue: invalidJSONString, - newValue: JSON.stringify(newValue), - }, - }) - }) - - storage.onChanged.addListener(KEY, callback, defaultValue) - - expect(callback).toHaveBeenCalledTimes(2) - - const call1 = callback.mock.calls[0] - expect(call1[0].data).toBeNull() - expect(call1[0].error).toMatchObject({ - type: 'Translated', - }) - - const call2 = callback.mock.calls[1] - expect(call2[0]).toEqual( - Result.Success({ - newValue, - oldValue: defaultValue, - }), - ) - }) - - test('should invoke callback with error when storage changed with invalid new value', () => { - const callback = vi.fn() - - vi.spyOn( - chrome.storage.local.onChanged, - 'addListener', - ).mockImplementation((listener) => { - listener({ - [KEY]: { - oldValue: JSON.stringify(oldValue), - newValue: invalidJSONString, - }, - }) - }) - - storage.onChanged.addListener(KEY, callback, defaultValue) - - const call = callback.mock.calls[0] - expect(call[0].data).toBeNull() - expect(call[0].error).toMatchObject({ - type: 'Translated', - }) - }) - }) - }) - }) - - describe('i18n', () => { - test('should return the message for the given key', () => { - const KEY = 'testKey' - - const message = i18n.getMessage(KEY) - - expect(message).toEqual(`Translated<${KEY}>`) - }) - }) -}) diff --git a/src/shared/shared/browser/i18n/__mocks__/index.ts b/src/shared/shared/browser/i18n/__mocks__/index.ts new file mode 100644 index 0000000..ee6c38f --- /dev/null +++ b/src/shared/shared/browser/i18n/__mocks__/index.ts @@ -0,0 +1,9 @@ +import { vi } from 'vitest' + +import { II18n } from '../types' + +export const i18nMock: II18n = { + getMessage: vi.fn((messageKey: string) => `Translated<${messageKey}>`), + + detectLanguage: vi.fn(), +} diff --git a/src/shared/shared/browser/storage/__test__/chromeStorage.test.ts b/src/shared/shared/browser/storage/__test__/chromeStorage.test.ts index 0b91aa3..e971099 100644 --- a/src/shared/shared/browser/storage/__test__/chromeStorage.test.ts +++ b/src/shared/shared/browser/storage/__test__/chromeStorage.test.ts @@ -3,7 +3,6 @@ import { MockedObject, beforeAll, describe, expect, test, vi } from 'vitest' import { Result } from '@shared/shared/libs/operationResult' -import { storageMock } from '../__mocks__' import { chromeStorageMock } from '../__mocks__/chromeStorageMock' import { chromeStorage } from '../chromeStorage' @@ -20,7 +19,7 @@ describe('chromeStorage', () => { local: chromeStorageMock, }, } as unknown as MockedObject - await storageMock.clear() + await chromeStorageMock.clear() vi.restoreAllMocks() }) diff --git a/src/shared/shared/browser/tabs/__mocks__/tabs.ts b/src/shared/shared/browser/tabs/__mocks__/tabs.ts new file mode 100644 index 0000000..a1388e2 --- /dev/null +++ b/src/shared/shared/browser/tabs/__mocks__/tabs.ts @@ -0,0 +1,13 @@ +import { vi } from 'vitest' + +import { ITabs } from '../types' + +export const tabsMock: ITabs = { + _getActiveTabWithRetry: vi.fn(), + + getActiveTab: vi.fn().mockResolvedValue(2), + + onActivated: { + addListener: vi.fn(), + }, +} From 189b6a368b4a86e277c28abc8e9e23110f884c66 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 21 May 2024 18:10:09 +0400 Subject: [PATCH 12/12] test: add tests for dictionary helpers --- .../model/__tests__/helpers.test.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/shared/entities/dictionary/model/__tests__/helpers.test.ts diff --git a/src/shared/entities/dictionary/model/__tests__/helpers.test.ts b/src/shared/entities/dictionary/model/__tests__/helpers.test.ts new file mode 100644 index 0000000..ee2433d --- /dev/null +++ b/src/shared/entities/dictionary/model/__tests__/helpers.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from 'vitest' + +import { INoteSelectors } from '@shared/entities/note' + +import { getDictionaryByPageUrl, getSelectorsFromDictionary } from '../helpers' +import { IDictionaryWithVariants, IDictionaryWithoutVariants } from '../types' + +describe('dictionary helpers', () => { + describe('getDictionaryByPageUrl', () => { + test('should return null if page url is empty string', () => { + expect(getDictionaryByPageUrl('')).toBeNull() + }) + + test("should return null if dictionaries don't have page url", () => { + expect( + getDictionaryByPageUrl('https://not.existing.dictionary/'), + ).toBeNull() + }) + + test('should return dictionary and language for existing page url', () => { + const dictionary = getDictionaryByPageUrl( + 'https://www.merriam-webster.com/dictionary/shrug', + ) + expect(dictionary?.languageCode).toBe('en') + expect(dictionary?.dictionary.id).toBe('en_MerriamWebster') + }) + }) + + describe('getSelectorsFromDictionary', () => { + const selectors: INoteSelectors = { + text: 'text', + translation: 'translation', + context: 'context', + transcription: 'transcription', + } + + test('should return dictionary selectors for dictionary with variants', () => { + const dictionary: IDictionaryWithVariants = { + id: 'dictionaryId', + name: 'dictionaryName', + url: 'dictionaryUrl', + variants: [ + { + label: 'British', + value: 'br', + selectors: selectors, + }, + { + label: 'American', + value: 'us', + selectors: { + text: '', + translation: '', + context: '', + transcription: '', + }, + }, + ], + activeVariant: 'br', + } + + expect(getSelectorsFromDictionary(dictionary)).toEqual(selectors) + }) + + test('should return dictionary selectors for dictionary with not variants', () => { + const dictionary: IDictionaryWithoutVariants = { + id: 'dictionaryId', + name: 'dictionaryName', + url: 'dictionaryUrl', + selectors: selectors, + } + + expect(getSelectorsFromDictionary(dictionary)).toEqual(selectors) + }) + }) +})