From 596aab54b6a6109af511e4b59847e5a07d6ef696 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 1 Oct 2026 12:58:45 +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=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20CRU?= =?UTF-8?q?D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/api/gift_category.js | 26 +++ src/components/SideMenu.vue | 8 +- src/router/index.js | 5 + src/utils/gift_category.js | 55 +++++ src/views/Gift/GiftCategory.vue | 344 +++++++++++++++++++++++++++ tests/gift_category.test.js | 399 ++++++++++++++++++++++++++++++++ 7 files changed, 837 insertions(+), 2 deletions(-) create mode 100644 src/api/gift_category.js create mode 100644 src/utils/gift_category.js create mode 100644 src/views/Gift/GiftCategory.vue create mode 100644 tests/gift_category.test.js diff --git a/package.json b/package.json index 7e95468..faf9ac0 100644 --- a/package.json +++ b/package.json @@ -7,7 +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", + "test:gift": "node tests/gift.test.js && node tests/gift_category.test.js", "lint": "vue-cli-service lint" }, "dependencies": { diff --git a/src/api/gift_category.js b/src/api/gift_category.js new file mode 100644 index 0000000..a5adf10 --- /dev/null +++ b/src/api/gift_category.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import { buildGiftCategoryRequest } from '@/utils/gift_category' + +const baseUrl = '/api/v2/admin/gift-categories' + +async function getGiftCategories() { + const response = await Vue.axios.get(baseUrl) + return response.data +} + +async function createGiftCategory(form) { + const response = await Vue.axios.post(baseUrl, buildGiftCategoryRequest(form)) + return response.data +} + +async function updateGiftCategory(categoryId, form) { + const response = await Vue.axios.put(`${baseUrl}/${encodeURIComponent(categoryId)}`, buildGiftCategoryRequest(form)) + return response.data +} + +async function deactivateGiftCategory(categoryId) { + const response = await Vue.axios.delete(`${baseUrl}/${encodeURIComponent(categoryId)}`) + return response.data +} + +export { getGiftCategories, createGiftCategory, updateGiftCategory, deactivateGiftCategory } diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue index 76c9f74..fef1891 100644 --- a/src/components/SideMenu.vue +++ b/src/components/SideMenu.vue @@ -112,12 +112,18 @@ export default { if (!giftMenu.items.some(item => item && item.route === '/gift/list')) { giftMenu.items.push({ title: '선물함 리스트', route: '/gift/list', items: null }) } + if (!giftMenu.items.some(item => item && item.route === '/gift/category')) { + giftMenu.items.push({ title: '선물 카테고리', route: '/gift/category', items: null }) + } } else { this.items.push({ title: '선물함 관리', route: null, icon: 'mdi-gift', - items: [{ title: '선물함 리스트', route: '/gift/list', items: null }] + items: [ + { title: '선물함 리스트', route: '/gift/list', items: null }, + { title: '선물 카테고리', route: '/gift/category', items: null } + ] }) } diff --git a/src/router/index.js b/src/router/index.js index 94c6e27..7ad5a6d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -367,6 +367,11 @@ const routes = [ name: 'GiftList', component: () => import(/* webpackChunkName: "gift" */ '../views/Gift/GiftList.vue') }, + { + path: '/gift/category', + name: 'GiftCategory', + component: () => import(/* webpackChunkName: "gift" */ '../views/Gift/GiftCategory.vue') + }, { path: '/original-work', name: 'OriginalList', diff --git a/src/utils/gift_category.js b/src/utils/gift_category.js new file mode 100644 index 0000000..e70bf15 --- /dev/null +++ b/src/utils/gift_category.js @@ -0,0 +1,55 @@ +export function createGiftCategoryForm() { + return { + classificationNumber: '', + categoryCode: '', + name: '', + receiptCode: '', + representativeItem: '', + requiresDamageWaiver: false, + isActive: true + } +} + +export function buildGiftCategoryRequest(form) { + return { + classificationNumber: form.classificationNumber, + categoryCode: form.categoryCode, + name: form.name, + receiptCode: form.receiptCode, + representativeItem: form.representativeItem, + requiresDamageWaiver: form.requiresDamageWaiver, + isActive: form.isActive + } +} + +export function validateGiftCategory(form) { + const errors = {} + const requiredFields = [ + ['classificationNumber', '분류번호', 3], + ['categoryCode', '카테고리 코드', 50], + ['name', '카테고리명', 50], + ['receiptCode', '접수 코드', 9], + ['representativeItem', '대표 품목', 100] + ] + for (const [key, label, maxLength] of requiredFields) { + const value = form[key] + if (typeof value !== 'string' || !value.trim()) { + errors[key] = `${label} 항목은 필수입니다.` + } else if (value.length > maxLength) { + errors[key] = `${label} 항목은 ${maxLength}자 이하로 입력해 주세요.` + } + } + if (!errors.classificationNumber && (!/^[0-9]{3}$/.test(form.classificationNumber) || form.classificationNumber.length !== 3)) { + errors.classificationNumber = '분류번호는 숫자 3자리로 입력해 주세요.' + } + if (!errors.receiptCode && /[^A-Z]/.test(form.receiptCode)) { + errors.receiptCode = '접수 코드는 영문 대문자 1~9자로 입력해 주세요.' + } + if (typeof form.requiresDamageWaiver !== 'boolean') { + errors.requiresDamageWaiver = '파손면책 동의 필요 여부를 선택해 주세요.' + } + if (typeof form.isActive !== 'boolean') { + errors.isActive = '활성 여부를 선택해 주세요.' + } + return errors +} diff --git a/src/views/Gift/GiftCategory.vue b/src/views/Gift/GiftCategory.vue new file mode 100644 index 0000000..acfa2fe --- /dev/null +++ b/src/views/Gift/GiftCategory.vue @@ -0,0 +1,344 @@ + + + diff --git a/tests/gift_category.test.js b/tests/gift_category.test.js new file mode 100644 index 0000000..2f97051 --- /dev/null +++ b/tests/gift_category.test.js @@ -0,0 +1,399 @@ +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_category') +const { createGiftCategoryForm, buildGiftCategoryRequest, validateGiftCategory } = require('../src/utils/gift_category') +const GiftCategory = require('../src/views/Gift/GiftCategory.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 category = (overrides = {}) => ({ + categoryId: 1, classificationNumber: '100', categoryCode: 'DOLL', name: '인형', + receiptCode: 'A', representativeItem: '피규어', requiresDamageWaiver: false, isActive: true, + ...overrides +}) +const request = (overrides = {}) => ({ + classificationNumber: '100', categoryCode: 'DOLL', name: '인형', receiptCode: 'A', + representativeItem: '피규어', requiresDamageWaiver: false, isActive: true, + ...overrides +}) +function page() { + const errors = [] + const successes = [] + const vm = new Vue({ ...GiftCategory, 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)) +} +const text = vnode => nodes(vnode).map(node => node.text || '').join('').trim() +const button = (vm, label) => nodes(vm._render()).find(node => node.tag === 'v-btn' && text(node) === label) + +test('카테고리 API: GET/POST/PUT/DELETE 경로, 본문 및 공통 envelope를 사용한다', async () => { + const calls = [] + const response = envelope(category()) + Vue.axios = Object.fromEntries(['get', 'post', 'put', 'delete'].map(method => [method, async (...args) => { + calls.push([method, ...args]) + return { data: response } + }])) + assert.equal(await api.getGiftCategories(), response) + assert.equal(await api.createGiftCategory(category({ extra: '제외' })), response) + assert.equal(await api.updateGiftCategory(1, category({ extra: '제외' })), response) + assert.equal(await api.deactivateGiftCategory(1), response) + assert.deepEqual(calls, [ + ['get', '/api/v2/admin/gift-categories'], + ['post', '/api/v2/admin/gift-categories', request()], + ['put', '/api/v2/admin/gift-categories/1', request()], + ['delete', '/api/v2/admin/gift-categories/1'] + ]) +}) + +test('카테고리 validation: 필수/형식/길이/boolean 오류를 검사한다', () => { + const invalid = { + classificationNumber: ['', ' ', null, undefined, 100, '10', '1000', 'A10', '123', '100\n', ' 10'], + categoryCode: ['', ' \t ', null, undefined, 1, 'A'.repeat(51)], + name: ['', ' \n ', null, undefined, 1, '가'.repeat(51)], + receiptCode: ['', ' ', null, undefined, 1, 'a', 'Aa', 'A1', '가', 'A B', 'A\n', 'A'.repeat(10)], + representativeItem: ['', ' ', null, undefined, 1, '가'.repeat(101)], + requiresDamageWaiver: [null, undefined, '', 'false', 0, 1], + isActive: [null, undefined, '', 'true', 0, 1] + } + for (const [key, values] of Object.entries(invalid)) { + for (const value of values) { + const errors = validateGiftCategory(request({ [key]: value })) + assert.deepEqual(Object.keys(errors), [key], `${key}: ${String(value)}`) + assert.equal(typeof errors[key], 'string') + } + } +}) + +test('카테고리 validation: 최대 길이/앞자리 0/false를 보존하며 ID는 본문에서 제외한다', () => { + for (const requiresDamageWaiver of [true, false]) { + for (const isActive of [true, false]) { + const form = request({ + classificationNumber: '001', categoryCode: 'A'.repeat(50), name: '가'.repeat(50), + receiptCode: 'ABCDEFGHI', representativeItem: '가'.repeat(100), requiresDamageWaiver, isActive + }) + assert.deepEqual(validateGiftCategory(form), {}) + assert.deepEqual(buildGiftCategoryRequest({ categoryId: 7, extra: '제외', ...form }), form) + } + } + const first = createGiftCategoryForm() + first.name = '변경' + assert.equal(createGiftCategoryForm().name, '') +}) + +test('카테고리 목록: 최초 GET의 response.data 배열과 필수 9개 컬럼을 표시한다', async () => { + const { vm } = page() + const calls = [] + Vue.axios = { get: async (...args) => { calls.push(args); return { data: envelope([category()]) } } } + await GiftCategory.created.call(vm) + assert.deepEqual(calls, [['/api/v2/admin/gift-categories']]) + assert.deepEqual(Array.from(vm.categories), [category()]) + const rendered = nodes(vm._render()) + assert.deepEqual(rendered.filter(node => node.tag === 'th').map(text), [ + '카테고리 ID', '분류번호', '카테고리 코드', '카테고리명', '접수 코드', '대표 품목', '파손면책 동의 필요 여부', '활성 여부', '관리' + ]) + assert.deepEqual(rendered.filter(node => node.tag === 'td').slice(0, 8).map(text), ['1', '100', 'DOLL', '인형', 'A', '피규어', '아니오', '활성']) + button(vm, '수정').data.on.click() + assert.equal(vm.isEdit, true) + vm.closeForm() + button(vm, '카테고리 등록').data.on.click() + assert.equal(vm.isEdit, false) + assert.equal(vm.show_form_dialog, true) +}) + +test('카테고리 목록: 빈 목록, 로딩 및 오류 재조회 상태를 표시한다', async () => { + const { vm, errors } = page() + assert.ok(text(vm._render()).includes('등록된 카테고리가 없습니다.')) + const pending = deferred() + Vue.axios = { get: async () => pending.promise } + const load = vm.getCategories() + assert.equal(vm.is_loading, true) + assert.ok(text(vm._render()).includes('조회 중입니다.')) + assert.equal(button(vm, '카테고리 등록').data.attrs.disabled, true) + pending.resolve({ data: { success: false, message: '조회 실패' } }) + await load + assert.deepEqual(errors, ['조회 실패']) + assert.equal(vm.is_loading, false) + assert.equal(vm.list_error, true) + Vue.axios.get = async () => ({ data: envelope([]) }) + await button(vm, '다시 조회').data.on.click() + assert.equal(vm.list_error, false) +}) + +test('카테고리 폼: 수정 코드는 읽기 전용이며 취소는 목록을 변경하지 않고 등록은 초기화한다', () => { + const { vm } = page() + const item = category() + vm.openEdit(item) + assert.equal(vm.categoryId, 1) + assert.deepEqual(vm.form, request()) + const fields = nodes(vm._render()).filter(node => node.tag === 'v-text-field') + assert.equal(fields.length, 5) + for (const field of fields) assert.equal(field.data.attrs.readonly, field.data.attrs.label === '카테고리 코드') + assert.equal(nodes(vm._render()).filter(node => node.tag === 'v-switch').length, 2) + vm.form.name = '변경' + vm.form.isActive = false + vm.closeForm() + assert.equal(item.name, '인형') + assert.equal(item.isActive, true) + vm.form_errors = { name: '오류' } + vm.openCreate() + assert.equal(vm.categoryId, null) + assert.deepEqual(vm.form, createGiftCategoryForm()) + assert.deepEqual(vm.form_errors, {}) + assert.equal(nodes(vm._render()).find(node => node.tag === 'v-text-field' && node.data.attrs.label === '카테고리 코드').data.attrs.readonly, false) +}) + +test('카테고리 폼: validation 실패 시 등록/수정 API 없이 필드 오류를 표시한다', async () => { + for (const edit of [false, true]) { + const { vm, successes } = page() + const calls = [] + Vue.axios = { post: async () => { calls.push('POST') }, put: async () => { calls.push('PUT') } } + if (edit) vm.openEdit(category()) + else vm.openCreate() + vm.form = request({ classificationNumber: '10', categoryCode: '', name: '', receiptCode: 'a', representativeItem: '', requiresDamageWaiver: null, isActive: null }) + const form = nodes(vm._render()).find(node => node.tag === 'v-form') + await form.data.on.submit({ preventDefault() {} }) + assert.deepEqual(calls, []) + assert.deepEqual(successes, []) + assert.equal(Object.keys(vm.form_errors).length, 7) + for (const field of nodes(vm._render()).filter(node => ['v-text-field', 'v-switch'].includes(node.tag))) { + assert.ok(field.data.attrs['error-messages']) + } + assert.equal(vm.show_form_dialog, true) + assert.equal(vm.is_operating, false) + } +}) + +test('카테고리 저장: POST/PUT 성공 후 재조회하고 모달을 닫으며 중복 제출을 차단한다', async () => { + for (const edit of [false, true]) { + const { vm, successes } = page() + const pending = deferred() + const calls = [] + const payload = request({ classificationNumber: '001', requiresDamageWaiver: true, isActive: false }) + const saved = category(payload) + Vue.axios = { + [edit ? 'put' : 'post']: async (...args) => { calls.push([edit ? 'PUT' : 'POST', ...args]); return { data: envelope(saved) } }, + get: async (...args) => { calls.push(['GET', ...args]); return pending.promise } + } + if (edit) vm.openEdit(category()) + else vm.openCreate() + vm.form = { ...payload, categoryId: 99, extra: '제외' } + const operation = vm.saveCategory() + await vm.saveCategory() + await Vue.nextTick() + assert.equal(vm.is_operating, true) + assert.equal(vm.show_form_dialog, true) + vm.closeForm() + vm.openEdit(category({ categoryId: 2 })) + vm.openCreate() + assert.equal(vm.categoryId, edit ? 1 : null) + assert.equal(vm.show_form_dialog, true) + pending.resolve({ data: envelope([saved]) }) + await operation + assert.deepEqual(calls, [ + [edit ? 'PUT' : 'POST', `/api/v2/admin/gift-categories${edit ? '/1' : ''}`, payload], + ['GET', '/api/v2/admin/gift-categories'] + ]) + assert.deepEqual(Array.from(vm.categories), [saved]) + assert.equal(vm.show_form_dialog, false) + assert.equal(vm.is_operating, false) + assert.deepEqual(successes, [edit ? '카테고리가 수정되었습니다.' : '카테고리가 등록되었습니다.']) + await vm.saveCategory() + assert.equal(calls.length, 2) + } +}) + +test('카테고리 저장 실패: 공통 오류 알림과 입력/모달을 유지한다', async () => { + for (const edit of [false, true]) { + for (const fail of [ + async () => ({ data: { success: false, message: '저장 실패' } }), + async () => { throw { response: { data: { message: '저장 실패' } } } }, + async () => { throw new Error('저장 실패') } + ]) { + const { vm, errors, successes } = page() + let loads = 0 + Vue.axios = { post: fail, put: fail, get: async () => { loads++ } } + if (edit) vm.openEdit(category()) + else vm.openCreate() + vm.form = request() + await vm.saveCategory() + assert.deepEqual(errors, ['저장 실패']) + assert.deepEqual(successes, []) + assert.deepEqual(vm.form, request()) + assert.equal(vm.show_form_dialog, true) + assert.equal(vm.is_operating, false) + assert.equal(loads, 0) + } + } +}) + +test('카테고리 저장 후 조회 실패: 저장 성공과 조회 오류를 구분하며 중복 등록을 방지한다', async () => { + const { vm, errors, successes } = page() + Vue.axios = { + post: async () => ({ data: envelope(category()) }), + get: async () => { throw new Error('조회 실패') } + } + vm.openCreate() + vm.form = request() + await vm.saveCategory() + assert.deepEqual(successes, ['카테고리가 등록되었습니다.']) + assert.deepEqual(errors, ['조회 실패']) + assert.equal(vm.show_form_dialog, false) + assert.equal(vm.list_error, true) + assert.equal(vm.is_operating, false) +}) + +test('카테고리 비활성화: 확인 전/취소 후/이미 비활성이면 DELETE하지 않는다', async () => { + const { vm } = page() + let calls = 0 + Vue.axios = { delete: async () => { calls++ } } + vm.categories = [category()] + button(vm, '비활성화').data.on.click() + assert.equal(vm.show_confirm_dialog, true) + assert.ok(text(vm._render()).includes('인형 카테고리를 비활성화하시겠습니까?')) + assert.equal(calls, 0) + vm.cancelDeactivation() + await vm.deactivateCategory() + assert.equal(calls, 0) + assert.equal(vm.pendingCategory, null) + vm.categories = [category({ isActive: false, requiresDamageWaiver: true })] + assert.equal(button(vm, '비활성화').data.attrs.disabled, true) + assert.ok(text(vm._render()).includes('비활성')) + assert.ok(text(vm._render()).includes('예')) + vm.requestDeactivation(vm.categories[0]) + await vm.deactivateCategory() + assert.equal(calls, 0) + assert.equal(vm.show_confirm_dialog, false) +}) + +test('카테고리 비활성화 성공: 응답의 isActive=false를 반영하고 row는 유지한다', async () => { + const { vm, successes } = page() + const pending = deferred() + const calls = [] + Vue.axios = { delete: async (...args) => { calls.push(args); return pending.promise } } + vm.categories = [category(), category({ categoryId: 2 })] + vm.requestDeactivation(vm.categories[0]) + const operation = button(vm, '확인').data.on.click() + await vm.deactivateCategory() + vm.cancelDeactivation() + assert.equal(vm.show_confirm_dialog, true) + assert.equal(vm.is_operating, true) + assert.equal(button(vm, '비활성화').data.attrs.disabled, true) + pending.resolve({ data: envelope(category({ isActive: false })) }) + await operation + assert.deepEqual(calls, [['/api/v2/admin/gift-categories/1']]) + assert.deepEqual(Array.from(vm.categories), [category({ isActive: false }), category({ categoryId: 2 })]) + assert.equal(button(vm, '비활성화').data.attrs.disabled, true) + assert.equal(vm.show_confirm_dialog, false) + assert.equal(vm.pendingCategory, null) + assert.equal(vm.is_operating, false) + assert.deepEqual(successes, ['카테고리가 비활성화되었습니다.']) +}) + +test('카테고리 비활성화 실패: 기존 활성 상태를 유지하고 공통 오류를 표시한다', async () => { + for (const fail of [ + async () => ({ data: { success: false, message: '비활성화 실패' } }), + async () => { throw { response: { data: { message: '비활성화 실패' } } } } + ]) { + const { vm, errors, successes } = page() + Vue.axios = { delete: fail } + vm.categories = [category()] + vm.requestDeactivation(vm.categories[0]) + await vm.deactivateCategory() + assert.equal(vm.categories[0].isActive, true) + assert.equal(vm.show_confirm_dialog, true) + assert.equal(vm.is_operating, false) + assert.deepEqual(errors, ['비활성화 실패']) + assert.deepEqual(successes, []) + } +}) + +test('카테고리 목록: 늦게 도착한 이전 응답은 최신 목록을 덮어쓰지 않는다', async () => { + const { vm } = page() + const pending = deferred() + Vue.axios = { get: async () => pending.promise } + const old = vm.getCategories() + Vue.axios.get = async () => ({ data: envelope([category({ categoryId: 2 })]) }) + await vm.getCategories() + pending.resolve({ data: envelope([category()]) }) + await old + assert.deepEqual(Array.from(vm.categories), [category({ categoryId: 2 })]) + assert.equal(vm.is_loading, false) +}) + +test('카테고리 메뉴: ADMIN 보완 메뉴와 서버 메뉴를 중복 없이 유지한다', async () => { + for (const role of ['ADMIN', 'CONTENT_MANAGER']) { + for (const existing of ['none', 'null', 'list', 'category', 'object']) { + const menus = [] + if (existing !== 'none') { + const items = existing === 'null' ? null : existing === 'list' + ? [{ title: '선물함 리스트', route: '/gift/list' }] + : existing === 'object' ? { title: '선물 카테고리', route: '/gift/category' } + : [{ title: '선물 카테고리', route: '/gift/category' }] + menus.push({ title: '선물함 관리', items }) + } + 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 !== 'none' ? 1 : 0) + const children = gifts.length && gifts[0].items ? [].concat(gifts[0].items) : [] + assert.equal(children.filter(item => item.route === '/gift/category').length, role === 'ADMIN' || ['category', 'object'].includes(existing) ? 1 : 0) + if (role === 'ADMIN') assert.equal(children.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) + } + } +})()