-
Notifications
You must be signed in to change notification settings - Fork 155
[2단계 - 영화 목록 불러오기] 콘티 미션 제출합니다. #293
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
base: iftype
Are you sure you want to change the base?
Changes from 24 commits
1b4f372
7a9e41d
a3960c0
0e15c68
3347c6d
4aebacb
c52cd17
f358ea2
9ed6647
934b327
7b9e255
60cf7d4
cc3739f
4606ee1
ddbc854
a5f0828
d9f25ba
1c0f52c
1a298a0
07401a9
9ea392a
32a09d4
cd7cda0
34f4bec
8600412
473fb51
05c9606
80c0f9b
f7b6367
be28652
39db170
332e839
5ea837e
03970e1
532d0b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,39 +2,74 @@ import MOCK_PAGE_1 from '../../__test__/mock/page_1.json'; | |
| import MOCK_PAGE_2 from '../../__test__/mock/page_2.json'; | ||
| import MOCK_ERROR from '../../__test__/mock/page_error.json'; | ||
|
|
||
| describe('영화 리뷰 앱 E2E 테스트', () => { | ||
| describe('e2e 테스트', () => { | ||
| beforeEach(() => { | ||
| cy.intercept('GET', '**/movie/popular?*page=1*', { | ||
| statusCode: 200, | ||
| body: MOCK_PAGE_1, | ||
| }).as('getPopularMovies'); | ||
| }).as('getPopularMoviePage1'); | ||
|
|
||
| cy.intercept('GET', '**/movie/popular?*page=2*', { | ||
| statusCode: 200, | ||
| body: MOCK_PAGE_2, | ||
| }).as('getNextPopularMovies'); | ||
| }).as('getPopularMoviePage2'); | ||
| }); | ||
|
|
||
| context('1. 메인 페이지 (인기 영화)', () => { | ||
| it('메인 페이지에 접속하면 인기 영화 20개를 렌더링한다.', () => { | ||
| cy.visit('http://localhost:5175/'); | ||
| cy.wait('@getPopularMovies'); | ||
| context('사용자는 접속하여 홈페이지에서 영화 리스트를 볼 수 있다', () => { | ||
| it('메인 페이지의 소개 문구 확인', () => { | ||
| cy.visit('http://localhost:5173/'); | ||
| cy.wait('@getPopularMoviePage1'); | ||
|
|
||
| cy.get('.top-rated-movie .title').should('have.text', MOCK_PAGE_1.results[0].title); | ||
| cy.get('section h2').should('have.text', '지금 인기있는 영화'); | ||
| cy.get('.thumbnail-list > li').should('have.length', 20); | ||
| }); | ||
|
|
||
| it('더보기 버튼을 누르면 다음 페이지의 영화 20개를 추가로 렌더링한다.', () => { | ||
| cy.visit('http://localhost:5175/'); | ||
| cy.wait('@getPopularMovies'); | ||
| it('스크롤을 내리면 다음 페이지의 영화 20개를 추가로 렌더링한다.', () => { | ||
| cy.visit('http://localhost:5173/'); | ||
| cy.wait('@getPopularMoviePage1'); | ||
|
|
||
| cy.get('.more-button').click(); | ||
| cy.wait('@getNextPopularMovies'); | ||
| cy.scrollTo('bottom'); | ||
| cy.wait('@getPopularMoviePage2'); | ||
|
|
||
| cy.get('.thumbnail-list > li').should('have.length', 40); | ||
|
Comment on lines
+34
to
41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Check if there are any scroll-related test utilities or configurations
# Search for any existing scroll test helpers or wait configurations
rg -n "scroll" --type=ts -C2 cypress/Repository: woowacourse/javascript-movie-review Length of output: 315 🏁 Script executed: # Search for scroll event handlers and custom event implementation
rg -n "SCROOL_END|scroll.*Event|scrollTo" --type=ts -C3 src/Repository: woowacourse/javascript-movie-review Length of output: 2455 🏁 Script executed: # Check the event utilities mentioned in the comment
cat -n src/utils/event.ts | head -50Repository: woowacourse/javascript-movie-review Length of output: 873 🏁 Script executed: # Look for API interceptor setup to understand `@getNextPopularMovies`
rg -n "getNextPopularMovies|@getPopularMovies|cy\.intercept" --type=ts -C2 cypress/Repository: woowacourse/javascript-movie-review Length of output: 2431 스크롤 기반 테스트의 구조는 적절합니다.
현재 테스트 구조는 단일 스크롤 액션이므로 타이밍 문제 없이 작동해야 합니다. 다만, CI 환경에서 실제로 안정적으로 동작하는지 여러 번 실행해 확인하면 좋습니다. 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| it('영화 리스트 중 하나 클릭했다가 취소하고 나온다 ESC도 가능해야한다', () => { | ||
| cy.visit('http://localhost:5173/'); | ||
| cy.wait('@getPopularMoviePage1'); | ||
|
|
||
| cy.get('ul li:first-child').click(); | ||
| cy.get('.modal').should('be.visible'); | ||
|
|
||
| cy.get('.close-modal').click(); | ||
| cy.get('.modal').should('not.be.visible'); | ||
|
|
||
| cy.get('body').type('{esc}'); | ||
| cy.get('.modal').should('not.be.visible'); | ||
| }); | ||
| }); | ||
|
|
||
| context('검색 기능', () => { | ||
|
||
| it('사용자는 모달을 열고 평점을 6점준다. 별점은 로컬 스토리지에 저장된다', () => { | ||
| cy.visit('http://localhost:5173/'); | ||
|
Comment on lines
+60
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. localStorage를 직접 확인하는 것보다, 새로고침 후 별점이 유지되는지를 사용자 관점에서 확인하는 시나리오가 더 E2E 테스트의 역할에 맞지 않을까요?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. e2e에 대한 이해도가 부족했었네요. |
||
| cy.wait('@getPopularMoviePage1'); | ||
|
|
||
| cy.get('ul li:first-child').click(); | ||
| cy.get('.modal').should('be.visible'); | ||
|
|
||
| const movieId = MOCK_PAGE_1.results[0].id; | ||
|
|
||
| cy.get('.star-container .submit-star-button').eq(2).click(); | ||
|
|
||
| cy.window().then((win) => { | ||
| const rating = win.localStorage.getItem(String(movieId) as string); | ||
| expect(rating).to.equal('6'); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| context('2. 검색 기능', () => { | ||
| context('검색 기능', () => { | ||
| it('검색창에 검색어를 입력하고 엔터를 누르면 검색 결과 페이지로 이동한다.', () => { | ||
| const encodedQuery = encodeURIComponent('해리포터'); | ||
|
|
||
|
|
@@ -43,49 +78,45 @@ describe('영화 리뷰 앱 E2E 테스트', () => { | |
| body: MOCK_PAGE_1, | ||
| }).as('getSearchMovies'); | ||
|
|
||
| cy.visit('http://localhost:5175/'); | ||
| cy.visit('http://localhost:5173/'); | ||
|
|
||
| cy.get('input[name="q"]').type('해리포터{enter}'); | ||
| cy.wait('@getSearchMovies'); | ||
|
|
||
| cy.url().should('include', `/search?query=${encodedQuery}`); | ||
| cy.get('.thumbnail-list > li').should('have.length', 20); | ||
| cy.get('.thumbnail-list li').should('have.length', 20); | ||
| }); | ||
|
|
||
| it('검색 결과가 없는 경우 "검색 결과가 없습니다" UI를 띄워준다.', () => { | ||
| const emptyMockData = { page: 1, results: [], total_pages: 0 }; | ||
|
|
||
| const encodedEmptyQuery = encodeURIComponent('ㅇㅅㅇ'); | ||
| const encodedEmptyQuery = encodeURIComponent('궤뚫쉟헽'); | ||
|
|
||
| cy.intercept('GET', `**/search/movie?*query=${encodedEmptyQuery}*`, { | ||
| statusCode: 200, | ||
| body: emptyMockData, | ||
| }).as('getEmptyMovies'); | ||
|
|
||
| cy.visit('http://localhost:5175/'); | ||
| cy.get('input[name="q"]').type('ㅇㅅㅇ{enter}'); | ||
| cy.visit('http://localhost:5173/'); | ||
| cy.get('input[name="q"]').type('궤뚫쉟헽{enter}'); | ||
| cy.wait('@getEmptyMovies'); | ||
|
|
||
| cy.get('.nothing').should('be.visible'); | ||
| cy.get('.nothing p').contains('검색 결과가 없습니다.'); | ||
| }); | ||
| }); | ||
|
|
||
| context('3. 예외 처리 (API 에러)', () => { | ||
| it('네트워크 통신에 실패할 경우 에러 메시지를 화면에 표시한다.', () => { | ||
| cy.on('uncaught:exception', (err, runnable) => { | ||
| return false; | ||
| }); | ||
|
|
||
| context('API 에러', () => { | ||
| it('에러가 발생 시 에러 컴포넌트를 불러옴', () => { | ||
| cy.intercept('GET', '**/movie/popular?*', { | ||
| statusCode: 500, | ||
| body: MOCK_ERROR, | ||
| }).as('getApiError'); | ||
|
|
||
| cy.visit('http://localhost:5175/'); | ||
| cy.visit('http://localhost:5173/'); | ||
| cy.wait('@getApiError'); | ||
| cy.get('.nothing').should('be.visible'); | ||
| cy.get('.nothing p').should('contain', 'TMDB'); | ||
| cy.get('.nothing p').should('contain', '에러'); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| import { URL } from './constant'; | ||
|
|
||
| export const getOriginalImageUrl = (src: string): string => { | ||
| const PLACEHOLDER = './images/empty.png'; | ||
| export const getOriginalImageUrl = (src: string | null): string => { | ||
| if (!src) return PLACEHOLDER; | ||
| return URL.ORIGINAL_IMAGE + src; | ||
| }; | ||
|
|
||
| export const getThumbnailImageUrl = (src: string): string => { | ||
| if (!src) return PLACEHOLDER; | ||
| return URL.THUMBNAIL_IMAGE + src; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,32 @@ | ||
| export type MovieData = { | ||
| id: number; | ||
| title: string; | ||
| poster_path: string; | ||
| backdrop_path: string; | ||
| vote_average: number; | ||
| }; | ||
|
|
||
| type Genres = { | ||
| name: string; | ||
| }; | ||
| export type MovieDetail = { | ||
| id: number; | ||
| title: string; | ||
| overview: string; | ||
| poster_path: string; | ||
| genres: Genres[]; | ||
| vote_average: string; | ||
| release_date: string; | ||
| }; | ||
|
Comment on lines
+12
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In TMDB’s Movie Details response ( Sources: Citations:
TMDB API 검증 결과에 따르면 현재 코드에서:
두 타입이 불일치합니다. 다음 질문을 고려하며 해결 방법을 생각해 보세요:
🤖 Prompt for AI Agents |
||
|
|
||
| export type ResponseMovie = { | ||
| results: MovieData[]; | ||
| page: number; | ||
| total_pages: number; | ||
| }; | ||
|
|
||
| type Params = { | ||
| page: number; | ||
| page?: number; | ||
| query?: string | undefined; | ||
| language?: string; | ||
| region?: string; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import TMDBError from '../../api/TMDBError'; | ||
|
|
||
| export const ErrorComponent = (error: Error | TMDBError) => { | ||
| const $div = document.createElement('div'); | ||
| const infoMessage = error instanceof TMDBError ? 'TMDB 에러' : '예상치못한 에러'; | ||
| $div.className = 'nothing'; | ||
| $div.innerHTML = ` | ||
| <img src="./images/empty.png" alt="nothing" /> | ||
| <p>${infoMessage}</p> | ||
| `; | ||
|
|
||
| return $div; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { Star } from './Star'; | ||
|
|
||
| export const Rate = (vote_average: string, filled?: boolean) => { | ||
| const $p = document.createElement('p'); | ||
| $p.className = 'rate'; | ||
|
|
||
| const $span = document.createElement('span'); | ||
|
|
||
| const roundVote = Math.round(Number(vote_average) * 10) / 10; | ||
| $span.textContent = roundVote.toFixed(1); | ||
|
|
||
| $p.append(Star(filled), $span); | ||
| return $p; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| export const Star = () => { | ||
| export const Star = (filled?: boolean) => { | ||
| const $img = document.createElement('img'); | ||
| $img.className = 'star'; | ||
| $img.src = './images/star_empty.png'; | ||
| $img.alt = 'star_empty'; | ||
| $img.src = filled ? './images/star_filled.png' : './images/star_empty.png'; | ||
| $img.alt = filled ? 'star_filled' : 'star_empty'; | ||
|
|
||
| return $img; | ||
| }; |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetchMovieDetails API는 mock하지 않고 있는데, 실제 API를 호출하는 건 의도일까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아뇨 전혀 의도가 아니었습니다.
테스트 짜는 과정 중 누락되었습니다.. 해당 부분 수정하겠습니다