Skip to content

Commit

Permalink
Merge pull request #7 from Rue-pro/feat-select-languages
Browse files Browse the repository at this point in the history
Feat select languages
  • Loading branch information
Rue-pro authored Mar 21, 2024
2 parents 4fffac1 + c0df961 commit abdade9
Show file tree
Hide file tree
Showing 22 changed files with 536 additions and 44 deletions.
24 changes: 24 additions & 0 deletions public/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,29 @@
},
"CLOSE": {
"message": "Close"
},
"CANCEL": {
"message": "Cancel"
},
"SAVE": {
"message": "Save"
},
"ERROR_CAN_NOT_GET_DATA_FROM_STORAGE": {
"message": "Can't get data from storage"
},
"ERROR_CAN_NOT_UPDATE_DATA_IN_STORAGE": {
"message": "Can't update data in storage"
},
"ERROR_CAN_NOT_GET_OLD_DATA_FROM_STORAGE": {
"message": "Data in storage changed, but can't get old data"
},
"ERROR_CAN_NOT_GET_NEW_DATA_FROM_STORAGE": {
"message": "Data in storage changed, can't get new data"
},
"SELECTED_LANGUAGES_SAVED": {
"message": "Selected languages succesfully saved"
},
"SELECT_LANGUAGES_FORM_TITLE": {
"message": "Select languages to learn"
}
}
18 changes: 14 additions & 4 deletions src/__tests__/testUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import { RenderOptions, render } from '@testing-library/preact'
import {
RenderOptions,
renderHook as baseRenderHook,
render,
} from '@testing-library/preact'
import { ComponentChildren } from 'preact'

const AllTheProviders = ({ children }: { children: ComponentChildren }) => {
return children
return <>{children}</>
}

const customRender = (
ui: ComponentChildren,
options?: Omit<RenderOptions, 'queries'>,
) => render(ui, { wrapper: AllTheProviders, ...options })

// re-export everything
export * from '@testing-library/preact'

// override render method
export { customRender as render }

export const renderHook = <Result, Props>(
hook: (initialProps: Props) => Result,
) => baseRenderHook(hook, { wrapper: AllTheProviders })

export const INVALID_JSON_STRING = `{"name": "Joe", "age": null]`
export const CIRCULAR_VALUE = { prop: 'value', circularRef: {} }
CIRCULAR_VALUE.circularRef = CIRCULAR_VALUE
5 changes: 5 additions & 0 deletions src/app/popup/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { SelectLanguages } from '@features/language/SelectLanguages'

import { LANGUAGES } from '@entities/language'

import { Toasts } from '@shared/ui/Toast'

export const Popup = () => {
return (
<main>
<SelectLanguages languages={LANGUAGES} />
<Toasts />
</main>
)
Expand Down
13 changes: 13 additions & 0 deletions src/app/popup/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,16 @@ main {
width: 320px;
height: 480px;
}

.form {
&__title {
margin-bottom: var(--spacing-2);
}

&__footer {
display: flex;
gap: var(--spacing-2);
justify-content: flex-end;
margin-top: var(--spacing-2);
}
}
1 change: 1 addition & 0 deletions src/entities/language/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './model'
2 changes: 2 additions & 0 deletions src/entities/language/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { TLanguageCode, ILanguage } from './types'
export { LANGUAGES } from './languages'
10 changes: 10 additions & 0 deletions src/entities/language/model/languages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ILanguage } from '.'

export const languageCodes = ['en', 'jp', 'pt', 'ko'] as const

export const LANGUAGES: ILanguage[] = [
{ label: 'English', value: 'en' },
{ label: 'Japanese', value: 'jp' },
{ label: 'Portuguese', value: 'pt' },
{ label: 'Korean', value: 'ko' },
]
7 changes: 7 additions & 0 deletions src/entities/language/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { languageCodes } from './languages'

export type TLanguageCode = (typeof languageCodes)[number]
export interface ILanguage {
label: string
value: TLanguageCode
}
1 change: 1 addition & 0 deletions src/entities/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './model'
35 changes: 35 additions & 0 deletions src/entities/storage/model/Storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { browser } from '@shared/browser'
import { TResult } from '@shared/libs/operationResult'

export class Storage<StorageValue> {
key: string
defaultValue: StorageValue

constructor(key: string, defaultValue: StorageValue) {
this.key = key
this.defaultValue = defaultValue
}

get() {
return browser.storage.local.get<StorageValue>(this.key, this.defaultValue)
}

set(value: StorageValue) {
return browser.storage.local.set<StorageValue>(this.key, value)
}

onChanged(
callback: (
changes: TResult<{
newValue: StorageValue
oldValue: StorageValue
}>,
) => void,
) {
browser.storage.local.onChanged<StorageValue>(
this.key,
callback,
this.defaultValue,
)
}
}
1 change: 1 addition & 0 deletions src/entities/storage/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Storage } from './Storage'
1 change: 1 addition & 0 deletions src/features/language/SelectLanguages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ui'
12 changes: 12 additions & 0 deletions src/features/language/SelectLanguages/model/__mock__/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MockedFunction, vi } from 'vitest'

import { checkIsSelected, commit, reset, toggle } from '../store'

