Skip to content

Commit

Permalink
release: 프론트엔드 v1.0.3 (#702)
Browse files Browse the repository at this point in the history
* refactor: E2E 테스트 코드 개선 및 추가 작성 (#701)

* fix: 로그인 E2E 테스트 에러 개선

* refactor: 로그인 로직 E2E 테스트 코드 개선

- 로그인-새로고침-로그아웃 테스트

* refactor: 구글 로그인 커스텀 커맨드 로직 수정

* test(e2e): 좋아요를 누른 음식점은 위시리스트에 저장된다.

* test(e2e): 좋아요를 취소하면 위시리스트에서 제거된다.

* test(e2e): 좋아요를 누른 음식점을 위시리스트에서 좋아요를 해제해도 바로 제거되지 않는다.

* refactor: getProfile 공통로직 훅 적용

* test(e2e): 모바일 환경 로그인 및 로그아웃 테스트 코드 작성

* hotfix: E2E 테스트 build 에러 개선

- 인프라 변경에 따라 코드 수정

* hotfix: E2E 테스트 env 오류 개선

* fix: msw worker가 뒤늦게 동작하는 에러 개선

* fix: 실행 스크립트 수정

- 모드와 환경변수 프리픽스와 일치하게

* infra: e2e 테스트 도중 생성된 비디오 s3에 업로드

* infra: e2e 테스트 도중 생성된 비디오 s3에 업로드

- cypress config 수정

* fix: lint 에러 수정

* infra: E2E 테스트 환경변수 설정

* hotfix: E2E 테스트 yml 코드 수정

* infra: 테스트 실패하더라도 s3 업로드 실행되도록 코드 수정

* infra: 테스트 실패했을 경우에만 s3 업로드 실행되도록 코드 수정

* infra: wait-on 설정

* fix: 환경변수 경로 오류 수정

* infra: timeout 시간 조정

* infra: timeout 시간 조정

* infra: timeout 시간 조정 (1시간)

* test: skip 해제 및 timeout 설정 초기화

* refactor: s3 업로드 로직 수정

* fix: s3 업로드 로직 오탈자 수정

* fix: defaultCommandTimeout 1분으로 조정

* fix: 실패한 경우에만 비디오 업로드
  • Loading branch information
shackstack authored Apr 7, 2024
1 parent c04702a commit 3bc7b4e
Show file tree
Hide file tree
Showing 17 changed files with 150 additions and 98 deletions.
41 changes: 33 additions & 8 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,44 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: 🍔 yarn install
run: yarn install
working-directory: ${{ env.working-directory }}
- name: ✨ Node.js 설정
uses: actions/setup-node@v3
with:
node-version: 18.x

- name: 환경변수 설정
run: |
cd /home/runner/work/2023-celuveat/2023-celuveat/frontend/
echo "BASE_URL=$BASE_URL" >> .dev.env
echo "PUBLIC_URL=$PUBLIC_URL" >> .dev.env
echo "GOOGLE_MAP_API_KEY=$GOOGLE_MAP_API_KEY" >> .dev.env
echo "SHARE_KAKAO_LINK_KEY=$SHARE_KAKAO_LINK_KEY" >> .dev.env
env:
BASE_URL: ${{ secrets.DEV_BASE_URL }}
PUBLIC_URL: ${{ secrets.PUBLIC_URL }}
GOOGLE_MAP_API_KEY: ${{ secrets.GOOGLE_MAP_API_KEY }}
SHARE_KAKAO_LINK_KEY: ${{ secrets.SHARE_KAKAO_LINK_KEY }}

- name: 🍔 E2E 테스트
uses: cypress-io/github-action@v5
uses: cypress-io/github-action@v6
with:
working-directory: ${{ env.working-directory }}
build: yarn build:msw
start: yarn start:msw
start: yarn start:dev
install-command: yarn --frozen-lockfile
parallel: true
wait-on: 'http://localhost:3000'
command: yarn cy:run
browser: chrome

- name: ✨ 테스트에 실패한 경우 생성된 비디오를 S3에서 저장
if: failure()
env:
SOURCE_DIR: '/home/runner/work/2023-celuveat/2023-celuveat/frontend/cypress/videos'
AWS_REGION: 'ap-northeast-2'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_S3_BUCKET_URI: ${{ secrets.AWS_S3_BUCKET_URI }}/e2e
run: |
aws s3 rm $AWS_S3_BUCKET_URI
aws s3 sync $SOURCE_DIR $AWS_S3_BUCKET_URI
4 changes: 2 additions & 2 deletions .github/workflows/frontend-prod-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
BASE_URL: ${{secrets.PROD_BASE_URL}}
GOOGLE_MAP_API_KEY: ${{secrets.GOOGLE_MAP_API_KEY}}
SHARE_KAKAO_LINK_KEY: ${{secrets.SHARE_KAKAO_LINK_KEY}}

- name: ✨ 의존성 설치
run: yarn install

Expand All @@ -48,5 +48,5 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_S3_BUCKET_URI: ${{ secrets.AWS_S3_BUCKET_URI }}
run: |
aws s3 rm $AWS_S3_BUCKET_URI --recursive --exclude 'images-data/*' --exclude 'review/*' --exclude "*.jpeg" --exclude "event/*"
aws s3 rm $AWS_S3_BUCKET_URI --recursive --exclude 'images-data/*' --exclude 'review/*' --exclude "*.jpeg" --exclude "event/*" --exclude "e2e/*"
aws s3 sync $SOURCE_DIR $AWS_S3_BUCKET_URI
2 changes: 1 addition & 1 deletion frontend/.webpack/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module.exports = merge(common, {
},
plugins: [
new Dotenv({
path: path.resolve(__dirname, `../.msw.env`),
path: path.resolve(__dirname, `../.dev.env`),
}),
],
});
4 changes: 2 additions & 2 deletions frontend/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module.exports = defineConfig({
viewportWidth: 1920,
viewportHeight: 1080,
baseUrl: 'http://localhost:3000',
defaultCommandTimeout: 20000,
video: false,
defaultCommandTimeout: 60000,
video: true,
experimentalStudio: true,
experimentalModifyObstructiveThirdPartyCode: true,
experimentalWebKitSupport: true,
Expand Down
45 changes: 32 additions & 13 deletions frontend/cypress/e2e/desktop/like.cy.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
describe('좋아요 관련 기능을 테스트 한다.', () => {
const targetName = '소문난성수감자탕';

beforeEach(() => {
cy.visit('/restaurants/311?celebId=7');
cy.visit('');

cy.loginGoogleForDesktop();
cy.getByAriaLabel(`${targetName} 카드`).first().click();
cy.contains('위시리스트').click();
});

it('좋아요를 누른 음식점은 위시리스트에 저장된다.', () => {
cy.getByAriaLabel('프로필').click();
cy.getByCy('dropdown').contains('위시리스트').click();

cy.get('ul').should('contain.text', targetName);
});

it('성시경 소문난성수감자탕 페이지에서 좋아요를 한 후 위시리스트에 잘 담겨 있는지 확인한다.', () => {
// 로그인이 되지 않은 상태에서 위시리스트 저장하기를 누른다.
// cy.contains('위시리스트에 저장하기').click();
// cy.contains('구글로 로그인하기').click();
// // cy.loginGoogleForDesktop();
// // 위시리스트 버튼을 다시 누른다.
// cy.contains('위시리스트에 저장하기').click();
// cy.get('button[aria-label="로그인"]').click(); // 프로필 아이콘을 누른다.
// cy.get('li[data-name="위시리스트"]').click(); // 위시리스트 버튼을 누른다.
// // 좋아요를 취소한다.
// cy.get('li[aria-label="소문난성수감자탕 카드"]').find('button').click();
// cy.shouldIsLiked('소문난성수감자탕 카드', false);
it('좋아요를 취소하면 위시리스트에서 제거된다.', () => {
cy.contains('위시리스트').click();
cy.getByAriaLabel('프로필').click();
cy.getByCy('dropdown').contains('위시리스트').click();

cy.get('ul').should('not.contain.text', targetName);
});

it('좋아요를 누른 음식점을 위시리스트에서 좋아요를 해제해도 바로 제거되지 않는다. 즉 새로고침을 해야 제거된다.', () => {
cy.getByAriaLabel('프로필').click();
cy.getByCy('dropdown').contains('위시리스트').click();

cy.getByAriaLabel(`${targetName} 카드`).find('[aria-label="좋아요"]').click();
cy.get('ul').should('contain.text', targetName);

cy.reload();

cy.get('ul').should('not.contain.text', targetName);
});
});
29 changes: 22 additions & 7 deletions frontend/cypress/e2e/desktop/login.cy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
describe('로그인 관련 로직을 테스트 한다.', () => {
it('사용자가 구글 로그인을 하면 비회원 상태이던 프로필 이미지가 구글 로그인 프로필 이미지로 변경된다.', () => {
cy.visit('/restaurants/311?celebId=7');
it('로그인 - 새로고침 - 로그아웃 테스트', () => {
cy.visit('');

cy.get('button[aria-label="로그인"]').click(); // 프로필 아이콘을 누른다.
cy.get('li[data-name="로그인"]').click(); // 로그인 버튼을 누른다.
cy.contains('구글로 로그인하기').click(); // 로그인 버튼을 누른다.
cy.getByAriaLabel('프로필').click();
/** 로그인 전에는 마이 페이지 탭이 존재하지 않는다. */
cy.getByCy('dropdown').should('not.contain.text', '마이 페이지');
cy.getByAriaLabel('로그인').click();
cy.contains('구글로 로그인하기').click();

// cy.loginGoogleForDesktop();
/** 로그인에 성공했다면 마이 페이지 탭이 존재한다. */
cy.getByAriaLabel('프로필').click();
cy.getByCy('dropdown').should('contain.text', '마이 페이지');

// cy.get('button[aria-label="로그인"]').find('img').should('have.attr', 'alt', '푸만능 프로필');
/** 새로고침을 하더라도 로그인 상태는 유지된다. */
cy.reload();
cy.getByAriaLabel('프로필').click();
cy.getByCy('dropdown').should('contain.text', '마이 페이지');

/** 로그아웃 이후 프로필 클릭시 로그인 탭만 존재한다. */
cy.getByCy('dropdown').contains('마이 페이지').click();
cy.contains('로그아웃').click();
cy.wait(1000);
cy.getByAriaLabel('프로필').click();
cy.getByCy('dropdown').should('contain.text', '로그인');
cy.getByCy('dropdown').should('not.contain.text', '마이 페이지');
});
});
40 changes: 17 additions & 23 deletions frontend/cypress/e2e/mobile/login.cy.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
describe('로그인 관련 로직을 테스트 한다.', () => {
beforeEach(() => {
it('로그인 - 로그아웃 테스트', () => {
cy.viewport('iphone-5');
cy.visit('');

cy.visit('/restaurants/311?celebId=7', {
onBeforeLoad: (win: any) => {
win.ontouchstart = true;
},
});
/** 로그인에 성공하면 마이페이지에 접근할 수 있다. */
cy.get('nav').find('button').last().click();
cy.get('#root').should('contain.text', '비회원으로 이용하기');
cy.get('#root').should('contain.text', '카카오로 로그인하기');
cy.get('#root').should('contain.text', '구글로 로그인하기');
cy.get('button[type="google"]').click();
cy.wait(5000);

cy.get('nav').find('button').last().click(); // 모바일 nav 하단의 마이 페이지 버튼을 누른다.
cy.get('button[type="google"]').click(); // 구글 로그인 하기 버튼을 누른다.
});
cy.get('nav').find('button').last().click();
cy.get('#root').should('contain.text', '로그아웃');

it('모바일에서 성시경, 소문난성수감자탕 페이지에서 로그인을 한 후 다시 성시경, 소문난성수감자탕 페이지로 돌아 간다.', () => {
// cy.location().should(loc => {
// expect(loc.href).to.eq('http://localhost:3000/restaurants/311?celebId=7');
// });
/** 로그아웃이 되었다면 프로필 버튼 클릭시 로그인 페이지로 이동한다. */
cy.contains('로그아웃').click();
cy.get('nav').find('button').last().click();
cy.get('#root').should('contain.text', '비회원으로 이용하기');
cy.get('#root').should('contain.text', '카카오로 로그인하기');
cy.get('#root').should('contain.text', '구글로 로그인하기');
});

// it('모바일에서 성시경, 소문난성수감자탕 페이지에서 로그인을 하고 회원 탈퇴 시 비회원 상태가 된다.', () => {
// cy.get('nav').find('button').last().click(); // 모바일 nav 하단의 마이 페이지 버튼을 누른다.
// cy.contains('회원탈퇴').click().wait(5000);
// cy.get('button').contains('탈퇴하기').click();

// cy.get('nav').find('button').last().click(); // 모바일 nav 하단의 마이 페이지 버튼을 누른다.

// cy.contains('비회원으로 이용하기');
// });
});
6 changes: 3 additions & 3 deletions frontend/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Cypress.Commands.add('loginGoogle', () => {
});

Cypress.Commands.add('loginGoogleForDesktop', () => {
cy.get('button[type="google"]').click(); // 구글 로그인 하기 버튼을 누른다.

cy.loginGoogle();
cy.getByAriaLabel('프로필').click();
cy.getByAriaLabel('로그인').click();
cy.contains('구글로 로그인하기').click();
});

Cypress.Commands.add('loginGoogleForMobile', () => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MIT",
"sideEffects": false,
"scripts": {
"start:dev": "webpack serve --port 3000 --config .webpack/webpack.dev.js",
"start:dev": "webpack serve --port 3000 --config .webpack/webpack.dev.js --env DEV=true",
"start:prod": "webpack serve --port 3000 --config .webpack/webpack.prod.js",
"build:dev": "webpack --config .webpack/webpack.dev.js",
"build:prod": "webpack --config .webpack/webpack.prod.js",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/@common/InfoButton/InfoButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface InfoButtonProps {

function InfoButton({ isShow = false, isSuccess, profile }: InfoButtonProps) {
return (
<StyledInfoButton isShow={isShow} aria-hidden>
<StyledInfoButton isShow={isShow} aria-label="프로필">
<Menu />
{isSuccess ? <ProfileImage name={profile.nickname} imageUrl={profile.profileImageUrl} size="30px" /> : <User />}
</StyledInfoButton>
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/components/BottomNavBar/BottomNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import { useRef } from 'react';
import { styled } from 'styled-components';
import { useNavigate } from 'react-router-dom';
import { shallow } from 'zustand/shallow';
import { useQuery } from '@tanstack/react-query';
import HomeIcon from '~/assets/icons/home.svg';
import UserIcon from '~/assets/icons/user.svg';
import MapIcon from '~/assets/icons/navmap.svg';
import HeartIcon from '~/assets/icons/navbar-heart.svg';
import useScrollBlock from '~/hooks/useScrollBlock';
import useBottomNavBarState from '~/hooks/store/useBottomNavBarState';
import { ProfileData } from '~/@types/api.types';
import { getProfile } from '~/api/user';
import useNavigateSignUp from '~/hooks/useNavigateSignUp';
import useCheckLogin from '~/hooks/server/useCheckLogin';

interface BottomNavBarProps {
isHide: boolean;
Expand All @@ -32,10 +30,7 @@ function BottomNavBar({ isHide }: BottomNavBarProps) {
shallow,
);

const { isSuccess: isLogin } = useQuery<ProfileData>({
queryKey: ['profile'],
queryFn: () => getProfile(),
});
const { isSuccess: isLogin } = useCheckLogin();

const clickHome = () => {
setHomeSelected();
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/InfoDropDown/InfoDropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function InfoDropDown() {
</StyledTriggerWrapper>
</DropDown.Trigger>
<DropDown.Options as="ul" isCustom>
<StyledDropDownWrapper>
<StyledDropDownWrapper data-cy="dropdown">
{isLogin ? (
<>
<DropDown.Option as="li" isCustom onClick={goMyPage}>
Expand All @@ -54,7 +54,7 @@ function InfoDropDown() {
) : (
<Modal.OpenButton modalTitle="로그인 및 회원가입" isCustom modalContent={<LoginModal />}>
<DropDown.Option isCustom>
<StyledDropDownOption>로그인</StyledDropDownOption>
<StyledDropDownOption aria-label="로그인">로그인</StyledDropDownOption>
</DropDown.Option>
</Modal.OpenButton>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function RestaurantDetailLikeButton({ showText = true, restaurantId, celebId }:
return (
<button type="button" onClick={toggleRestaurantLike}>
<WhiteLove width={30} {...(isLiked && { fill: '#fff' })} />
{showText && <div>위시리스트에서 삭제하기</div>}
{showText && <div>위시리스트</div>}
</button>
);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/context/ReviewModalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface ReviewModalProviderProps {
}

function ReviewModalProvider({ children }: ReviewModalProviderProps) {
const { isLogin } = useCheckLogin();
const { isSuccess: isLogin } = useCheckLogin();

const [formType, setFormType] = useState<ReviewFormType>(null);
const [reviewId, setReviewId] = useState(null);
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/hooks/server/useCheckLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@ import { getProfile } from '~/api/user';

import type { ProfileData } from '~/@types/api.types';

const useCheckLogin = () => {
const { data } = useQuery<ProfileData>({
const useCheckLogin = () =>
useQuery<ProfileData>({
queryKey: ['profile'],
queryFn: getProfile,
});

return {
isLogin: Boolean(data),
};
};

export default useCheckLogin;
45 changes: 27 additions & 18 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,38 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App from '~/App';
import GlobalStyles from './styles/GlobalStyles';
import { worker } from './mocks/browser';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';

import '~/assets/fonts/font.css';

if (process.env.NODE_ENV === 'development') worker.start();
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') {
return;
}

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
const { worker } = await import('./mocks/browser');

worker.start();
}

enableMocking().then(() => {
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
},
});
});

root.render(
<>
<GlobalStyles />
<QueryClientProvider client={queryClient}>
<App />
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools />}
</QueryClientProvider>
</>,
);
root.render(
<>
<GlobalStyles />
<QueryClientProvider client={queryClient}>
<App />
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools />}
</QueryClientProvider>
</>,
);
});
Loading

0 comments on commit 3bc7b4e

Please sign in to comment.