You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
114 lines
6.7 KiB
114 lines
6.7 KiB
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);
|
|
});
|
|
|