export const commitMock: MockedFunction<typeof commit> = vi.fn()

export const resetMock: MockedFunction<typeof reset> = vi.fn()

export const toggleMock: MockedFunction<typeof toggle> = vi.fn()

export const checkIsSelectedMock: MockedFunction<typeof checkIsSelected> =
vi.fn()
121 changes: 121 additions & 0 deletions src/features/language/SelectLanguages/model/__tests__/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { allTasks, cleanStores, keepMount } from 'nanostores'
import { afterEach, describe, expect, test, vi } from 'vitest'

import { Result } from '@shared/libs/operationResult'
import { getErrorToastMock } from '@shared/ui/Toast/helpers/__mock__/getErrorToast'
import { addToastMock } from '@shared/ui/Toast/model/__mock__/store'

import { waitFor } from '@tests/testUtils'

import {
$selectedLanguages,
SelectedLanguagesStorage,
checkIsSelected,
commit,
reset,
toggle,
} from '../store'

describe('selectedLanguages store', () => {
const error = {
type: `ERROR`,
error: null,
}

afterEach(async () => {
await chrome.storage.local.clear()
cleanStores($selectedLanguages)
$selectedLanguages.set([])
})

test('should initialize with empty array', () => {
keepMount($selectedLanguages)

expect($selectedLanguages.get()).toEqual([])
expect(getErrorToastMock).toBeCalledTimes(0)
})

describe('commmon', () => {
test('should set value from SelectedLanguagesStorage', async () => {
await SelectedLanguagesStorage.set(['en'])

keepMount($selectedLanguages)
await allTasks()

expect($selectedLanguages.get()).toEqual(['en'])
expect(getErrorToastMock).toBeCalledTimes(0)
})

test('should set empty array and show toast with error when SelectedLanguagesStorage returns error', async () => {
vi.spyOn(SelectedLanguagesStorage, 'get').mockResolvedValueOnce(
Result.Error(error),
)

keepMount($selectedLanguages)
await allTasks()

expect($selectedLanguages.get()).toEqual([])
expect(getErrorToastMock).toBeCalledWith(error)
})
})

describe('toggle', () => {
test('should select language if not selected and reset', async () => {
await SelectedLanguagesStorage.set([])

keepMount($selectedLanguages)
await allTasks()

toggle('en')
expect(checkIsSelected('en')).toBeTruthy()

reset()
expect(checkIsSelected('en')).toBeFalsy()
})

test('should unselect language if selected and reset', async () => {
await SelectedLanguagesStorage.set(['en'])

keepMount($selectedLanguages)
await allTasks()

toggle('en')
expect(checkIsSelected('en')).toBeFalsy()

reset()
expect(checkIsSelected('en')).toBeTruthy()
})
})

describe('commit', () => {
test('should set to selected languages store selected values and show opration success information', async () => {
$selectedLanguages.set(['en'])

keepMount($selectedLanguages)
await allTasks()

waitFor(async () => {
await commit()

const call = addToastMock.mock.calls[0]
expect(call[0].type).toBe('success')
expect(call[0].title).toBeDefined()
})
})

test('should show error when can not update languages', async () => {
vi.spyOn(SelectedLanguagesStorage, 'set').mockResolvedValue(
Result.Error(error),
)

keepMount($selectedLanguages)
await allTasks()

waitFor(async () => {
await commit()

expect(getErrorToastMock).toBeCalledWith(error)
})
})
})
})
56 changes: 56 additions & 0 deletions src/features/language/SelectLanguages/model/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { atom, onMount, task } from 'nanostores'

import { TLanguageCode } from '@entities/language'
import { Storage } from '@entities/storage'

import { browser } from '@shared/browser'
import { addToast, getErrorToast } from '@shared/ui/Toast'

export const SelectedLanguagesStorage = new Storage<TLanguageCode[]>(
'selected_languages',
[],
)

let defaultValue: TLanguageCode[] = []
export const $selectedLanguages = atom<TLanguageCode[]>([])

onMount($selectedLanguages, () => {
task(async () => {
const getResult = await SelectedLanguagesStorage.get()
if (getResult.data) {
defaultValue = getResult.data
$selectedLanguages.set(getResult.data)
} else {
addToast(getErrorToast(getResult.error))
}
})
})

export const toggle = async (languageCode: TLanguageCode) => {
const isSelected = $selectedLanguages.get().includes(languageCode)

const newSelectedLanguages = isSelected
? $selectedLanguages.get().filter((lang) => lang !== languageCode)
: [...$selectedLanguages.get(), languageCode]

$selectedLanguages.set(newSelectedLanguages)
}

export const checkIsSelected = (languageCode: TLanguageCode) => {
return $selectedLanguages.get().includes(languageCode)
}

export const reset = () => {
$selectedLanguages.set(defaultValue)
}

export const commit = async () => {
const setResult = await SelectedLanguagesStorage.set($selectedLanguages.get())

setResult.data
? addToast({
type: 'success',
title: browser.i18n.getMessage('SELECTED_LANGUAGES_SAVED'),
})
: addToast(getErrorToast(setResult.error))
}
Loading

0 comments on commit abdade9

Please sign in to comment.