diff --git a/lang/default.json b/lang/default.json index 6c91282314..7ed32826c3 100644 --- a/lang/default.json +++ b/lang/default.json @@ -707,6 +707,10 @@ "defaultMessage": "invites you to join the circle {circleName} , and you can experience it for {freePeriod} days for free", "description": "src/components/Notice/CircleNotice/CircleInvitationNotice.tsx" }, + "Ix3e3Q": { + "defaultMessage": "Comment", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "J6f6iN": { "defaultMessage": "Number of comments", "description": "src/components/ArticleDigest/Published/FooterActions/index.tsx" @@ -1252,6 +1256,10 @@ "defaultMessage": "Most comments", "description": "src/views/Me/Works/Published/SortTabs.tsx" }, + "aZ6EYx": { + "defaultMessage": "Send", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "aa0nss": { "defaultMessage": "Unpin from Trending", "description": "src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx" diff --git a/lang/en.json b/lang/en.json index b353fbc186..3ad7cac4b6 100644 --- a/lang/en.json +++ b/lang/en.json @@ -707,6 +707,10 @@ "defaultMessage": "invites you to join the circle {circleName} , and you can experience it for {freePeriod} days for free", "description": "src/components/Notice/CircleNotice/CircleInvitationNotice.tsx" }, + "Ix3e3Q": { + "defaultMessage": "Comment", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "J6f6iN": { "defaultMessage": "Number of comments", "description": "src/components/ArticleDigest/Published/FooterActions/index.tsx" @@ -1252,6 +1256,10 @@ "defaultMessage": "Most comments", "description": "src/views/Me/Works/Published/SortTabs.tsx" }, + "aZ6EYx": { + "defaultMessage": "Send", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "aa0nss": { "defaultMessage": "Unpin from Trending", "description": "src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx" diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 7cd22a78c0..63873890cc 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -707,6 +707,10 @@ "defaultMessage": "邀你加入围炉 {circleName} ,你可以免费体验 {freePeriod} 天", "description": "src/components/Notice/CircleNotice/CircleInvitationNotice.tsx" }, + "Ix3e3Q": { + "defaultMessage": "发布评论", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "J6f6iN": { "defaultMessage": "评论数量", "description": "src/components/ArticleDigest/Published/FooterActions/index.tsx" @@ -1252,6 +1256,10 @@ "defaultMessage": "最多评论", "description": "src/views/Me/Works/Published/SortTabs.tsx" }, + "aZ6EYx": { + "defaultMessage": "送出", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "aa0nss": { "defaultMessage": "取消精选", "description": "src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx" diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index c3fb379920..0c08a74fca 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -707,6 +707,10 @@ "defaultMessage": "邀你加入圍爐 {circleName} ,你可以免費體驗 {freePeriod} 天", "description": "src/components/Notice/CircleNotice/CircleInvitationNotice.tsx" }, + "Ix3e3Q": { + "defaultMessage": "發布評論", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "J6f6iN": { "defaultMessage": "評論數量", "description": "src/components/ArticleDigest/Published/FooterActions/index.tsx" @@ -1252,6 +1256,10 @@ "defaultMessage": "最多評論", "description": "src/views/Me/Works/Published/SortTabs.tsx" }, + "aZ6EYx": { + "defaultMessage": "送出", + "description": "src/components/Forms/CommentForm/index.tsx" + }, "aa0nss": { "defaultMessage": "取消精選", "description": "src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx" diff --git a/src/components/Forms/CommentForm/CommentForm.test.tsx b/src/components/Forms/CommentForm/CommentForm.test.tsx new file mode 100644 index 0000000000..268836baff --- /dev/null +++ b/src/components/Forms/CommentForm/CommentForm.test.tsx @@ -0,0 +1,41 @@ +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' + +import { render, screen } from '~/common/utils/test' + +import { CommentForm } from './' + +describe('', () => { + it('should render a CommentForm', async () => { + const placeholder = 'Test placeholder' + const handleSubmit = vi.fn() + + const { container } = render( + + ) + + // eslint-disable-next-line + const $editor = container.querySelector('.ProseMirror')! + expect($editor).toBeInTheDocument() + + // eslint-disable-next-line + const $placeholder = container.querySelector('[data-placeholder]')! + expect($placeholder).toHaveAttribute('data-placeholder', placeholder) + + const comment = 'Test comment' + await userEvent.type($editor, comment) + expect($editor).toHaveTextContent(comment) + + // submit & loading + const $submit = screen.getByRole('button', { name: 'Send' }) + expect($submit).toBeInTheDocument() + $submit.click() + expect( + screen.queryByRole('button', { name: 'Send' }) + ).not.toBeInTheDocument() + }) +}) diff --git a/src/components/Forms/CommentForm/index.tsx b/src/components/Forms/CommentForm/index.tsx index 059874ac7e..5c75d2c51a 100644 --- a/src/components/Forms/CommentForm/index.tsx +++ b/src/components/Forms/CommentForm/index.tsx @@ -1,15 +1,14 @@ import { useQuery } from '@apollo/react-hooks' import dynamic from 'next/dynamic' -import { useContext, useState } from 'react' +import { useState } from 'react' +import { FormattedMessage, useIntl } from 'react-intl' -import { dom, stripHtml, translate } from '~/common/utils' +import { dom, stripHtml } from '~/common/utils' import { Button, IconSpinner16, - LanguageContext, Spinner, TextIcon, - Translate, useMutation, } from '~/components' import PUT_COMMENT from '~/components/GQL/mutations/putComment' @@ -52,7 +51,7 @@ export const CommentForm: React.FC = ({ placeholder, }) => { - const { lang } = useContext(LanguageContext) + const intl = useIntl() // retrieve comment draft const commentDraftId = `${articleId || circleId}-${type}-${commentId || 0}-${ @@ -132,7 +131,11 @@ export const CommentForm: React.FC = ({ className={styles.form} id={formId} onSubmit={handleSubmit} - aria-label={translate({ id: 'putComment', lang })} + aria-label={intl.formatMessage({ + defaultMessage: 'Comment', + id: 'Ix3e3Q', + description: 'src/components/Forms/CommentForm/index.tsx', + })} >
= ({ icon={isSubmitting && } > {isSubmitting ? null : ( - + )} diff --git a/src/components/Search/SearchBar/SearchBar.test.tsx b/src/components/Search/SearchBar/SearchBar.test.tsx new file mode 100644 index 0000000000..e49e4760c5 --- /dev/null +++ b/src/components/Search/SearchBar/SearchBar.test.tsx @@ -0,0 +1,62 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { INPUT_DEBOUNCE } from '~/common/enums' +import { act, fireEvent, render, screen } from '~/common/utils/test' +import { SearchBar } from '~/components' + +beforeEach(() => { + // Tests should run in serial for improved isolation + // To prevent collision with global state, reset all toasts for each test + vi.useFakeTimers() +}) + +afterEach(() => { + act(() => { + vi.runAllTimers() + vi.useRealTimers() + }) +}) + +const waitTime = (time: number) => { + act(() => { + vi.advanceTimersByTime(time) + }) +} + +describe('', () => { + it('should render a SeachBar', () => { + render() + + // search button + const $searchButton = screen.getByRole('button', { name: 'Search' }) + expect($searchButton).toBeInTheDocument() + + // search input + const $searchInput = screen.getByPlaceholderText('Search') + expect($searchInput).toBeInTheDocument() + expect($searchInput).toHaveAttribute('type', 'search') + expect($searchInput).toHaveAttribute('aria-label', 'Search') + expect($searchInput).toHaveAttribute('name', 'q') + expect($searchInput).toHaveAttribute('value', '') + }) + + it('should update search bar value when typing', async () => { + const handleOnChange = vi.fn() + + render() + + const $searchInput = screen.getByPlaceholderText('Search') + expect($searchInput).toBeInTheDocument() + + const searchValue = 'test' + fireEvent.change($searchInput, { target: { value: searchValue } }) + expect($searchInput).toHaveValue(searchValue) + + waitTime(INPUT_DEBOUNCE) + expect(handleOnChange).toBeCalledWith(searchValue) + + // dropdown + const $dropdown = screen.getByText(/Loading/i) + expect($dropdown).toBeInTheDocument() + }) +}) diff --git a/src/components/Search/SearchBar/index.tsx b/src/components/Search/SearchBar/index.tsx index d0eeebce2e..7781abde4b 100644 --- a/src/components/Search/SearchBar/index.tsx +++ b/src/components/Search/SearchBar/index.tsx @@ -83,11 +83,7 @@ export const SearchBar: React.FC = ({ const [debouncedSearch] = useDebounce(search, INPUT_DEBOUNCE) const intl = useIntl() - const textAriaLabel = intl.formatMessage({ - defaultMessage: 'Search', - id: 'xmcVZ0', - }) - const textPlaceholder = intl.formatMessage({ + const searchText = intl.formatMessage({ defaultMessage: 'Search', id: 'xmcVZ0', }) @@ -121,9 +117,10 @@ export const SearchBar: React.FC = ({ } useNativeEventListener('keydown', (event: KeyboardEvent) => { - if (event.code.toLowerCase() === KEYVALUE.arrowUp) { - if (!showDropdown) return + if (!showDropdown) return + const code = event.code.toLowerCase() + if (code === KEYVALUE.arrowUp) { event.preventDefault() const activeIndex = items.indexOf(activeItem) if (activeIndex === 0) return @@ -131,9 +128,7 @@ export const SearchBar: React.FC = ({ setActiveItem(items[activeIndex - 1]) } - if (event.code.toLowerCase() === KEYVALUE.arrowDown) { - if (!showDropdown) return - + if (code === KEYVALUE.arrowDown) { event.preventDefault() const activeIndex = items.indexOf(activeItem) if (activeIndex === items.length - 1) return @@ -141,9 +136,7 @@ export const SearchBar: React.FC = ({ setActiveItem(items[activeIndex + 1]) } - if (event.code.toLowerCase() === KEYVALUE.escape) { - if (!showDropdown) return - + if (code === KEYVALUE.escape) { setShowDropdown(false) } }) @@ -199,19 +192,19 @@ export const SearchBar: React.FC = ({
{ handleChange(e) @@ -272,13 +265,13 @@ export const SearchBar: React.FC = ({ ref={ref} > { handleChange(e) diff --git a/vitest.setup.ts b/vitest.setup.ts index d870a18f6b..842dc92495 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -38,6 +38,32 @@ Object.defineProperty(window, 'matchMedia', { })), }) +document.createRange = () => { + const range = new Range() + + range.getBoundingClientRect = vi.fn(() => ({ + x: 851.671875, + y: 200.046875, + width: 8.34375, + height: 17, + top: 967.046875, + right: 860.015625, + bottom: 984.046875, + left: 851.671875, + toJSON: vi.fn(), + })) + + // @ts-ignore + range.getClientRects = vi.fn(() => ({ + item: () => null, + length: 0, + })) + + return range +} + +document.elementFromPoint = vi.fn(() => null) + vi.mock('next/router', () => require('next-router-mock')) vi.mock('next/dynamic', async () => {