Skip to content

Commit 9ec4560

Browse files
committed
Separazione, aggiunta, fix unit tests frontend
1 parent e218386 commit 9ec4560

File tree

9 files changed

+445
-62
lines changed

9 files changed

+445
-62
lines changed

frontend/src/components/DialogLLM.test.tsx

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { render, screen } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
3-
import React from 'react';
43
import { describe, expect, it, vi, beforeEach } from 'vitest';
54
import DialogLLM from './DialogLLM';
65

@@ -34,10 +33,9 @@ describe('DialogLLM', () => {
3433
expect(screen.getByText('Nessun risultato')).toBeInTheDocument();
3534
});
3635

37-
it('shows insert buttons with selection and triggers handlers', async () => {
36+
it('calls onReplace when Sostituisci Testo button is clicked', async () => {
3837
const user = userEvent.setup();
3938
const onReplace = vi.fn();
40-
const onInsertBelow = vi.fn();
4139

4240
render(
4341
<DialogLLM
@@ -48,14 +46,34 @@ describe('DialogLLM', () => {
4846
hasSelection
4947
onClose={vi.fn()}
5048
onReplace={onReplace}
51-
onInsertBelow={onInsertBelow}
49+
onInsertBelow={vi.fn()}
5250
/>
5351
);
5452

5553
await user.click(screen.getByRole('button', { name: 'Sostituisci Testo' }));
56-
await user.click(screen.getByRole('button', { name: 'Inserisci Sotto' }));
5754

5855
expect(onReplace).toHaveBeenCalledTimes(1);
56+
});
57+
58+
it('calls onInsertBelow when Inserisci Sotto button is clicked', async () => {
59+
const user = userEvent.setup();
60+
const onInsertBelow = vi.fn();
61+
62+
render(
63+
<DialogLLM
64+
text='ok text'
65+
open
66+
loading={false}
67+
actionType='insert'
68+
hasSelection
69+
onClose={vi.fn()}
70+
onReplace={vi.fn()}
71+
onInsertBelow={onInsertBelow}
72+
/>
73+
);
74+
75+
await user.click(screen.getByRole('button', { name: 'Inserisci Sotto' }));
76+
5977
expect(onInsertBelow).toHaveBeenCalledTimes(1);
6078
});
6179

@@ -78,7 +96,7 @@ describe('DialogLLM', () => {
7896
expect(onCreateNewNote).toHaveBeenCalledTimes(1);
7997
});
8098

81-
it('copies valid text with and without onCopySuccess', async () => {
99+
it('calls onCopySuccess when copy succeeds with callback', async () => {
82100
const user = userEvent.setup();
83101
const onCopySuccess = vi.fn();
84102
const writeText = vi.fn().mockResolvedValue(undefined);
@@ -87,20 +105,41 @@ describe('DialogLLM', () => {
87105
value: { writeText },
88106
});
89107

90-
const { rerender } = render(
108+
render(
91109
<DialogLLM text='copy me' open loading={false} onClose={vi.fn()} onCopySuccess={onCopySuccess} />
92110
);
93111

94112
await user.click(screen.getByRole('button', { name: 'Copia' }));
113+
95114
expect(onCopySuccess).toHaveBeenCalledTimes(1);
115+
});
116+
117+
it('copies text to clipboard when onCopySuccess is not provided', async () => {
118+
const user = userEvent.setup();
119+
const writeText = vi.fn().mockResolvedValue(undefined);
120+
Object.defineProperty(navigator, 'clipboard', {
121+
configurable: true,
122+
value: { writeText },
123+
});
124+
125+
render(
126+
<DialogLLM text='copy me too' open loading={false} onClose={vi.fn()} />
127+
);
96128

97-
rerender(<DialogLLM text='copy me too' open loading={false} onClose={vi.fn()} />);
98129
await user.click(screen.getByRole('button', { name: 'Copia' }));
99130

100131
expect(writeText).toHaveBeenCalledWith('copy me too');
101132
});
102133

103-
it('does not copy invalid text and logs copy errors', async () => {
134+
it('does not show copy button for invalid text', () => {
135+
render(
136+
<DialogLLM text="Generazione annullata dall'utente." open loading={false} onClose={vi.fn()} />
137+
);
138+
139+
expect(screen.queryByRole('button', { name: 'Copia' })).not.toBeInTheDocument();
140+
});
141+
142+
it('logs copy errors when clipboard fails', async () => {
104143
const user = userEvent.setup();
105144
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
106145
const writeText = vi.fn().mockRejectedValue(new Error('denied'));
@@ -109,15 +148,13 @@ describe('DialogLLM', () => {
109148
value: { writeText },
110149
});
111150

112-
const { rerender } = render(
113-
<DialogLLM text="Generazione annullata dall'utente." open loading={false} onClose={vi.fn()} />
151+
render(
152+
<DialogLLM text='x' open loading={false} onClose={vi.fn()} />
114153
);
115154

116-
expect(screen.queryByRole('button', { name: 'Copia' })).not.toBeInTheDocument();
117-
118-
rerender(<DialogLLM text='x' open loading={false} onClose={vi.fn()} />);
119155
await user.click(screen.getByRole('button', { name: 'Copia' }));
120156
await Promise.resolve();
157+
121158
expect(errorSpy).toHaveBeenCalledTimes(1);
122159
});
123160
});

frontend/src/components/FileSidebar.test.tsx

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,117 @@ const notes = [
1111
];
1212

1313
describe('FileSidebar', () => {
14-
it('triggers create/import/export/select/delete actions', async () => {
14+
it('calls onCreate when create button is clicked', async () => {
1515
const user = userEvent.setup();
16-
const onSelect = vi.fn();
1716
const onCreate = vi.fn();
18-
const onDelete = vi.fn();
19-
const onRename = vi.fn();
20-
const onImport = vi.fn();
21-
const onExport = vi.fn();
2217

2318
render(
2419
<FileSidebar
2520
notes={notes}
2621
activeId='1'
27-
onSelect={onSelect}
22+
onSelect={vi.fn()}
2823
onCreate={onCreate}
29-
onDelete={onDelete}
30-
onRename={onRename}
31-
onImport={onImport}
32-
onExport={onExport}
24+
onDelete={vi.fn()}
25+
onRename={vi.fn()}
26+
onImport={vi.fn()}
27+
onExport={vi.fn()}
3328
/>
3429
);
3530

3631
await user.click(screen.getByTitle('Nuova nota'));
37-
await user.click(screen.getByTitle('Carica da file'));
38-
await user.click(screen.getByTitle('Salva nota su disco'));
39-
await user.click(screen.getByText('Two'));
40-
await user.click(screen.getAllByTitle('Elimina')[0]);
4132

4233
expect(onCreate).toHaveBeenCalledTimes(1);
34+
});
35+
36+
it('calls onImport when import button is clicked', async () => {
37+
const user = userEvent.setup();
38+
const onImport = vi.fn();
39+
40+
render(
41+
<FileSidebar
42+
notes={notes}
43+
activeId='1'
44+
onSelect={vi.fn()}
45+
onCreate={vi.fn()}
46+
onDelete={vi.fn()}
47+
onRename={vi.fn()}
48+
onImport={onImport}
49+
onExport={vi.fn()}
50+
/>
51+
);
52+
53+
await user.click(screen.getByTitle('Carica da file'));
54+
4355
expect(onImport).toHaveBeenCalledTimes(1);
56+
});
57+
58+
it('calls onExport when export button is clicked', async () => {
59+
const user = userEvent.setup();
60+
const onExport = vi.fn();
61+
62+
render(
63+
<FileSidebar
64+
notes={notes}
65+
activeId='1'
66+
onSelect={vi.fn()}
67+
onCreate={vi.fn()}
68+
onDelete={vi.fn()}
69+
onRename={vi.fn()}
70+
onImport={vi.fn()}
71+
onExport={onExport}
72+
/>
73+
);
74+
75+
await user.click(screen.getByTitle('Salva nota su disco'));
76+
4477
expect(onExport).toHaveBeenCalledWith('1');
78+
});
79+
80+
it('calls onSelect when note is clicked', async () => {
81+
const user = userEvent.setup();
82+
const onSelect = vi.fn();
83+
84+
render(
85+
<FileSidebar
86+
notes={notes}
87+
activeId='1'
88+
onSelect={onSelect}
89+
onCreate={vi.fn()}
90+
onDelete={vi.fn()}
91+
onRename={vi.fn()}
92+
onImport={vi.fn()}
93+
onExport={vi.fn()}
94+
/>
95+
);
96+
97+
await user.click(screen.getByText('Two'));
98+
4599
expect(onSelect).toHaveBeenCalledWith('2');
100+
});
101+
102+
it('calls onDelete when delete button is clicked', async () => {
103+
const user = userEvent.setup();
104+
const onDelete = vi.fn();
105+
106+
render(
107+
<FileSidebar
108+
notes={notes}
109+
activeId='1'
110+
onSelect={vi.fn()}
111+
onCreate={vi.fn()}
112+
onDelete={onDelete}
113+
onRename={vi.fn()}
114+
onImport={vi.fn()}
115+
onExport={vi.fn()}
116+
/>
117+
);
118+
119+
await user.click(screen.getAllByTitle('Elimina')[0]);
120+
46121
expect(onDelete).toHaveBeenCalledTimes(1);
47122
});
48123

49-
it('renames note on enter and cancels on escape', async () => {
124+
it('renames note when enter is pressed', async () => {
50125
const user = userEvent.setup();
51126
const onRename = vi.fn();
52127

@@ -69,11 +144,30 @@ describe('FileSidebar', () => {
69144
await user.type(input, 'Renamed{Enter}');
70145

71146
expect(onRename).toHaveBeenCalledWith('1', 'Renamed');
147+
});
148+
149+
it('cancels rename when escape is pressed', async () => {
150+
const user = userEvent.setup();
151+
const onRename = vi.fn();
152+
153+
render(
154+
<FileSidebar
155+
notes={notes}
156+
activeId='1'
157+
onSelect={vi.fn()}
158+
onCreate={vi.fn()}
159+
onDelete={vi.fn()}
160+
onRename={onRename}
161+
onImport={vi.fn()}
162+
onExport={vi.fn()}
163+
/>
164+
);
72165

73166
await user.dblClick(screen.getByText('Two'));
74-
const input2 = screen.getByDisplayValue('Two');
75-
await user.type(input2, '{Escape}');
76-
expect(onRename).toHaveBeenCalledTimes(1);
167+
const input = screen.getByDisplayValue('Two');
168+
await user.type(input, '{Escape}');
169+
170+
expect(onRename).not.toHaveBeenCalled();
77171
});
78172

79173
it('saves rename on blur only for non-empty title', async () => {

frontend/src/components/MarkdownEditor.test.tsx

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,8 @@ describe('MarkdownEditor', () => {
9999
expect(focus).toHaveBeenCalled();
100100
});
101101

102-
it('handles preview click branches for note/anchor/no-anchor/no-link', () => {
102+
it('handles preview click for note link', () => {
103103
const onNavigate = vi.fn();
104-
const scrollIntoView = vi.fn();
105-
106-
vi.spyOn(document, 'getElementById').mockImplementation((id: string) => {
107-
if (id === 'raw-anchor') return { scrollIntoView } as any;
108-
if (id === 'anchorid') return { scrollIntoView } as any;
109-
return null;
110-
});
111-
112104
const { container } = render(<MarkdownEditor initialValue='x' onNavigate={onNavigate} />);
113105
const wrapper = container.querySelector('.editor-fade-container') as HTMLDivElement;
114106

@@ -117,31 +109,76 @@ describe('MarkdownEditor', () => {
117109
wrapper.appendChild(noteLink);
118110
fireEvent.click(noteLink);
119111

112+
expect(onNavigate).toHaveBeenCalledWith('note-1', 'sec');
113+
});
114+
115+
it('handles preview click for raw anchor link', () => {
116+
const scrollIntoView = vi.fn();
117+
vi.spyOn(document, 'getElementById').mockImplementation((id: string) => {
118+
if (id === 'raw-anchor') return { scrollIntoView } as any;
119+
return null;
120+
});
121+
122+
const { container } = render(<MarkdownEditor initialValue='x' />);
123+
const wrapper = container.querySelector('.editor-fade-container') as HTMLDivElement;
124+
120125
const anchorRaw = document.createElement('a');
121126
anchorRaw.setAttribute('href', '#raw-anchor');
122127
wrapper.appendChild(anchorRaw);
123128
fireEvent.click(anchorRaw);
124129

130+
expect(scrollIntoView).toHaveBeenCalled();
131+
});
132+
133+
it('handles preview click for normalized anchor link', () => {
134+
const scrollIntoView = vi.fn();
135+
vi.spyOn(document, 'getElementById').mockImplementation((id: string) => {
136+
if (id === 'anchorid') return { scrollIntoView } as any;
137+
return null;
138+
});
139+
140+
const { container } = render(<MarkdownEditor initialValue='x' />);
141+
const wrapper = container.querySelector('.editor-fade-container') as HTMLDivElement;
142+
125143
const anchorNormalized = document.createElement('a');
126144
anchorNormalized.setAttribute('href', '#Anchor Id');
127145
wrapper.appendChild(anchorNormalized);
128146
fireEvent.click(anchorNormalized);
129147

148+
expect(scrollIntoView).toHaveBeenCalled();
149+
});
150+
151+
it('handles preview click for missing anchor without crashing', () => {
152+
vi.spyOn(document, 'getElementById').mockReturnValue(null);
153+
154+
const { container } = render(<MarkdownEditor initialValue='x' />);
155+
const wrapper = container.querySelector('.editor-fade-container') as HTMLDivElement;
156+
130157
const missingAnchor = document.createElement('a');
131158
missingAnchor.setAttribute('href', '#not-found');
132159
wrapper.appendChild(missingAnchor);
133-
fireEvent.click(missingAnchor);
160+
161+
expect(() => fireEvent.click(missingAnchor)).not.toThrow();
162+
});
163+
164+
it('handles preview click on link without href', () => {
165+
const { container } = render(<MarkdownEditor initialValue='x' />);
166+
const wrapper = container.querySelector('.editor-fade-container') as HTMLDivElement;
134167

135168
const noHref = document.createElement('a');
136169
wrapper.appendChild(noHref);
137-
fireEvent.click(noHref);
170+
171+
expect(() => fireEvent.click(noHref)).not.toThrow();
172+
});
173+
174+
it('handles preview click on non-link element', () => {
175+
const { container } = render(<MarkdownEditor initialValue='x' />);
176+
const wrapper = container.querySelector('.editor-fade-container') as HTMLDivElement;
138177

139178
const plain = document.createElement('span');
140179
wrapper.appendChild(plain);
141-
fireEvent.click(plain);
142180

143-
expect(onNavigate).toHaveBeenCalledWith('note-1', 'sec');
144-
expect(scrollIntoView).toHaveBeenCalled();
181+
expect(() => fireEvent.click(plain)).not.toThrow();
145182
});
146183

147184
it('does not call onNavigate when not provided', () => {

0 commit comments

Comments
 (0)