• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

abdulkader138 / personal-expense-tracker / #84

12 Jan 2026 12:04AM UTC coverage: 98.739% (-1.2%) from 99.925%
#84

push

abdulkader138
code refactoring

12 of 26 new or added lines in 4 files covered. (46.15%)

2 existing lines in 1 file now uncovered.

1331 of 1348 relevant lines covered (98.74%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.71
/src/main/java/com/mycompany/pet/ui/CategoryDialog.java
1
package com.mycompany.pet.ui;
2

3
import java.awt.BorderLayout;
4
import java.awt.FlowLayout;
5

6
import javax.swing.BorderFactory;
7
import javax.swing.JButton;
8
import javax.swing.JDialog;
9
import javax.swing.JFrame;
10
import javax.swing.JLabel;
11
import javax.swing.JOptionPane;
12
import javax.swing.JPanel;
13
import javax.swing.JScrollPane;
14
import javax.swing.JTable;
15
import javax.swing.JTextField;
16
import javax.swing.table.DefaultTableModel;
17

18
import com.mycompany.pet.controller.CategoryController;
19
import com.mycompany.pet.model.Category;
20

21
/**
22
 * Dialog for managing categories.
23
 * 
24
 * This dialog uses CategoryController to separate UI concerns from business logic.
25
 * All database operations are handled asynchronously by the controller.
26
 */
27
public class CategoryDialog extends JDialog {
28
    private static final long serialVersionUID = 1L;
29
    private static final String TEST_MODE_PROPERTY = "test.mode";
30
    
31
    private final transient CategoryController controller;
32
    volatile String lastErrorMessage = null; // Store last error message for test mode (package-private for tests)
1✔
33
    
34
    /**
35
     * Test helper method to get the last error message.
36
     * This is more reliable than checking the label in test mode.
37
     */
38
    String getLastErrorMessage() {
39
        return lastErrorMessage;
1✔
40
    }
41
    
42
    // UI Components (package-private for testing)
43
    JTable categoryTable;
44
    DefaultTableModel categoryTableModel;
45
    JTextField nameField;
46
    JButton addButton;
47
    JButton updateButton;
48
    JButton deleteButton;
49
    JLabel labelMessage; // For displaying messages to user
50
    
51
    /**
52
     * Creates a new CategoryDialog.
53
     * 
54
     * @param parent Parent frame
55
     * @param controller Category controller for business logic
56
     */
57
    public CategoryDialog(JFrame parent, CategoryController controller) {
58
        super(parent, "Manage Categories", true); // Always modal
1✔
59
        this.controller = controller;
1✔
60
        initializeUI();
1✔
61
        // In test mode, don't call loadCategories from constructor to avoid race conditions
62
        // Tests can call it explicitly if needed, or it will be called after user actions
63
        boolean isTestMode = "true".equals(System.getProperty(TEST_MODE_PROPERTY));
1✔
64
        if (!isTestMode) {
1✔
65
            loadCategories();
1✔
66
        }
67
        // In test mode, loadCategories will be called explicitly by tests or after user actions
68
    }
1✔
69
    
70

71
    private void initializeUI() {
72
        setSize(500, 420);
1✔
73
        setLocationRelativeTo(getParent());
1✔
74

75
        JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
1✔
76
        mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
1✔
77

78
        // Top panel for add category
79
        JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1✔
80
        nameField = new JTextField(15);
1✔
81
        addButton = new JButton("Add Category");
1✔
82
        addButton.addActionListener(e -> onAddButtonClick());
1✔
83
        topPanel.add(new JLabel("Name:"));
1✔
84
        topPanel.add(nameField);
1✔
85
        topPanel.add(addButton);
1✔
86
        mainPanel.add(topPanel, BorderLayout.NORTH);
1✔
87

88
        // Center panel for category table
89
        String[] columnNames = {"ID", "Name"};
1✔
90
        categoryTableModel = new DefaultTableModel(columnNames, 0) {
1✔
91
            private static final long serialVersionUID = 1L;
92
            
93
            @Override
94
            public boolean isCellEditable(int row, int column) {
95
                return column == 1; // Only name is editable
1✔
96
            }
97
        };
98
        categoryTable = new JTable(categoryTableModel);
1✔
99
        categoryTable.getColumnModel().getColumn(0).setPreferredWidth(50);
1✔
100
        categoryTable.getColumnModel().getColumn(1).setPreferredWidth(300);
1✔
101
        JScrollPane scrollPane = new JScrollPane(categoryTable);
1✔
102
        mainPanel.add(scrollPane, BorderLayout.CENTER);
1✔
103

104
        // Bottom panel for buttons
105
        JPanel bottomPanel = new JPanel(new FlowLayout());
1✔
106
        updateButton = new JButton("Update Selected");
1✔
107
        updateButton.addActionListener(e -> onUpdateButtonClick());
1✔
108
        bottomPanel.add(updateButton);
1✔
109

110
        deleteButton = new JButton("Delete Selected");
1✔
111
        deleteButton.addActionListener(e -> onDeleteButtonClick());
1✔
112
        bottomPanel.add(deleteButton);
1✔
113

114
        JButton closeButton = new JButton("Close");
1✔
115
        closeButton.addActionListener(e -> {
1✔
116
            setVisible(false);
1✔
117
            dispose();
1✔
118
        });
1✔
119
        bottomPanel.add(closeButton);
1✔
120
        mainPanel.add(bottomPanel, BorderLayout.SOUTH);
1✔
121

122
        // Message area for user feedback
123
        labelMessage = new JLabel("");
1✔
124
        labelMessage.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
1✔
125
        mainPanel.add(labelMessage, BorderLayout.PAGE_END);
1✔
126

127
        add(mainPanel);
1✔
128
    }
1✔
129

130
    /**
131
     * Sets error message for tests and displays it to user.
132
     * Package-private for testing.
133
     */
134
    void setErrorMessage(String msg) {
135
        lastErrorMessage = msg;
1✔
136
        showMessage(msg);
1✔
137
    }
1✔
138

139
    /**
140
     * Handles add button click.
141
     * Delegates to controller for business logic.
142
     * Package-private for testing.
143
     */
144
    void onAddButtonClick() {
145
        String name = nameField.getText().trim();
1✔
146
        if (name.isEmpty()) {
1✔
147
            setErrorMessage("Category name cannot be empty.");
1✔
148
            return;
1✔
149
        }
150
        
151
        controller.createCategory(name,
1✔
152
            category -> {
153
                // Success: clear field and reload
154
                nameField.setText("");
1✔
155
                showMessage("");
1✔
156
                loadCategories();
1✔
157
            },
1✔
158
            error -> {
159
                // Error: set lastErrorMessage and show message
160
                lastErrorMessage = error; // Always set for tests
1✔
161
                showMessage(error);
1✔
162
            }
1✔
163
        );
164
    }
1✔
165

166
    /**
167
     * Handles update button click.
168
     * Delegates to controller for business logic.
169
     * Package-private for testing.
170
     */
171
    void onUpdateButtonClick() {
172
        // Stop any cell editing that might be in progress
173
        if (categoryTable.isEditing()) {
1✔
174
            categoryTable.getCellEditor().stopCellEditing();
1✔
175
        }
176
        
177
        int selectedRow = categoryTable.getSelectedRow();
1✔
178
        if (selectedRow < 0) {
1✔
179
            setErrorMessage("Please select a category to update.");
1✔
180
            return;
1✔
181
        }
182

183
        Integer categoryId = (Integer) categoryTableModel.getValueAt(selectedRow, 0);
1✔
184
        String name = (String) categoryTableModel.getValueAt(selectedRow, 1);
1✔
185
        
186
        if (name == null || name.trim().isEmpty()) {
1✔
187
            setErrorMessage("Category name cannot be empty.");
1✔
188
            return;
1✔
189
        }
190

191
        controller.updateCategory(categoryId, name.trim(),
1✔
192
            category -> {
193
                // Success: reload categories
194
                showMessage("");
1✔
195
                loadCategories();
1✔
196
            },
1✔
197
            error -> {
198
                // Error: set lastErrorMessage and show message
199
                lastErrorMessage = error; // Always set for tests
1✔
200
                showMessage(error);
1✔
201
            }
1✔
202
        );
203
    }
1✔
204

205
    /**
206
     * Handles delete button click.
207
     * Delegates to controller for business logic.
208
     * Package-private for testing.
209
     */
210
    void onDeleteButtonClick() {
211
        int selectedRow = categoryTable.getSelectedRow();
1✔
212
        if (selectedRow < 0) {
1✔
213
            setErrorMessage("Please select a category to delete.");
1✔
214
            return;
1✔
215
        }
216

217
        // In test mode, skip confirmation dialog and proceed directly
218
        boolean isTestMode = "true".equals(System.getProperty(TEST_MODE_PROPERTY));
1✔
219
        int confirm = JOptionPane.YES_OPTION; // Default to YES in test mode
1✔
220
        
221
        if (!isTestMode) {
1✔
222
            confirm = JOptionPane.showConfirmDialog(this,
1✔
223
                "Are you sure you want to delete this category?",
224
                "Confirm Delete",
225
                JOptionPane.YES_NO_OPTION);
226
        }
227

228
        if (confirm == JOptionPane.YES_OPTION) {
1✔
229
            Integer categoryId = (Integer) categoryTableModel.getValueAt(selectedRow, 0);
1✔
230
            controller.deleteCategory(categoryId,
1✔
231
                () -> {
232
                    // Success: reload categories
233
                    showMessage("");
1✔
234
                    loadCategories();
1✔
235
                },
1✔
236
                error -> {
237
                    // Error: set lastErrorMessage and show message
238
                    lastErrorMessage = error; // Always set for tests
1✔
239
                    showMessage(error);
1✔
240
                }
1✔
241
            );
242
        }
243
    }
1✔
244

245
    /**
246
     * Checks if message is an error message.
247
     * Package-private for testing.
248
     */
249
    boolean isErrorMessage(String msg) {
250
        return msg.contains("Error") || msg.contains("select") ||
1✔
251
               msg.contains("cannot be empty") || msg.contains("Please") 
1✔
252
               || msg.contains("Category name");
1✔
253
    }
254

255
    /**
256
     * Updates label text and visibility.
257
     * Package-private for testing.
258
     */
259
    private void updateLabelText(String text) {
260
        if (labelMessage != null) {
1✔
261
            labelMessage.setText(text);
1✔
262
            labelMessage.setVisible(!text.isEmpty());
1✔
263
        }
264
    }
1✔
265

266
    /**
267
     * Sets label text on EDT.
268
     * Package-private for testing.
269
     */
270
    void setLabelTextOnEDT(String text) {
271
        if (javax.swing.SwingUtilities.isEventDispatchThread()) {
1✔
272
            updateLabelText(text);
1✔
273
        } else {
274
            try {
275
                javax.swing.SwingUtilities.invokeAndWait(() -> updateLabelText(text));
1✔
276
            } catch (InterruptedException e) {
1✔
277
                Thread.currentThread().interrupt();
1✔
278
                javax.swing.SwingUtilities.invokeLater(() -> updateLabelText(text));
1✔
UNCOV
279
            } catch (Exception e) {
×
UNCOV
280
                javax.swing.SwingUtilities.invokeLater(() -> updateLabelText(text));
×
281
            }
1✔
282
        }
283
    }
1✔
284

285
    /**
286
     * Shows a message to the user.
287
     * In production, uses JOptionPane. For testing, uses labelMessage.
288
     * 
289
     * @param msg Message to show (empty string clears message)
290
     */
291
    void showMessage(String msg) {
292
        if (labelMessage == null) {
1✔
293
            return;
1✔
294
        }
295
        
296
        final boolean isTestMode = "true".equals(System.getProperty(TEST_MODE_PROPERTY));
1✔
297
        
298
        if (msg != null && !msg.isEmpty()) {
1✔
299
            // CRITICAL: For error messages, ALWAYS set lastErrorMessage FIRST
300
            if (isErrorMessage(msg)) {
1✔
301
                lastErrorMessage = msg;
1✔
302
            }
303
            
304
            setLabelTextOnEDT(msg);
1✔
305
            
306
            // Production mode: show dialog
307
            if (!isTestMode) {
1✔
308
                javax.swing.SwingUtilities.invokeLater(() -> 
1✔
309
                    JOptionPane.showMessageDialog(this, msg, "Info", JOptionPane.WARNING_MESSAGE)
1✔
310
                );
311
            }
312
        } else {
313
            // Empty message: clear label but NEVER clear lastErrorMessage in test mode
314
            setLabelTextOnEDT("");
1✔
315
            if (!isTestMode) {
1✔
316
                lastErrorMessage = null;
1✔
317
            }
318
        }
319
    }
1✔
320
    
321

322
    /**
323
     * Loads categories from the database and populates the table.
324
     * Uses controller for async operation.
325
     */
326
    void loadCategories() {
327
        // CRITICAL: In test mode, NEVER touch labelMessage - preserve ALL messages
328
        String testModeProp = System.getProperty(TEST_MODE_PROPERTY);
1✔
329
        boolean isTestMode = "true".equals(testModeProp);
1✔
330
        
331
        controller.loadCategories(
1✔
332
            categories -> {
333
                categoryTableModel.setRowCount(0);
1✔
334
                for (Category category : categories) {
1✔
335
                    categoryTableModel.addRow(new Object[]{
1✔
336
                        category.getCategoryId(),
1✔
337
                        category.getName()
1✔
338
                    });
339
                }
1✔
340
                // CRITICAL: In test mode, NEVER touch labelMessage - preserve ALL messages
341
                if (isTestMode) {
1✔
342
                    // In test mode: ONLY populate table, NEVER touch labelMessage
343
                    // This prevents any race conditions with showMessage
344
                    return;
1✔
345
                }
346
                
347
                // Production mode: only clear non-error messages
348
                if (labelMessage != null) {
1✔
349
                    String currentMsg = labelMessage.getText();
1✔
350
                    if (currentMsg != null && !currentMsg.isEmpty()) {
1✔
351
                        // Check if it's an error message - if so, preserve it
352
                        if (currentMsg.contains("Error") || currentMsg.contains("select") ||
1✔
353
                            currentMsg.contains("cannot be empty") || currentMsg.contains("Please")) {
1✔
354
                            // It's an error message - preserve it
355
                            return;
1✔
356
                        }
357
                        // Not an error message - safe to clear
358
                        labelMessage.setText("");
1✔
359
                    }
360
                }
361
            },
1✔
362
            error -> {
363
                lastErrorMessage = error; 
1✔
364
                showMessage(error);
1✔
365
            }
1✔
366
        );
367
    }
1✔
368
}
369

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc