Skip to content

Commit 2f1536a

Browse files
authored
test: cover config persistence (#42)
1 parent b5c0678 commit 2f1536a

File tree

2 files changed

+191
-22
lines changed

2 files changed

+191
-22
lines changed

src/App.jsx

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useCallback, useEffect } from 'react';
1+
import React, { useState, useCallback, useEffect, useRef } from 'react';
22
import { FileUpload } from './components/FileUpload';
33
import { RegexControls } from './components/RegexControls';
44
import { FileList } from './components/FileList';
@@ -9,27 +9,36 @@ import { FileConfigModal } from './components/FileConfigModal';
99
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
1010
import { mergeFilesWithReplacement } from './utils/mergeFiles.js';
1111

12+
// 默认全局解析配置
13+
export const DEFAULT_GLOBAL_PARSING_CONFIG = {
14+
metrics: [
15+
{
16+
name: 'Loss',
17+
mode: 'keyword',
18+
keyword: 'loss:',
19+
regex: 'loss:\\s*([\\d.eE+-]+)'
20+
},
21+
{
22+
name: 'Grad Norm',
23+
mode: 'keyword',
24+
keyword: 'norm:',
25+
regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)'
26+
}
27+
],
28+
useStepKeyword: false,
29+
stepKeyword: 'step:'
30+
};
31+
1232
function App() {
13-
const [uploadedFiles, setUploadedFiles] = useState([]);
14-
33+
const [uploadedFiles, setUploadedFiles] = useState(() => {
34+
const stored = localStorage.getItem('uploadedFiles');
35+
return stored ? JSON.parse(stored) : [];
36+
});
37+
1538
// 全局解析配置状态
16-
const [globalParsingConfig, setGlobalParsingConfig] = useState({
17-
metrics: [
18-
{
19-
name: 'Loss',
20-
mode: 'keyword', // 'keyword' | 'regex'
21-
keyword: 'loss:',
22-
regex: 'loss:\\s*([\\d.eE+-]+)'
23-
},
24-
{
25-
name: 'Grad Norm',
26-
mode: 'keyword',
27-
keyword: 'norm:',
28-
regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)'
29-
}
30-
],
31-
useStepKeyword: false,
32-
stepKeyword: 'step:'
39+
const [globalParsingConfig, setGlobalParsingConfig] = useState(() => {
40+
const stored = localStorage.getItem('globalParsingConfig');
41+
return stored ? JSON.parse(stored) : JSON.parse(JSON.stringify(DEFAULT_GLOBAL_PARSING_CONFIG));
3342
});
3443

3544
const [compareMode, setCompareMode] = useState('normal');
@@ -42,6 +51,29 @@ function App() {
4251
const [xRange, setXRange] = useState({ min: undefined, max: undefined });
4352
const [maxStep, setMaxStep] = useState(0);
4453
const [sidebarVisible, setSidebarVisible] = useState(true);
54+
const savingDisabledRef = useRef(false);
55+
56+
// 持久化配置到 localStorage
57+
useEffect(() => {
58+
if (savingDisabledRef.current) return;
59+
localStorage.setItem('globalParsingConfig', JSON.stringify(globalParsingConfig));
60+
}, [globalParsingConfig]);
61+
62+
useEffect(() => {
63+
if (savingDisabledRef.current) return;
64+
const serialized = uploadedFiles.map(({ id, name, enabled, content, config }) => ({
65+
id,
66+
name,
67+
enabled,
68+
content,
69+
config
70+
}));
71+
if (serialized.length > 0) {
72+
localStorage.setItem('uploadedFiles', JSON.stringify(serialized));
73+
} else {
74+
localStorage.removeItem('uploadedFiles');
75+
}
76+
}, [uploadedFiles]);
4577

4678
const handleFilesUploaded = useCallback((files) => {
4779
const filesWithDefaults = files.map(file => ({
@@ -132,7 +164,19 @@ function App() {
132164
useStepKeyword: newConfig.useStepKeyword,
133165
stepKeyword: newConfig.stepKeyword
134166
}
135-
})));
167+
})));
168+
}, []);
169+
170+
// 重置配置
171+
const handleResetConfig = useCallback(() => {
172+
savingDisabledRef.current = true;
173+
localStorage.removeItem('globalParsingConfig');
174+
localStorage.removeItem('uploadedFiles');
175+
setGlobalParsingConfig(JSON.parse(JSON.stringify(DEFAULT_GLOBAL_PARSING_CONFIG)));
176+
setUploadedFiles([]);
177+
setTimeout(() => {
178+
savingDisabledRef.current = false;
179+
}, 0);
136180
}, []);
137181

138182
// 全局拖拽事件处理
@@ -306,9 +350,16 @@ function App() {
306350
</svg>
307351
<span>GitHub</span>
308352
</a>
353+
<button
354+
onClick={handleResetConfig}
355+
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
356+
aria-label="重置配置"
357+
>
358+
重置配置
359+
</button>
309360
</div>
310361
</div>
311-
362+
312363
<FileUpload onFilesUploaded={handleFilesUploaded} />
313364

314365
<RegexControls
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React from 'react';
2+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { describe, it, expect, beforeEach, vi } from 'vitest';
5+
import '@testing-library/jest-dom/vitest';
6+
import App from '../../App.jsx';
7+
8+
// Mock chart.js and react-chartjs-2 to avoid canvas requirements
9+
vi.mock('chart.js', () => {
10+
const Chart = {
11+
register: vi.fn(),
12+
defaults: { plugins: { legend: { labels: { generateLabels: vi.fn(() => []) } } } }
13+
};
14+
return {
15+
Chart,
16+
ChartJS: Chart,
17+
CategoryScale: {},
18+
LinearScale: {},
19+
PointElement: {},
20+
LineElement: {},
21+
Title: {},
22+
Tooltip: {},
23+
Legend: {},
24+
};
25+
});
26+
27+
vi.mock('react-chartjs-2', async () => {
28+
const React = await import('react');
29+
return {
30+
Line: React.forwardRef(() => <div data-testid="line-chart" />)
31+
};
32+
});
33+
34+
vi.mock('chartjs-plugin-zoom', () => ({ default: {} }));
35+
36+
function stubFileReader(result) {
37+
class FileReaderMock {
38+
constructor() {
39+
this.onload = null;
40+
}
41+
readAsText() {
42+
this.onload({ target: { result } });
43+
}
44+
}
45+
global.FileReader = FileReaderMock;
46+
}
47+
48+
describe('App configuration persistence', () => {
49+
beforeEach(() => {
50+
localStorage.clear();
51+
});
52+
53+
it('saves and restores config from localStorage', async () => {
54+
stubFileReader('loss: 1');
55+
const user = userEvent.setup();
56+
57+
const { unmount } = render(<App />);
58+
59+
const input = screen.getByLabelText('选择日志文件,支持所有文本格式');
60+
const file = new File(['hello'], 'test.log', { type: 'text/plain' });
61+
await user.upload(input, file);
62+
63+
const stepToggle = screen.getAllByLabelText('使用 Step 关键字')[0];
64+
await user.click(stepToggle);
65+
const stepInput = screen.getByPlaceholderText('step:');
66+
fireEvent.change(stepInput, { target: { value: 'iter:' } });
67+
68+
await waitFor(() => expect(JSON.parse(localStorage.getItem('uploadedFiles'))).toHaveLength(1));
69+
await waitFor(() => {
70+
const cfg = JSON.parse(localStorage.getItem('globalParsingConfig'));
71+
expect(cfg.stepKeyword).toBe('iter:');
72+
expect(cfg.useStepKeyword).toBe(true);
73+
});
74+
75+
unmount();
76+
77+
render(<App />);
78+
79+
expect(await screen.findByText('test.log')).toBeInTheDocument();
80+
const restoredToggle = screen.getAllByLabelText('使用 Step 关键字')[0];
81+
expect(restoredToggle).toBeChecked();
82+
const restoredInput = screen.getByPlaceholderText('step:');
83+
expect(restoredInput.value).toBe('iter:');
84+
});
85+
86+
it('resets config and clears localStorage', async () => {
87+
localStorage.setItem('globalParsingConfig', JSON.stringify({ metrics: [], useStepKeyword: true, stepKeyword: 'foo:' }));
88+
localStorage.setItem('uploadedFiles', JSON.stringify([
89+
{
90+
id: '1',
91+
name: 'saved.log',
92+
enabled: true,
93+
content: 'loss:1',
94+
config: { metrics: [], dataRange: { start: 0, end: undefined, useRange: false }, useStepKeyword: true, stepKeyword: 'foo:' }
95+
}
96+
]));
97+
98+
const user = userEvent.setup();
99+
render(<App />);
100+
101+
expect(screen.getByText('saved.log')).toBeInTheDocument();
102+
expect(screen.getAllByLabelText('使用 Step 关键字')[0]).toBeChecked();
103+
104+
const resetButtons = screen.getAllByRole('button', { name: '重置配置' });
105+
for (const btn of resetButtons) {
106+
await user.click(btn);
107+
}
108+
109+
await waitFor(() => {
110+
expect(localStorage.getItem('uploadedFiles')).toBeNull();
111+
expect(localStorage.getItem('globalParsingConfig')).toBeNull();
112+
expect(screen.queryByText('saved.log')).not.toBeInTheDocument();
113+
});
114+
115+
expect(screen.getAllByLabelText('使用 Step 关键字')[0]).not.toBeChecked();
116+
});
117+
});
118+

0 commit comments

Comments
 (0)