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')) assert.deepEqual(compiler.compile(template.content).errors, []) module._compile(compileScript(script.content, filename), filename) Object.assign(module.exports.default, compiler.compileToFunctions(template.content)) } const GiftMailbox = require('../src/views/Gift/GiftMailbox.vue').default const SideMenu = require('../src/components/SideMenu.vue').default const tests = [] const test = (name, run) => tests.push({ name, run }) const envelope = data => ({ data: { success: true, data } }) const saved = { name: '운영팀', phoneNumber: '010-1234-5678', address: '(01234) 서울시 도로 1, 2층, 운영실' } const form = () => ({ name: ' 운영팀 ', phoneNumber: ' 010-1234-5678 ', zipCode: '01234', address: '서울시 도로 1', addressDetail: '2층, 운영실' }) function page() { const vm = new Vue({ ...GiftMailbox, created: undefined }) const errors = [] const successes = [] vm.$dialog = { notify: { error: message => errors.push(message), success: message => successes.push(message) } } return { vm, errors, successes } } function nodes(vnode) { return vnode ? [vnode].concat((vnode.children || []).flatMap(nodes)) : [] } const text = vm => nodes(vm._render()).map(node => node.text || '').join('') test('초기 GET과 null 빈 폼, 안내 및 필드 표시', async () => { const { vm } = page() const calls = [] Vue.axios = { get: async (...args) => { calls.push(args); return envelope(null) } } await GiftMailbox.created.call(vm) assert.deepEqual(calls, [['/api/v2/admin/gift-mailbox']]) assert.ok(Object.values(vm.form).every(value => value === '')) assert.ok(text(vm).includes('등록된 받을 주소가 없습니다.')) assert.equal(nodes(vm._render()).filter(node => node.tag === 'v-text-field').length, 5) }) test('등록된 주소 조회 및 상세주소 없는 주소 편집', async () => { const { vm } = page() Vue.axios = { get: async () => envelope(saved) } await vm.getMailbox() assert.deepEqual(vm.form, { ...form(), name: saved.name, phoneNumber: saved.phoneNumber }) Vue.axios.get = async () => envelope({ ...saved, address: '(01234) 서울시 도로 1' }) await vm.getMailbox() assert.equal(vm.form.address, '서울시 도로 1') assert.equal(vm.form.addressDetail, '') }) test('필수값 공백 시 필드 오류 표시 및 PUT 차단', async () => { const { vm } = page() vm.loaded = true let calls = 0 Vue.axios = { put: async () => { calls++ } } for (const key of ['name', 'phoneNumber', 'zipCode', 'address']) { for (const value of ['', ' \t\n ', null]) { vm.form = { ...form(), [key]: value } await vm.saveMailbox() assert.deepEqual(Object.keys(vm.form_errors), [key]) const field = nodes(vm._render()).find(node => node.tag === 'v-text-field' && node.key === key) assert.ok(field.data.attrs['error-messages']) } } assert.equal(calls, 0) }) test('생성·수정 PUT 본문, 선택값 null, 성공 알림 및 서버 응답 반영', async () => { for (const detail of ['2층, 운영실', ' ']) { const { vm, successes } = page() vm.loaded = true vm.form = { ...form(), addressDetail: detail, extra: '제외' } const calls = [] const result = { ...saved, name: '서버 이름', phoneNumber: '서버 연락처' } Vue.axios = { put: async (...args) => { calls.push(args); return envelope(result) } } await vm.saveMailbox() await vm.saveMailbox() assert.equal(calls.length, 2) assert.deepEqual(calls[0], ['/api/v2/admin/gift-mailbox', { ...form(), name: saved.name, phoneNumber: saved.phoneNumber, addressDetail: detail.trim() || null }]) assert.equal(vm.form.name, result.name) assert.equal(vm.form.phoneNumber, result.phoneNumber) assert.equal(vm.form.address, form().address) assert.equal(vm.form.addressDetail, detail) assert.equal(successes.length, 2) assert.ok(text(vm).includes(saved.address)) } }) test('조회·저장 실패 알림, 입력 유지, 조회 재시도', async () => { for (const fail of [ async () => ({ data: { success: false, message: '실패' } }), async () => { throw { response: { data: { message: '실패' } } } } ]) { const { vm, errors, successes } = page() let puts = 0 Vue.axios = { get: fail, put: async () => { puts++; return fail() } } await vm.getMailbox() await vm.saveMailbox() assert.equal(puts, 0) assert.equal(vm.load_error, true) assert.ok(text(vm).includes('다시 조회')) Vue.axios.get = async () => envelope(null) await vm.getMailbox() vm.form = form() await vm.saveMailbox() assert.deepEqual(vm.form, form()) assert.deepEqual(errors, ['실패', '실패']) assert.deepEqual(successes, []) assert.equal(vm.is_operating, false) } }) test('조회 중 저장 및 중복 저장 차단', async () => { const { vm } = page() let resolve let puts = 0 Vue.axios = { get: () => new Promise(done => { resolve = done }), put: () => { puts++; return new Promise(done => { resolve = done }) } } const load = vm.getMailbox() await vm.saveMailbox() assert.equal(puts, 0) resolve(envelope(null)) await load vm.form = form() const save = vm.saveMailbox() await vm.saveMailbox() assert.equal(puts, 1) resolve(envelope(saved)) await save }) test('주소 검색 선택: 읽기 전용 필드, 도로명·지번 선택 및 PUT 반영', async () => { const postcode = require('../src/utils/postcode') const original = postcode.loadPostcode try { for (const type of ['R', 'J']) { const { vm } = page() vm.loaded = true vm.form = form() vm.form_errors = { zipCode: '필수', address: '필수' } vm.$refs.postcodeContainer = { innerHTML: '이전 검색' } let options postcode.loadPostcode = async () => function(config) { options = config this.embed = container => assert.equal(container, vm.$refs.postcodeContainer) } const fields = nodes(vm._render()).filter(node => node.tag === 'v-text-field') for (const field of fields) { assert.equal(field.data.attrs.readonly, ['zipCode', 'address'].includes(field.key)) assert.equal(field.data.attrs.disabled, ['zipCode', 'address'].includes(field.key)) } const postcodeRow = nodes(vm._render()).find(node => node.data && node.data.class && node.data.class['mailbox-postcode-row']) assert.ok(postcodeRow, '우편번호와 검색 버튼을 같은 행에 배치한다') assert.ok(nodes(postcodeRow).some(node => node.key === 'zipCode')) assert.ok(nodes(postcodeRow).some(node => node.tag === 'v-btn' && nodes(node).map(child => child.text || '').join('').trim() === '주소 검색')) await vm.openAddressSearch() assert.equal(vm.show_address_search, true) options.oncomplete({ zonecode: '04524', userSelectedType: type, roadAddress: '서울 중구 세종대로 110', jibunAddress: '서울 중구 태평로1가 31' }) assert.equal(vm.form.zipCode, '04524') assert.equal(vm.form.address, type === 'R' ? '서울 중구 세종대로 110' : '서울 중구 태평로1가 31') assert.equal(vm.form.addressDetail, '') assert.equal(vm.form_errors.zipCode, '') assert.equal(vm.form_errors.address, '') assert.equal(vm.show_address_search, false) vm.form.addressDetail = '3층' let request Vue.axios = { put: async (url, body) => { request = body; return envelope(saved) } } await vm.saveMailbox() assert.equal(request.zipCode, '04524') assert.equal(request.address, vm.form.address) assert.equal(request.addressDetail, '3층') } } finally { postcode.loadPostcode = original } }) test('주소 검색 취소·화면 종료·오류 시 기존 입력 보존 및 재시도', async () => { const postcode = require('../src/utils/postcode') const original = postcode.loadPostcode try { const { vm, errors } = page() vm.loaded = true vm.form = form() let resolve let embeds = 0 postcode.loadPostcode = () => new Promise(done => { resolve = done }) const pending = vm.openAddressSearch() vm.show_address_search = false resolve(function() { this.embed = () => { embeds++ } }) await pending assert.equal(embeds, 0) assert.deepEqual(vm.form, form()) postcode.loadPostcode = async () => { throw new Error('검색 로딩 실패') } await vm.openAddressSearch() assert.deepEqual(errors, ['검색 로딩 실패']) assert.equal(vm.is_postcode_loading, false) assert.equal(vm.show_address_search, false) assert.deepEqual(vm.form, form()) postcode.loadPostcode = () => new Promise(done => { resolve = done }) const retry = vm.openAddressSearch() GiftMailbox.beforeDestroy.call(vm) resolve(function() { this.embed = () => { embeds++ } }) await retry assert.equal(embeds, 0) } finally { postcode.loadPostcode = original } }) test('주소 검색 스크립트 단일 로드, 실패 후 재시도 및 기존 SDK 재사용', async () => { const { loadPostcode } = require('../src/utils/postcode') const scripts = [] const previousWindow = global.window const previousDocument = global.document global.window = {} global.document = { createElement: () => ({ remove() { this.removed = true } }), head: { appendChild: script => scripts.push(script) } } try { const first = loadPostcode() assert.equal(loadPostcode(), first) assert.equal(scripts.length, 1) assert.equal(scripts[0].src, 'https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js') scripts[0].onerror() await assert.rejects(first, /주소 검색/) assert.equal(scripts[0].removed, true) const retry = loadPostcode() assert.equal(scripts.length, 2) global.window.daum = { Postcode: function() {} } scripts[1].onload() assert.equal(await retry, global.window.daum.Postcode) assert.equal(await loadPostcode(), global.window.daum.Postcode) assert.equal(scripts.length, 2) } finally { if (previousWindow === undefined) delete global.window else global.window = previousWindow if (previousDocument === undefined) delete global.document else global.document = previousDocument } }) test('관리자 선물함 메뉴 연결 및 중복 방지, 라우트 등록', async () => { for (const items of [[], [{ title: '선물함 관리', items: null }], [{ title: '선물함 관리', items: [{ title: '받을 주소', route: '/gift/mailbox' }] }]]) { Vue.axios = { get: async () => ({ status: 200, ...envelope(items) }) } const vm = new Vue({ ...SideMenu, created: undefined }) vm.$store = { state: { accountStore: { role: 'ADMIN' } } } await vm.getMenus() const children = vm.items.find(item => item.title === '선물함 관리').items assert.equal(children.filter(item => item.route === '/gift/mailbox').length, 1) assert.equal(children.find(item => item.route === '/gift/mailbox').title, '받을 주소') } const router = fs.readFileSync(path.join(root, 'src/router/index.js'), 'utf8') assert.match(router, /path: '\/gift\/mailbox',\s*name: 'GiftMailbox',\s*component: \(\) => import\([^\n]*'\.\.\/views\/Gift\/GiftMailbox.vue'\)/) }) ;(async () => { for (const { name, run } of tests) { try { await run() console.log(`PASS ${name}`) } catch (error) { process.exitCode = 1 console.error(`FAIL ${name}`, error) } } })()