From a9a8ebb59ab0a4eaf5f420242ad8b080cd8ca10c Mon Sep 17 00:00:00 2001 From: hiroro-work Date: Fri, 21 Jul 2023 14:56:52 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E7=94=BB=E9=9D=A2=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=83=91=E3=82=B9=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=A8=E3=81=93=E3=82=8D=E3=81=BE=E3=81=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- src/App.css | 42 ---------------------- src/App.tsx | 35 ------------------ src/assets/react.svg | 1 - src/components/App.tsx | 7 ++++ src/index.css | 69 ------------------------------------ src/main.tsx | 2 +- test/components/App.test.tsx | 11 ++++++ 8 files changed, 21 insertions(+), 149 deletions(-) delete mode 100644 src/App.css delete mode 100644 src/App.tsx delete mode 100644 src/assets/react.svg create mode 100644 src/components/App.tsx delete mode 100644 src/index.css create mode 100644 test/components/App.test.tsx diff --git a/package.json b/package.json index 3a5047e..a3eed50 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "preview": "vite preview", "deploy": "yarn build && firebase deploy --except functions", "test": "vitest run", - "emulators:start:firestore": "firebase emulators:start --project testable-firebase-test --only firestore" + "emulators:start:firestore": "firebase emulators:start --project testable-firebase-test --only firestore", + "emulators:test": "firebase emulators:exec --project testable-firebase-test --only firestore 'yarn test'" }, "dependencies": { "firebase": "^10.1.0", diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index afe48ac..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/App.tsx b/src/components/App.tsx new file mode 100644 index 0000000..ab058a0 --- /dev/null +++ b/src/components/App.tsx @@ -0,0 +1,7 @@ +export const App = () => { + return ( +
+

Sample Chat App

+
+ ); +}; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 2c3fac6..0000000 --- a/src/index.css +++ /dev/null @@ -1,69 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/src/main.tsx b/src/main.tsx index d9f7233..4345d57 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App.tsx'; +import { App } from './components/App'; import './index.css'; import '@/lib/firebase'; diff --git a/test/components/App.test.tsx b/test/components/App.test.tsx new file mode 100644 index 0000000..3efcba4 --- /dev/null +++ b/test/components/App.test.tsx @@ -0,0 +1,11 @@ +import { render, cleanup, screen } from '@testing-library/react'; +import { App } from '@/components/App'; + +describe('App', () => { + afterEach(() => cleanup()); + + it('タイトル文字列が表示される', async () => { + render(); + expect(screen.getByText('Sample Chat App')).toBeTruthy(); + }); +}); From 03960e7cbaf20c5a88a7aafe2464b828d8984bdc Mon Sep 17 00:00:00 2001 From: hiroro-work Date: Fri, 21 Jul 2023 15:11:19 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E4=BD=9C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/contexts/UsersContext.tsx | 29 +++++++++++++++++++++++++++ src/hooks/useCollectionData.ts | 10 ++++++++++ test/contexts/UsersContext.test.tsx | 31 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/contexts/UsersContext.tsx create mode 100644 src/hooks/useCollectionData.ts create mode 100644 test/contexts/UsersContext.test.tsx diff --git a/src/contexts/UsersContext.tsx b/src/contexts/UsersContext.tsx new file mode 100644 index 0000000..ca1a64b --- /dev/null +++ b/src/contexts/UsersContext.tsx @@ -0,0 +1,29 @@ +import { ReactNode, createContext, useContext, useMemo } from 'react'; +import { keyBy } from 'lodash-es'; +import { User } from '@/types/user'; +import { useCollectionData } from '@/hooks/useCollectionData'; +import { usersRef } from '@/lib/user'; + +type UsersContextValue = { + users: User[]; + usersById: { [id: string]: User }; + loading: boolean; +}; + +export const UsersContext = createContext({ + users: [], + usersById: {}, + loading: true, +}); + +export const UsersProvider = ({ children }: { children: ReactNode }) => { + const [users, loading] = useCollectionData(usersRef()); + const usersById = useMemo(() => keyBy(users, 'id'), [users]); + + return {children}; +}; + +export const useUsers = () => { + const { users, usersById, loading } = useContext(UsersContext); + return { users, usersById, loading }; +}; diff --git a/src/hooks/useCollectionData.ts b/src/hooks/useCollectionData.ts new file mode 100644 index 0000000..2814948 --- /dev/null +++ b/src/hooks/useCollectionData.ts @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; +import { Query } from 'firebase/firestore'; +import { useCollectionData as _useCollectionData } from 'react-firebase-hooks/firestore'; + +export const useCollectionData = (_query: Query, deps: unknown[] = []) => { + const query = useMemo(() => _query, deps); + return _useCollectionData(query, { + snapshotOptions: { serverTimestamps: 'estimate' }, + }); +}; diff --git a/test/contexts/UsersContext.test.tsx b/test/contexts/UsersContext.test.tsx new file mode 100644 index 0000000..6971bda --- /dev/null +++ b/test/contexts/UsersContext.test.tsx @@ -0,0 +1,31 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useUsers, UsersProvider } from '@/contexts/UsersContext'; +import { ReactNode } from 'react'; + +describe('useUsers', () => { + const wrapper = ({ children }: { children: ReactNode }) => {children}; + + vi.mock('@/hooks/useCollectionData', () => { + return { + useCollectionData: () => [[{ id: 'test-user-uid', name: 'てすたろう' }], false], + }; + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it('usersとusersById,loadingを返す', async () => { + const { result } = renderHook(() => useUsers(), { wrapper }); + expect(result.current).toEqual({ + users: [{ id: 'test-user-uid', name: 'てすたろう' }], + usersById: { + 'test-user-uid': { + id: 'test-user-uid', + name: 'てすたろう', + }, + }, + loading: false, + }); + }); +}); From 3a21fb2b1b33a49884dac7e7b881c8a87f93cbfc Mon Sep 17 00:00:00 2001 From: hiroro-work Date: Fri, 21 Jul 2023 15:14:06 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Message=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=8D=E3=83=B3=E3=83=88=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/components/Message.tsx | 23 +++++++++++++ test/components/Message.test.tsx | 58 ++++++++++++++++++++++++++++++++ yarn.lock | 9 ++++- 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/components/Message.tsx create mode 100644 test/components/Message.test.tsx diff --git a/package.json b/package.json index a3eed50..b1fe37f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "emulators:test": "firebase emulators:exec --project testable-firebase-test --only firestore 'yarn test'" }, "dependencies": { + "date-fns": "^2.30.0", "firebase": "^10.1.0", "lodash-es": "^4.17.21", "react": "^18.2.0", diff --git a/src/components/Message.tsx b/src/components/Message.tsx new file mode 100644 index 0000000..a21b903 --- /dev/null +++ b/src/components/Message.tsx @@ -0,0 +1,23 @@ +import { format } from 'date-fns'; +import { useUsers } from '@/contexts/UsersContext'; +import { LoadingScreen } from './LoadingScreen'; +import { Message as MessageType } from '@/types/message'; +import nonameIcon from '@/images/noname.png'; + +export const Message = ({ message }: { message: MessageType }) => { + const { usersById, loading } = useUsers(); + const sender = usersById[message.senderId]; + + if (loading) return ; + + return ( +
+
+ + {sender?.name || '名無しさん'} + {format(message.createdAt.toDate(), 'yyyy-MM-dd HH:mm')} +
+

{message.content}

+
+ ); +}; diff --git a/test/components/Message.test.tsx b/test/components/Message.test.tsx new file mode 100644 index 0000000..a858bb9 --- /dev/null +++ b/test/components/Message.test.tsx @@ -0,0 +1,58 @@ +import { render, cleanup, screen, waitFor } from '@testing-library/react'; +import { userFactory } from '@/../test/factories/user'; +import { messageFactory } from '@/../test/factories/message'; +import { Timestamp } from 'firebase/firestore'; + +const sender = userFactory.build({ + id: 'user-id', + name: 'テストユーザー', + photoUrl: 'user-photo-url', +}); +vi.mock('@/context/UsersContext', () => { + return { + useUsers: { usersById: { 'user-id': [sender] } }, + }; +}); + +describe('Message', async () => { + const { Message } = await import('@/components/Message'); + + afterEach(() => cleanup()); + + vi.mock('@/context/UsersContext', () => { + return { + useUsers: { usersById: { 'user-id': [sender] } }, + }; + }); + + const message = messageFactory.build({ + content: `テストのメッセージ`, + senderId: 'user-id', + createdAt: Timestamp.fromDate(new Date('2022-07-01 00:00:00+09:00')), + }); + + it('loading中はloadingメッセージが表示される', () => { + render(); + expect(screen.getByText('loading...')).toBeTruthy(); + }); + + it('アイコン画像が表示される', () => { + render(); + waitFor(() => expect(screen.getByRole('img').getAttribute('src')).toBe('user-photo-url')); + }); + + it('送信者の名前が表示される', () => { + render(); + waitFor(() => expect(screen.getByText('テストユーザー')).toBeTruthy()); + }); + + it('送信時間が表示される', () => { + render(); + waitFor(() => expect(screen.getByText('2022-07-01 00:00')).toBeTruthy()); + }); + + it('メッセージが表示される', () => { + render(); + waitFor(() => expect(screen.getByText('テストのメッセージ')).toBeTruthy()); + }); +}); diff --git a/yarn.lock b/yarn.lock index 56a82fb..50434bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -178,7 +178,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/runtime@^7.12.5": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.21.0": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== @@ -1502,6 +1502,13 @@ data-urls@^4.0.0: whatwg-mimetype "^3.0.0" whatwg-url "^12.0.0" +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"