From 87d9121ca9315d8dabd5bf98c45776a1a2744b33 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 1 Oct 2026 12:40:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(gift):=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=EB=AC=BC=ED=95=A8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9A=B4=EC=98=81=20=EC=95=A1=EC=85=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/api/gift.js | 39 +++ src/components/SideMenu.vue | 17 + src/router/index.js | 5 + src/utils/gift.js | 23 ++ src/views/Gift/GiftActions.vue | 33 ++ src/views/Gift/GiftList.vue | 562 +++++++++++++++++++++++++++++++++ tests/gift.test.js | 416 ++++++++++++++++++++++++ 8 files changed, 1096 insertions(+) create mode 100644 src/api/gift.js create mode 100644 src/utils/gift.js create mode 100644 src/views/Gift/GiftActions.vue create mode 100644 src/views/Gift/GiftList.vue create mode 100644 tests/gift.test.js diff --git a/package.json b/package.json index f9e8f84..7e95468 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "serve": "vue-cli-service serve --port 8888", "build": "vue-cli-service build", "build_development": "vue-cli-service build --mode development", + "test:gift": "node tests/gift.test.js", "lint": "vue-cli-service lint" }, "dependencies": { diff --git a/src/api/gift.js b/src/api/gift.js new file mode 100644 index 0000000..bfbf4b8 --- /dev/null +++ b/src/api/gift.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; + +const baseUrl = '/api/v2/admin/gifts' + +async function getGifts({ status, applicationNo, nickname, page = 0, size = 20 } = {}) { + const params = { page, size } + if (status) params.status = status + if (applicationNo) params.applicationNo = applicationNo + if (nickname) params.nickname = nickname + const response = await Vue.axios.get(baseUrl, { params }) + return response.data +} + +async function getGift(applicationNo) { + const response = await Vue.axios.get(`${baseUrl}/${encodeURIComponent(applicationNo)}`) + return response.data +} + +async function arriveMailbox(applicationNo) { + const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/arrive-mailbox`) + return response.data +} + +async function completeInspection(applicationNo) { + const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/complete-inspection`) + return response.data +} + +async function completeDelivery(applicationNo) { + const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/complete-delivery`) + return response.data +} + +async function markUndeliverable(applicationNo, reason) { + const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/mark-undeliverable`, { reason }) + return response.data +} + +export { getGifts, getGift, arriveMailbox, completeInspection, completeDelivery, markUndeliverable } diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue index a4a5a27..76c9f74 100644 --- a/src/components/SideMenu.vue +++ b/src/components/SideMenu.vue @@ -104,6 +104,23 @@ export default { // ADMIN 권한 전용 추가 메뉴들 if (role === 'ADMIN') { + const giftMenu = this.items.find(m => m && m.title === '선물함 관리') + if (giftMenu) { + if (!Array.isArray(giftMenu.items)) { + this.$set(giftMenu, 'items', giftMenu.items ? [].concat(giftMenu.items) : []) + } + if (!giftMenu.items.some(item => item && item.route === '/gift/list')) { + giftMenu.items.push({ title: '선물함 리스트', route: '/gift/list', items: null }) + } + } else { + this.items.push({ + title: '선물함 관리', + route: null, + icon: 'mdi-gift', + items: [{ title: '선물함 리스트', route: '/gift/list', items: null }] + }) + } + // '시리즈 관리' 메뉴에 '배너 등록' 하위 메뉴 추가 try { const seriesMenu = this.items.find(m => m && m.title === '시리즈 관리') diff --git a/src/router/index.js b/src/router/index.js index 335422d..94c6e27 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -362,6 +362,11 @@ const routes = [ name: 'CharacterCalculate', component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue') }, + { + path: '/gift/list', + name: 'GiftList', + component: () => import(/* webpackChunkName: "gift" */ '../views/Gift/GiftList.vue') + }, { path: '/original-work', name: 'OriginalList', diff --git a/src/utils/gift.js b/src/utils/gift.js new file mode 100644 index 0000000..1fe0de3 --- /dev/null +++ b/src/utils/gift.js @@ -0,0 +1,23 @@ +export const giftStatuses = [ + { text: '전체', value: '' }, + { text: '접수 완료', value: 'RECEIVED' }, + { text: '발송 확인', value: 'TRACKING_REGISTERED' }, + { text: '사서함 도착', value: 'ARRIVED_AT_MAILBOX' }, + { text: '검수 완료', value: 'INSPECTION_COMPLETED' }, + { text: '전달 완료', value: 'DELIVERED' }, + { text: '전달 불가', value: 'UNDELIVERABLE' }, + { text: '신청 취소', value: 'CANCELED' } +] + +export const giftActions = [ + { value: 'ARRIVE_MAILBOX', label: '사서함 도착 처리', method: 'arriveMailbox' }, + { value: 'COMPLETE_INSPECTION', label: '검수 완료 처리', method: 'completeInspection' }, + { value: 'COMPLETE_DELIVERY', label: '전달 완료 처리', method: 'completeDelivery' }, + { value: 'MARK_UNDELIVERABLE', label: '전달 불가 처리', method: 'markUndeliverable' } +] + +export function validateUndeliverableReason(reason) { + if (typeof reason !== 'string' || !reason.trim()) return '전달 불가 사유를 입력해 주세요.' + if (reason.length > 255) return '전달 불가 사유는 255자 이내로 입력해 주세요.' + return true +} diff --git a/src/views/Gift/GiftActions.vue b/src/views/Gift/GiftActions.vue new file mode 100644 index 0000000..7b9db6f --- /dev/null +++ b/src/views/Gift/GiftActions.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/views/Gift/GiftList.vue b/src/views/Gift/GiftList.vue new file mode 100644 index 0000000..84ec6c8 --- /dev/null +++ b/src/views/Gift/GiftList.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/tests/gift.test.js b/tests/gift.test.js new file mode 100644 index 0000000..3c78700 --- /dev/null +++ b/tests/gift.test.js @@ -0,0 +1,416 @@ +const assert = require('assert').strict +const fs = require('fs') +const path = require('path') +const Module = require('module') +const { transformSync } = require('@babel/core') +const compiler = require('vue-template-compiler') +const Vue = require('vue') + +const root = path.resolve(__dirname, '..') +const originalResolve = Module._resolveFilename +Module._resolveFilename = function(request, parent, ...rest) { + return originalResolve.call(this, request.startsWith('@/') ? path.join(root, 'src', request.slice(2)) : request, parent, ...rest) +} +const originalJs = require.extensions['.js'] +function compileScript(source, filename) { + return transformSync(source, { + filename, + babelrc: false, + configFile: false, + plugins: ['@babel/plugin-transform-modules-commonjs'] + }).code +} +require.extensions['.js'] = (module, filename) => { + if (!filename.startsWith(path.join(root, 'src') + path.sep)) return originalJs(module, filename) + module._compile(compileScript(fs.readFileSync(filename, 'utf8'), filename), filename) +} +require.extensions['.vue'] = (module, filename) => { + const { script, template } = compiler.parseComponent(fs.readFileSync(filename, 'utf8')) + const compiled = compiler.compile(template.content) + assert.deepEqual(compiled.errors, [], `${filename}: 템플릿 컴파일`) + module._compile(compileScript(script.content, filename), filename) + Object.assign(module.exports.default, compiler.compileToFunctions(template.content)) +} + +const api = require('../src/api/gift') +const { giftActions, giftStatuses, validateUndeliverableReason } = require('../src/utils/gift') +const GiftActions = require('../src/views/Gift/GiftActions.vue').default +const GiftList = require('../src/views/Gift/GiftList.vue').default +const SideMenu = require('../src/components/SideMenu.vue').default +const tests = [] +const test = (name, run) => tests.push({ name, run }) +const envelope = data => ({ success: true, data }) +const list = (overrides = {}) => ({ items: [], page: 0, size: 20, totalCount: 0, hasNext: false, ...overrides }) +const detail = (overrides = {}) => ({ + applicationNo: 'G-001', status: 'TRACKING_REGISTERED', statusName: '발송 확인', + availableActions: ['ARRIVE_MAILBOX', 'MARK_UNDELIVERABLE'], + senderInfo: { nickname: '발송인', name: '이름', phoneNumber: '010', address: '주소' }, + recipientInfo: { nickname: '수취인', name: '', phoneNumber: '', address: '' }, + productInfo: { sizeName: '소형', sizeCode: 'S', categoryName: '문구', classificationNumber: 'C1' }, + inboundDeliveryInfo: { courierCompanyName: null, trackingNumber: null }, + ...overrides +}) +function page() { + const errors = [] + const successes = [] + const vm = new Vue({ ...GiftList, created: undefined }) + vm.$dialog = { notify: { error: message => errors.push(message), success: message => successes.push(message) } } + return { vm, errors, successes } +} +function deferred() { + let resolve + const promise = new Promise(done => { resolve = done }) + return { promise, resolve } +} +function nodes(vnode) { + if (!vnode) return [] + return [vnode].concat((vnode.children || (vnode.componentOptions && vnode.componentOptions.children) || []).flatMap(nodes)) +} + +test('API: 기본 페이지와 조합 필터를 query로 전달하고 envelope를 반환한다', async () => { + const calls = [] + const response = envelope(list()) + Vue.axios = { get: async (...args) => { calls.push(args); return { data: response } } } + assert.equal(await api.getGifts(), response) + assert.deepEqual(calls[0], ['/api/v2/admin/gifts', { params: { page: 0, size: 20 } }]) + await api.getGifts({ status: 'ARRIVED_AT_MAILBOX', applicationNo: 'G-01', nickname: '닉 네임', page: 2, size: 10 }) + assert.deepEqual(calls[1][1].params, { status: 'ARRIVED_AT_MAILBOX', applicationNo: 'G-01', nickname: '닉 네임', page: 2, size: 10 }) + await api.getGifts({ status: '', applicationNo: '', nickname: '' }) + assert.deepEqual(calls[2][1].params, { page: 0, size: 20 }) +}) + +test('API: 상세 및 네 가지 POST 경로/본문을 정확하게 전달한다', async () => { + const calls = [] + Vue.axios = { + get: async (...args) => { calls.push(['GET', ...args]); return { data: envelope(detail()) } }, + post: async (...args) => { calls.push(['POST', ...args]); return { data: envelope({ status: 'ARRIVED_AT_MAILBOX' }) } } + } + await api.getGift('G/001') + await api.arriveMailbox('G/001') + await api.completeInspection('G/001') + await api.completeDelivery('G/001') + await api.markUndeliverable('G/001', '파손') + assert.deepEqual(calls, [ + ['GET', '/api/v2/admin/gifts/G%2F001'], + ['POST', '/api/v2/admin/gifts/G%2F001/arrive-mailbox'], + ['POST', '/api/v2/admin/gifts/G%2F001/complete-inspection'], + ['POST', '/api/v2/admin/gifts/G%2F001/complete-delivery'], + ['POST', '/api/v2/admin/gifts/G%2F001/mark-undeliverable', { reason: '파손' }] + ]) +}) + +test('액션: 서버 availableActions에 있는 버튼만 실제 템플릿에서 노출한다', () => { + assert.deepEqual(giftActions.map(action => [action.value, action.label]), [ + ['ARRIVE_MAILBOX', '사서함 도착 처리'], ['COMPLETE_INSPECTION', '검수 완료 처리'], + ['COMPLETE_DELIVERY', '전달 완료 처리'], ['MARK_UNDELIVERABLE', '전달 불가 처리'] + ]) + const scenarios = [[], ['ARRIVE_MAILBOX', 'MARK_UNDELIVERABLE'], ['COMPLETE_INSPECTION', 'MARK_UNDELIVERABLE'], ['COMPLETE_DELIVERY', 'MARK_UNDELIVERABLE'], [], [], []] + for (const actions of scenarios.concat([['UNKNOWN'], ['MARK_UNDELIVERABLE']])) { + const vm = new Vue({ ...GiftActions, propsData: { actions } }) + const buttons = nodes(vm._render()).filter(node => node.tag === 'v-btn') + assert.deepEqual(buttons.map(button => nodes(button).map(node => node.text || '').join('').trim()), giftActions.filter(action => actions.includes(action.value)).map(action => action.label)) + } +}) + +test('필터: 전체와 일곱 가지 상태를 한국어로 제공한다', () => { + assert.deepEqual(giftStatuses.map(status => [status.value, status.text]), [ + ['', '전체'], ['RECEIVED', '접수 완료'], ['TRACKING_REGISTERED', '발송 확인'], + ['ARRIVED_AT_MAILBOX', '사서함 도착'], ['INSPECTION_COMPLETED', '검수 완료'], + ['DELIVERED', '전달 완료'], ['UNDELIVERABLE', '전달 불가'], ['CANCELED', '신청 취소'] + ]) +}) + +test('액션 버튼: 선택 이벤트와 중복 실행 방지 속성을 연결한다', () => { + const vm = new Vue({ ...GiftActions, propsData: { actions: ['ARRIVE_MAILBOX'], disabled: true } }) + const button = nodes(vm._render()).find(node => node.tag === 'v-btn') + assert.equal(button.data.attrs.disabled, true) + let selected + let stopped = false + vm.$on('select', action => { selected = action }) + button.data.on.click({ stopPropagation: () => { stopped = true } }) + assert.equal(selected, 'ARRIVE_MAILBOX') + assert.equal(stopped, true) +}) + +test('전달 불가 사유: 빈 값/공백/256자는 거부하고 255자는 허용한다', () => { + for (const value of ['', ' \n\t ', null, undefined, '가'.repeat(256)]) { + assert.notEqual(validateUndeliverableReason(value), true) + } + assert.equal(validateUndeliverableReason('가'.repeat(255)), true) + assert.equal(validateUndeliverableReason(' 파손 '), true) +}) + +test('목록: 검색은 첫 페이지, 페이지 이동은 적용된 필터와 서버 size를 유지한다', async () => { + const { vm } = page() + const calls = [] + Vue.axios = { get: async (url, config) => { + calls.push(config.params) + return { data: envelope(list({ page: config.params.page, size: 10, totalCount: 25, hasNext: config.params.page < 2 })) } + } } + vm.filters = { status: 'RECEIVED', applicationNo: ' G-01 ', nickname: ' 닉네임 ' } + vm.page = 3 + await vm.search() + assert.deepEqual(calls[0], { status: 'RECEIVED', applicationNo: 'G-01', nickname: '닉네임', page: 0, size: 20 }) + assert.equal(vm.page, 1) + assert.equal(vm.total_page, 3) + vm.filters.nickname = '아직 검색하지 않은 값' + await vm.changePage(2) + assert.deepEqual(calls[1], { status: 'RECEIVED', applicationNo: 'G-01', nickname: '닉네임', page: 1, size: 10 }) + await vm.reset() + assert.deepEqual(calls[2], { page: 0, size: 10 }) + assert.deepEqual(vm.filters, { status: '', applicationNo: '', nickname: '' }) +}) + +test('페이지네이션: 빈 결과 및 마지막 페이지의 hasNext를 반영한다', () => { + const { vm } = page() + assert.equal(vm.total_page, 1) + vm.totalCount = 100 + vm.page = 2 + vm.hasNext = false + assert.equal(vm.total_page, 2) + vm.hasNext = true + assert.equal(vm.total_page, 5) +}) + +test('목록: 열과 빈 값을 표시하고 상세 버튼과 액션 이벤트를 연결한다', async () => { + const { vm } = page() + const item = { + applicationNo: 'G-001', senderNickname: '발송인', recipientNickname: '수취인', + sizeName: '소형', sizeCode: 'S', categoryName: '문구', classificationNumber: 'C1', + courierCompanyName: null, trackingNumber: null, statusName: '발송 확인', availableActions: ['ARRIVE_MAILBOX'] + } + vm.gifts = [item] + const rendered = nodes(vm._render()) + const cells = rendered.filter(node => node.tag === 'td').map(node => nodes(node).map(child => child.text || '').join('').trim()) + assert.deepEqual(cells.slice(0, 9), ['G-001', '발송인', '수취인', '소형 / S', '문구', 'C1', '-', '-', '발송 확인']) + assert.equal(rendered.filter(node => node.tag === 'th').length, 10) + const actions = rendered.find(node => node.componentOptions && node.componentOptions.Ctor.options.name === 'GiftActions') + assert.deepEqual(Array.from(actions.componentOptions.propsData.actions), ['ARRIVE_MAILBOX']) + actions.componentOptions.listeners.select('ARRIVE_MAILBOX') + assert.equal(vm.pendingAction.applicationNo, 'G-001') + vm.cancelAction() + Vue.axios = { get: async url => { + assert.equal(url, '/api/v2/admin/gifts/G-001') + return { data: envelope(detail()) } + } } + const button = rendered.find(node => node.tag === 'v-btn' && nodes(node).some(child => (child.text || '').trim() === '상세')) + await button.data.on.click() + assert.equal(vm.show_detail_dialog, true) + assert.equal(vm.detail.applicationNo, 'G-001') +}) + +test('상세: 수취인 미입력과 배송정보 null을 표시하고 모든 정보를 제공한다', async () => { + const { vm } = page() + Vue.axios = { get: async () => ({ data: envelope(detail()) }) } + await vm.openDetail('G-001') + const text = nodes(vm._render()).map(node => node.text || '').join(' ') + for (const label of ['발송인 정보', '수취인 정보', '상품 정보', '입고 배송 정보', '미입력', '발송 확인', 'G-001', '소형', '문구', 'C1']) assert.ok(text.includes(label), label) + assert.ok(text.includes('-')) +}) + +test('액션: 확인 전 POST하지 않고 취소/미허용 액션은 실행하지 않는다', async () => { + const { vm } = page() + let posts = 0 + Vue.axios = { post: async () => { posts++; return { data: envelope({}) } } } + vm.requestAction(detail(), 'COMPLETE_DELIVERY') + assert.equal(vm.show_confirm_dialog, false) + vm.requestAction(detail(), 'ARRIVE_MAILBOX') + assert.equal(vm.show_confirm_dialog, true) + assert.equal(posts, 0) + vm.cancelAction() + await vm.executeAction() + assert.equal(posts, 0) +}) + +test('전달 불가: 사유 검증 후 확인창을 열고 trim된 사유만 전송한다', async () => { + const { vm } = page() + const posts = [] + Vue.axios = { + post: async (...args) => { posts.push(args); return { data: envelope({ status: 'UNDELIVERABLE' }) } }, + get: async () => ({ data: envelope(list()) }) + } + vm.requestAction(detail(), 'MARK_UNDELIVERABLE') + assert.equal(vm.show_reason_dialog, true) + assert.equal(vm.show_confirm_dialog, false) + for (const reason of ['', ' \n ', '가'.repeat(256)]) { + vm.reason = reason + vm.confirmReason() + await vm.executeAction() + assert.equal(vm.show_confirm_dialog, false) + assert.equal(posts.length, 0) + } + vm.reason = ' 파손 ' + vm.confirmReason() + assert.equal(vm.show_confirm_dialog, true) + assert.equal(posts.length, 0) + await vm.executeAction() + assert.deepEqual(posts, [['/api/v2/admin/gifts/G-001/mark-undeliverable', { reason: '파손' }]]) +}) + +test('전달 불가: 확인 취소 시 사유를 유지하고 입력 모달 취소 시 초기화한다', () => { + const { vm } = page() + vm.requestAction(detail(), 'MARK_UNDELIVERABLE') + vm.reason = '파손' + vm.confirmReason() + vm.cancelConfirmation() + assert.equal(vm.show_confirm_dialog, false) + assert.equal(vm.show_reason_dialog, true) + assert.equal(vm.reason, '파손') + vm.cancelAction() + assert.equal(vm.show_reason_dialog, false) + assert.equal(vm.pendingAction, null) + assert.equal(vm.reason, '') +}) + +test('액션: 각 무본문 액션을 확인 후 실행하며 닫힌 상세는 조회하지 않는다', async () => { + const scenarios = [ + ['ARRIVE_MAILBOX', 'arrive-mailbox'], ['COMPLETE_INSPECTION', 'complete-inspection'], ['COMPLETE_DELIVERY', 'complete-delivery'] + ] + for (const [action, endpoint] of scenarios) { + const { vm, successes } = page() + const calls = [] + Vue.axios = { + post: async (...args) => { calls.push(args); return { data: envelope({}) } }, + get: async (url, config) => { + assert.equal(url, '/api/v2/admin/gifts') + assert.equal(config.params.page, 0) + return { data: envelope(list()) } + } + } + vm.requestAction(detail({ availableActions: [action] }), action) + await vm.executeAction() + assert.deepEqual(calls, [[`/api/v2/admin/gifts/G-001/${endpoint}`]]) + assert.equal(successes.length, 1) + } +}) + +test('성공: 중복 실행을 차단하고 목록과 열린 상세를 재조회한다', async () => { + const { vm, successes } = page() + const pending = deferred() + const calls = [] + Vue.axios = { + post: async (...args) => { calls.push(['POST', ...args]); await pending.promise; return { data: envelope({ status: 'ARRIVED_AT_MAILBOX' }) } }, + get: async (url, config) => { + calls.push(['GET', url, config]) + return { data: envelope(config ? list({ page: 2, totalCount: 60 }) : detail({ status: 'ARRIVED_AT_MAILBOX', availableActions: ['COMPLETE_INSPECTION', 'MARK_UNDELIVERABLE'] })) } + } + } + vm.appliedFilters = { status: 'TRACKING_REGISTERED', applicationNo: '', nickname: '발송인' } + vm.page = 3 + vm.detail = detail() + vm.show_detail_dialog = true + vm.requestAction(vm.detail, 'ARRIVE_MAILBOX') + const operation = vm.executeAction() + await vm.executeAction() + pending.resolve() + await operation + assert.equal(calls.filter(call => call[0] === 'POST').length, 1) + assert.deepEqual(calls.find(call => call[1] === '/api/v2/admin/gifts')[2].params, { status: 'TRACKING_REGISTERED', nickname: '발송인', page: 2, size: 20 }) + assert.ok(calls.some(call => call[1] === '/api/v2/admin/gifts/G-001')) + assert.equal(vm.detail.status, 'ARRIVED_AT_MAILBOX') + assert.equal(successes.length, 1) + assert.equal(vm.is_operating, false) + assert.equal(vm.show_confirm_dialog, false) +}) + +test('실패: envelope 실패/HTTP 오류에 공통 오류 알림을 표시한다', async () => { + const { vm, errors, successes } = page() + Vue.axios = { get: async () => ({ data: { success: false, message: '조회 실패' } }) } + await vm.getGifts() + assert.ok(errors.includes('조회 실패')) + for (const fail of [async () => ({ data: { success: false, message: '상태 변경 실패' } }), async () => { throw { response: { data: { message: '권한 없음' } } } }]) { + Vue.axios.post = fail + vm.requestAction(detail(), 'ARRIVE_MAILBOX') + await vm.executeAction() + assert.equal(vm.is_operating, false) + vm.cancelAction() + } + assert.deepEqual(errors, ['조회 실패', '상태 변경 실패', '권한 없음']) + assert.equal(successes.length, 0) +}) + +test('성공 후 재조회 실패: 오래된 액션을 제거하고 성공/조회 실패를 구분한다', async () => { + const { vm, errors, successes } = page() + vm.detail = detail() + vm.show_detail_dialog = true + Vue.axios = { + post: async () => ({ data: envelope({}) }), + get: async () => { throw new Error('조회 실패') } + } + vm.requestAction(vm.detail, 'ARRIVE_MAILBOX') + await vm.executeAction() + assert.equal(successes.length, 1) + assert.equal(errors.length, 2) + assert.equal(vm.detail, null) + assert.equal(vm.gifts.length, 0) + assert.equal(vm.list_error, true) + assert.equal(vm.is_operating, false) + Vue.axios.get = async () => ({ data: envelope(detail({ availableActions: ['COMPLETE_INSPECTION'] })) }) + await vm.getDetail(vm.detailApplicationNo) + assert.deepEqual(Array.from(vm.detail.availableActions), ['COMPLETE_INSPECTION']) +}) + +test('목록: 늦게 도착한 응답이 최신 페이지를 덮어쓰지 않는다', async () => { + const { vm } = page() + const pending = deferred() + Vue.axios = { get: async (url, config) => config.params.page === 0 ? pending.promise : { data: envelope(list({ page: 1, totalCount: 40 })) } } + const old = vm.getGifts(1) + await vm.getGifts(2) + pending.resolve({ data: envelope(list({ page: 0, totalCount: 1 })) }) + await old + assert.equal(vm.page, 2) + assert.equal(vm.totalCount, 40) + assert.equal(vm.is_loading, false) +}) + +test('상세: 뒤늦은 이전 응답이 새로 선택한 선물을 덮어쓰지 않는다', async () => { + const { vm } = page() + const pending = deferred() + Vue.axios = { get: async url => url.endsWith('/OLD') ? pending.promise : { data: envelope(detail({ applicationNo: 'NEW' })) } } + const old = vm.openDetail('OLD') + await vm.openDetail('NEW') + pending.resolve({ data: envelope(detail({ applicationNo: 'OLD' })) }) + await old + assert.equal(vm.detail.applicationNo, 'NEW') +}) + +test('상세: 조회 중 닫은 모달에 늦은 응답을 반영하지 않는다', async () => { + const { vm } = page() + const pending = deferred() + Vue.axios = { get: async () => pending.promise } + const request = vm.openDetail('G-001') + vm.closeDetail() + pending.resolve({ data: envelope(detail()) }) + await request + assert.equal(vm.show_detail_dialog, false) + assert.equal(vm.detail, null) + assert.equal(vm.is_detail_loading, false) +}) + +test('메뉴: ADMIN에 선물함 리스트를 추가하고 기존 서버 메뉴와 중복되지 않는다', async () => { + for (const role of ['ADMIN', 'CONTENT_MANAGER']) { + for (const existing of [false, true]) { + const menus = [{ title: '콘텐츠 리스트', route: '/content/list' }] + if (existing) menus.push({ title: '선물함 관리', items: [{ title: '선물함 리스트', route: '/gift/list' }] }) + Vue.axios = { get: async () => ({ status: 200, data: envelope(menus) }) } + const vm = new Vue({ ...SideMenu, created: undefined }) + vm.$store = { state: { accountStore: { role } } } + await vm.getMenus() + const gifts = vm.items.filter(item => item.title === '선물함 관리') + assert.equal(gifts.length, role === 'ADMIN' || existing ? 1 : 0) + if (gifts.length) assert.equal(gifts[0].items.filter(item => item.route === '/gift/list').length, 1) + } + } +}) + +;(async () => { + for (const { name, run } of tests) { + try { + await run() + console.log(`PASS ${name}`) + } catch (error) { + process.exitCode = 1 + console.error(`FAIL ${name}`, error) + } + } +})()