const test = require('node:test'); const assert = require('node:assert/strict'); const clear = require('../frontend/web/static/js/port-log-clear.js'); function node() { return { children: [], dataset: {}, attributes: {}, events: {}, value: '', disabled: false, append(...items) { this.children.push(...items); }, appendChild(item) { this.children.push(item); }, setAttribute(key, value) { this.attributes[key] = value; }, addEventListener(name, fn) { this.events[name] = fn; }, removeEventListener(name, fn) { if (this.events[name] === fn) delete this.events[name]; } }; } function setup(overrides = {}) { const parent = node(), select = node(), calls = [], notices = [], errors = [], prompts = []; const state = { answer: 'CH01', disposed: false, busy: 0, settled: 0 }; const options = { kind: 'port', role: 'administrator', document: { createElement: node }, parent, channelField: select, isDisposed: () => state.disposed, confirm: text => { prompts.push(text); return state.answer; }, send: async body => { calls.push(body); return { channel: body.channel, deleted: 3 }; }, onBusy: () => state.busy++, onSettled: async () => state.settled++, onSuccess: (channel, result) => notices.push({ channel, result }), onError: error => errors.push(error), ...overrides }; const control = clear.mount(options); const area = parent.children[0], button = area?.children[1], copy = area?.children[0]; function choose(value) { select.value = value; select.events.change?.(); } return { parent, select, state, control, button, copy, area, choose, calls, notices, errors, prompts }; } test('administrator clear control is absent on protected log pages and for operators', () => { for (const kind of ['link', 'audit']) assert.equal(setup({ kind }).control, null); for (const role of ['port_operator', '', 'viewer']) assert.equal(setup({ role }).control, null); }); test('system clear excludes protected records and requires exact SYSTEM confirmation', async () => { const s=setup({kind:'system',channelField:null}); assert.equal(s.button.disabled,false); assert.equal(s.button.dataset.systemLogClear,'true'); assert.match(s.copy.children[2].textContent,/감사 기록은 클리어 후에도/); for(const answer of [null,'','CH01','system',' SYSTEM ']){s.state.answer=answer;await s.button.events.click();} assert.equal(s.calls.length,0); s.state.answer='SYSTEM';await s.button.events.click(); assert.deepEqual(s.calls,[{confirmation:'SYSTEM'}]); assert.match(s.prompts.at(-1),/감사 로그·포트 로그·포트 상태 이벤트는 보존/); s.control.dispose();assert.equal(s.button.disabled,true); }); test('clear is visible but disabled for all ports or invalid targets; all 48 ports are supported', async () => { const s = setup(); assert.equal(s.button.textContent, '로그 클리어 (삭제)'); assert.equal(s.button.disabled, true); assert.match(s.copy.children[0].textContent, /포트 1개/); for (let i = 1; i <= 48; i++) { s.choose('CH' + String(i).padStart(2, '0')); assert.equal(s.button.disabled, false); } for (const channel of ['', 'ALL', 'CH00', 'CH49', 'ch01', '../system']) { s.choose(channel); assert.equal(s.button.disabled, true); await s.button.events.click(); } assert.equal(s.prompts.length, 0); assert.equal(s.calls.length, 0); }); test('deletion scope is visible before confirmation, distinct from configuration backup', () => { const s = setup(); s.choose('CH18'); assert.match(s.copy.children[0].textContent, /CH18/); assert.match(s.copy.children[1].textContent, /기간·검색 조건·표시 개수와 관계없이/); assert.match(s.copy.children[1].textContent, /새 로그 수집은 계속/); assert.match(s.copy.children[2].textContent, /다른 포트와 시스템·감사·포트 상태 이벤트는 유지/); assert.match(s.copy.children[2].textContent, /설정 백업에는 로그 원문이 포함되지/); assert.equal(s.button.attributes['aria-describedby'], s.copy.children[1].id); }); test('cancel, empty, wrong port, and whitespace confirmations never call the API', async () => { const s = setup(); s.choose('CH01'); for (const answer of [null, '', 'CH02', 'ch01', ' CH01 ']) { s.state.answer = answer; await s.button.events.click(); } assert.equal(s.calls.length, 0); assert.equal(s.state.busy, 0); assert.equal(s.notices.length, 0); }); test('exact confirmation sends only one captured port and explains export limitations', async () => { const s = setup(); s.choose('CH01'); await s.button.events.click(); assert.deepEqual(s.calls, [{ channel: 'CH01', confirmation: 'CH01' }]); assert.match(s.prompts[0], /내보내기에는 현재 검색 조건과 표시 개수 제한/); assert.equal(s.notices[0].result.deleted, 3); assert.equal(s.state.settled, 1); assert.equal(s.button.disabled, false); }); test('duplicate clicks and filter changes cannot retarget an in-flight deletion', async () => { let finish; const sent = []; const s = setup({ send: body => { sent.push(body); return new Promise(resolve => { finish = resolve; }); } }); s.choose('CH01'); const pending = s.button.events.click(); assert.equal(s.button.disabled, true); assert.equal(s.area.attributes['aria-busy'], 'true'); s.choose('CH02'); await s.button.events.click(); assert.deepEqual(sent, [{ channel: 'CH01', confirmation: 'CH01' }]); finish({ channel: 'CH01', deleted: 2 }); await pending; assert.equal(s.notices[0].channel, 'CH01'); assert.equal(s.state.settled, 1); assert.match(s.copy.children[0].textContent, /CH02/); }); test('errors are reported without success and release refresh guards for retry', async () => { const s = setup({ send: async () => { throw new Error('disk unavailable'); } }); s.choose('CH01'); await s.button.events.click(); assert.equal(s.errors[0].message, 'disk unavailable'); assert.equal(s.notices.length, 0); assert.equal(s.button.disabled, false); assert.equal(s.state.settled, 1); }); test('filter reset disables deletion, and leaving the page detaches the control', async () => { const s = setup(); s.choose('CH01'); s.select.value = ''; s.control.update(); assert.equal(s.button.disabled, true); s.choose('CH01'); const oldClick = s.button.events.click; s.control.dispose(); assert.equal(s.button.disabled, true); assert.equal(s.select.events.change, undefined); await oldClick(); assert.equal(s.calls.length, 0); }); test('a response after navigation does not update a new page but releases the guard', async () => { let finish; const s = setup({ send: () => new Promise(resolve => { finish = resolve; }) }); s.choose('CH01'); const pending = s.button.events.click(); s.state.disposed = true; s.control.dispose(); finish({ deleted: 3 }); await pending; assert.equal(s.notices.length, 0); assert.equal(s.state.settled, 1); assert.equal(s.button.disabled, true); });