Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat select languages #7

Merged
merged 14 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading