Repository navigation
Expand file tree
/
Copy pathtest-export.mjs
More file actions
162 lines (141 loc) · 7.64 KB
/
Copy pathtest-export.mjs
File metadata and controls
162 lines (141 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* CSV / JSON 내보내기.
*
* · 팝오버가 열리고 닫히는가 (✕ · Esc · 바깥 클릭)
* · CSV 가 진짜 CSV 인가 — 헤더가 맞고, 모든 행의 열 수가 같고, 행 수가 버퍼와 맞는가
* · 시각이 역산이 아니라 샘플에 박힌 값인가 (간격이 실제 샘플과 일치)
* · JSON 이 파싱되고 세 범위·누적 합계·단위가 다 들어 있는가
* · '저장'이 실제로 파일을 떨어뜨리는가 (다운로드 디렉터리를 지정해 확인)
*
* node scripts/test-export.mjs
*/
import { mkdtempSync, rmSync, readdirSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
sleep, launch, watchProblems, loadExtension, newTab, setSettings,
serveLoadPage, dom, isolatedWorld, checklist, verdict,
} from './lib/harness.mjs';
const PORT = 8771, DEBUG = 9373;
const COLLECT_MS = 12000;
const dlDir = mkdtempSync(join(tmpdir(), 'ww-dl-'));
const page = await serveLoadPage({ port: PORT, title: '내보내기 테스트', heap: true, up: true });
const { cdp, targets, contexts, close } = await launch({ debugPort: DEBUG });
const problems = watchProblems(cdp);
const checks = checklist();
try {
const { sw } = await loadExtension(cdp, targets);
await setSettings(cdp, sw, { autoStart: true, theme: 'dark', lang: 'ko', defaultRange: 60 });
const { session: pg } = await newTab(cdp);
const D = dom(cdp, pg);
// 다운로드를 임시 폴더로 받게 한다 (헤드리스 기본값은 저장하지 않고 버린다)
await cdp.send('Browser.setDownloadBehavior',
{ behavior: 'allow', downloadPath: dlDir, eventsEnabled: true });
contexts.length = 0;
await cdp.send('Page.navigate', { url: page.url }, pg);
console.log(`샘플을 ${COLLECT_MS / 1000}초간 모으는 중…`);
await sleep(COLLECT_MS);
const world = isolatedWorld(contexts, pg);
if (!world) throw new Error('isolated world 를 찾지 못했습니다');
const inWorld = expr => cdp.eval(expr, { session: pg, contextId: world.id });
/* ---------------- 팝오버 열기 ---------------- */
checks.add('열기 전에는 팝오버가 숨어 있다', (await D.isHidden('exp')) === true);
await D.clickId('export');
await sleep(500);
checks.add('⤓ 로 팝오버가 열린다', (await D.isHidden('exp')) === false);
const popText = await D.textIn(await D.byId('exp'));
checks.add('CSV/JSON 선택지', popText.includes('CSV') && popText.includes('JSON'));
checks.add('저장 버튼', popText.includes('저장'));
checks.add('파일명 표시', /webworks-127\.0\.0\.1.*\.csv/.test(popText), popText.match(/webworks\S+/)?.[0]);
/* ---------------- CSV 내용 ---------------- */
const samples = await inWorld('WW.Metrics.buffers[60].length');
const csv = await inWorld('WW.Export.build("csv", 60).text');
const lines = csv.replace(/^/, '').trim().split('\r\n');
const header = lines[0];
const body = lines.slice(1);
console.log(`\nCSV 미리보기 (${lines.length}줄 중 3줄)`);
console.log(' ' + lines.slice(0, 3).join('\n '));
checks.add('CSV 헤더', header === 'timestamp,cpu_pct,mem_mb,in_kbs,out_kbs', header);
checks.add('CSV 행 수 = 버퍼 샘플 수', body.length === samples, `rows=${body.length}, samples=${samples}`);
checks.add('CSV 모든 행이 5열', body.every(l => l.split(',').length === 5));
checks.add('CSV 앞에 BOM', csv.charCodeAt(0) === 0xFEFF);
checks.add('CSV 첫 열이 ISO 시각',
body.every(l => /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(l.split(',')[0])), body[0]?.split(',')[0]);
checks.add('CSV 시간순(오래된 것 먼저)',
body.length < 2 || new Date(body[0].split(',')[0]) < new Date(body.at(-1).split(',')[0]));
/* 시각이 역산이 아니라 샘플에 박힌 값인지 — 버퍼의 t 와 글자 단위로 같아야 한다 */
const bufTimes = await inWorld('JSON.stringify(WW.Metrics.buffers[60].map(s => new Date(s.t).toISOString()))');
const csvTimes = JSON.stringify(body.map(l => l.split(',')[0]));
checks.add('시각이 샘플에 기록된 값과 일치', bufTimes === csvTimes);
/* ---------------- JSON 내용 ---------------- */
const jsonText = await inWorld('WW.Export.build("json", 60).text');
let j = null;
try { j = JSON.parse(jsonText); } catch (e) { problems.push('JSON 파싱 실패: ' + e.message); }
checks.add('JSON 파싱됨', !!j);
if (j) {
checks.add('JSON: 세 범위 모두', ['60', '300', '3600'].every(r => j.ranges?.[r]?.samples),
Object.keys(j.ranges ?? {}).join(','));
checks.add('JSON: 60 범위 샘플 수 일치', j.ranges['60'].samples.length === samples,
`${j.ranges['60'].samples.length} vs ${samples}`);
checks.add('JSON: 누적 합계', j.totals?.inBytes > 0 && j.totals?.requests > 0,
`in=${j.totals?.inBytes} req=${j.totals?.requests}`);
checks.add('JSON: 단위 명시', !!j.units?.cpu && !!j.units?.out);
checks.add('JSON: 측정 방식 기록', j.mode === 'web-api', j.mode);
checks.add('JSON: 시작 시각', /^\d{4}-\d{2}-\d{2}T/.test(j.startedAt ?? ''), j.startedAt);
checks.add('JSON: 호스트', j.host === `127.0.0.1:${PORT}`, j.host);
}
/* ---------------- 닫기 세 가지 ---------------- */
await D.clickId('exp-close');
await sleep(300);
checks.add('✕ 로 닫힌다', (await D.isHidden('exp')) === true);
await D.clickId('export');
await sleep(300);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }, pg);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }, pg);
await sleep(300);
checks.add('Esc 로 닫힌다', (await D.isHidden('exp')) === true);
await D.clickId('export');
await sleep(300);
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: 60, y: 500, button: 'left', clickCount: 1 }, pg);
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: 60, y: 500, button: 'left', clickCount: 1 }, pg);
await sleep(400);
checks.add('바깥 클릭으로 닫힌다', (await D.isHidden('exp')) === true);
/* ---------------- 실제 저장 ---------------- */
for (const fmt of ['csv', 'json']) {
await D.clickId('export');
await sleep(300);
const btn = await D.find(n => {
const a = n.attributes ?? [];
for (let i = 0; i < a.length; i += 2) if (a[i] === 'data-fmt' && a[i + 1] === fmt) return true;
return false;
});
await D.click(btn);
await sleep(200);
await D.clickId('exp-go');
await sleep(1500);
}
const files = readdirSync(dlDir).filter(f => !f.endsWith('.crdownload'));
console.log(`\n저장된 파일: ${files.join(', ') || '(없음)'}`);
checks.add('CSV 파일이 실제로 저장됨', files.some(f => f.endsWith('.csv')), files.join(','));
checks.add('JSON 파일이 실제로 저장됨', files.some(f => f.endsWith('.json')));
const savedCsv = files.find(f => f.endsWith('.csv'));
if (savedCsv) {
const disk = readFileSync(join(dlDir, savedCsv), 'utf8');
checks.add('저장된 CSV 가 헤더로 시작', disk.replace(/^/, '').startsWith('timestamp,cpu_pct'));
checks.add('저장된 CSV 에 데이터 행 존재', disk.trim().split('\r\n').length > 1);
}
const savedJson = files.find(f => f.endsWith('.json'));
if (savedJson) {
let ok = true;
try { JSON.parse(readFileSync(join(dlDir, savedJson), 'utf8')); } catch { ok = false; }
checks.add('저장된 JSON 이 파싱됨', ok);
}
verdict(checks, problems);
} catch (e) {
console.error('\n실패:', e.message);
process.exitCode = 1;
} finally {
await close();
page.close();
try { rmSync(dlDir, { recursive: true, force: true }); } catch { /* 잠긴 파일은 넘어간다 */ }
}