Skip to content

Commit 0919b06

Browse files
super3claude
andcommitted
Achieve 100% test coverage
- Add tests for error handling edge cases - Cover console.clear fallback scenarios - Test non-FileReadError and non-MdTailError paths - Add tests for process.stdout.rows fallback - Increase total tests from 107 to 111 - All files now at 100% coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fb5c3ec commit 0919b06

10 files changed

Lines changed: 474 additions & 39 deletions

‎TODO.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
- [x] Add proper TypeScript definitions (index.d.ts)
2727

2828
### Improved Error Handling
29-
- [ ] Create custom error classes (FileNotFoundError, InvalidMarkdownError)
30-
- [ ] Implement graceful degradation for missing terminal features
31-
- [ ] Better error messages with suggestions for fixes
29+
- [x] Create custom error classes (FileNotFoundError, InvalidMarkdownError)
30+
- [x] Implement graceful degradation for missing terminal features
31+
- [x] Better error messages with suggestions for fixes
3232

3333
### Enhanced CLI & Display
3434
- [ ] Add chalk library for colored output

‎lib/display.js‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const path = require('path');
2+
const { TerminalError } = require('./errors');
23

34
// ANSI escape codes
45
const ANSI = {
@@ -10,22 +11,60 @@ const ANSI = {
1011
class Display {
1112
constructor() {
1213
this.defaultWidth = 80;
14+
this.supportsAnsi = this.checkAnsiSupport();
15+
}
16+
17+
checkAnsiSupport() {
18+
// Check if terminal supports ANSI escape codes
19+
const term = process.env.TERM;
20+
const isWindows = process.platform === 'win32';
21+
const isDumbTerm = term === 'dumb';
22+
23+
// Most modern terminals support ANSI
24+
return !isDumbTerm && !!(term || isWindows);
1325
}
1426

1527
clearScreen() {
16-
console.clear();
28+
try {
29+
if (console.clear) {
30+
console.clear();
31+
} else {
32+
// Fallback for environments without console.clear
33+
console.log('\n'.repeat(process.stdout.rows || 25));
34+
}
35+
} catch (error) {
36+
// If clearing fails, just continue
37+
console.log('\n');
38+
}
1739
}
1840

1941
hideCursor() {
20-
process.stdout.write(ANSI.HIDE_CURSOR);
42+
if (this.supportsAnsi && process.stdout.isTTY) {
43+
try {
44+
process.stdout.write(ANSI.HIDE_CURSOR);
45+
} catch {
46+
// Gracefully degrade if cursor hiding fails
47+
}
48+
}
2149
}
2250

2351
showCursor() {
24-
process.stdout.write(ANSI.SHOW_CURSOR);
52+
if (this.supportsAnsi && process.stdout.isTTY) {
53+
try {
54+
process.stdout.write(ANSI.SHOW_CURSOR);
55+
} catch {
56+
// Gracefully degrade if cursor showing fails
57+
}
58+
}
2559
}
2660

2761
getTerminalWidth() {
28-
return process.stdout.columns || this.defaultWidth;
62+
try {
63+
return process.stdout.columns || this.defaultWidth;
64+
} catch {
65+
// If we can't get terminal width, use default
66+
return this.defaultWidth;
67+
}
2968
}
3069

3170
renderTabs(files, currentIndex) {

‎lib/errors.js‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Custom error classes for mdtail
3+
*/
4+
5+
class MdTailError extends Error {
6+
constructor(message, suggestion = '') {
7+
super(message);
8+
this.name = this.constructor.name;
9+
this.suggestion = suggestion;
10+
}
11+
12+
toString() {
13+
let result = `${this.name}: ${this.message}`;
14+
if (this.suggestion) {
15+
result += `\n 💡 ${this.suggestion}`;
16+
}
17+
return result;
18+
}
19+
}
20+
21+
class FileNotFoundError extends MdTailError {
22+
constructor(filePath) {
23+
super(
24+
`File not found: ${filePath}`,
25+
'Check the file path and ensure the file exists'
26+
);
27+
this.filePath = filePath;
28+
}
29+
}
30+
31+
class InvalidMarkdownError extends MdTailError {
32+
constructor(filePath) {
33+
super(
34+
`Not a markdown file: ${filePath}`,
35+
'Only .md files are supported. Rename your file with .md extension'
36+
);
37+
this.filePath = filePath;
38+
}
39+
}
40+
41+
class NoFilesFoundError extends MdTailError {
42+
constructor() {
43+
super(
44+
'No markdown files found to watch',
45+
'Try:\n • mdtail README.md - to watch a specific file\n • mdtail *.md - to watch all markdown files\n • Create a TODO.md file in the current directory'
46+
);
47+
}
48+
}
49+
50+
class TerminalError extends MdTailError {
51+
constructor(feature) {
52+
super(
53+
`Terminal feature not available: ${feature}`,
54+
'Try running in a different terminal emulator'
55+
);
56+
this.feature = feature;
57+
}
58+
}
59+
60+
class DirectoryReadError extends MdTailError {
61+
constructor(directory, originalError) {
62+
super(
63+
`Unable to read directory: ${directory}`,
64+
`Check permissions for the directory. Error: ${originalError.message}`
65+
);
66+
this.directory = directory;
67+
this.originalError = originalError;
68+
}
69+
}
70+
71+
class FileReadError extends MdTailError {
72+
constructor(filePath, originalError) {
73+
super(
74+
`Unable to read file: ${filePath}`,
75+
`Check file permissions. Error: ${originalError.message}`
76+
);
77+
this.filePath = filePath;
78+
this.originalError = originalError;
79+
}
80+
}
81+
82+
module.exports = {
83+
MdTailError,
84+
FileNotFoundError,
85+
InvalidMarkdownError,
86+
NoFilesFoundError,
87+
TerminalError,
88+
DirectoryReadError,
89+
FileReadError
90+
};

‎lib/fileManager.js‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
const fs = require('fs').promises;
22
const fsSync = require('fs');
33
const path = require('path');
4+
const {
5+
FileNotFoundError,
6+
InvalidMarkdownError,
7+
NoFilesFoundError,
8+
DirectoryReadError,
9+
FileReadError
10+
} = require('./errors');
411

512
class FileManager {
613
constructor() {
@@ -34,19 +41,19 @@ class FileManager {
3441
.map(f => path.resolve(currentDir, f));
3542
files.push(...mdFiles);
3643
} catch (error) {
37-
console.error(`Warning: Unable to read directory: ${error.message}`);
44+
console.error(new DirectoryReadError(currentDir, error).toString());
3845
}
3946
} else {
4047
// Resolve individual file paths
4148
const filePath = path.resolve(currentDir, arg);
4249
if (!filePath.endsWith('.md')) {
43-
console.error(`Warning: ${arg} is not a markdown file`);
50+
console.error(new InvalidMarkdownError(arg).toString());
4451
} else {
4552
try {
4653
await fs.access(filePath);
4754
files.push(filePath);
4855
} catch {
49-
console.error(`Warning: ${arg} not found`);
56+
console.error(new FileNotFoundError(arg).toString());
5057
}
5158
}
5259
}
@@ -60,13 +67,17 @@ class FileManager {
6067

6168
validateFiles(files) {
6269
if (!Array.isArray(files) || files.length === 0) {
63-
throw new Error('No markdown files found to watch');
70+
throw new NoFilesFoundError();
6471
}
6572
return true;
6673
}
6774

6875
async readFile(filePath) {
69-
return await fs.readFile(filePath, 'utf8');
76+
try {
77+
return await fs.readFile(filePath, 'utf8');
78+
} catch (error) {
79+
throw new FileReadError(filePath, error);
80+
}
7081
}
7182

7283
startWatching(files, onFileChange) {

‎lib/mdtail.js‎

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const path = require('path');
22
const readline = require('readline');
33
const Display = require('./display');
44
const FileManager = require('./fileManager');
5+
const { MdTailError, FileReadError } = require('./errors');
56

67
class MdTail {
78
constructor() {
@@ -59,7 +60,11 @@ class MdTail {
5960

6061
this.display.render(content, filename, this.files, this.currentTabIndex);
6162
} catch (error) {
62-
console.error(`Error reading ${currentFile}:`, error.message);
63+
if (error instanceof FileReadError) {
64+
console.error(error.toString());
65+
} else {
66+
console.error(`Error reading ${currentFile}:`, error.message);
67+
}
6368
}
6469
}
6570

@@ -103,26 +108,31 @@ Examples:
103108
return;
104109
}
105110

106-
readline.emitKeypressEvents(process.stdin);
107-
process.stdin.setRawMode(true);
108-
process.stdin.resume();
109-
110-
process.stdin.on('keypress', async (str, key) => {
111-
if (key && key.ctrl && key.name === 'c') {
112-
await this.cleanup();
113-
process.exit(0);
114-
}
111+
try {
112+
readline.emitKeypressEvents(process.stdin);
113+
process.stdin.setRawMode(true);
114+
process.stdin.resume();
115115

116-
if (this.files.length > 1) {
117-
if (key && key.name === 'left') {
118-
this.navigateTab('left');
119-
await this.displayCurrentFile();
120-
} else if (key && key.name === 'right') {
121-
this.navigateTab('right');
122-
await this.displayCurrentFile();
116+
process.stdin.on('keypress', async (str, key) => {
117+
if (key && key.ctrl && key.name === 'c') {
118+
await this.cleanup();
119+
process.exit(0);
123120
}
124-
}
125-
});
121+
122+
if (this.files.length > 1) {
123+
if (key && key.name === 'left') {
124+
this.navigateTab('left');
125+
await this.displayCurrentFile();
126+
} else if (key && key.name === 'right') {
127+
this.navigateTab('right');
128+
await this.displayCurrentFile();
129+
}
130+
}
131+
});
132+
} catch (error) {
133+
// If keyboard setup fails, app still works without navigation
134+
console.warn('Keyboard navigation unavailable in this environment');
135+
}
126136
}
127137

128138
async startWatching() {
@@ -155,7 +165,11 @@ Examples:
155165

156166
async cleanup() {
157167
if (process.stdin.isTTY) {
158-
process.stdin.setRawMode(false);
168+
try {
169+
process.stdin.setRawMode(false);
170+
} catch {
171+
// Gracefully handle if setRawMode fails
172+
}
159173
}
160174
this.display.showCursor();
161175
console.log('\n\nStopping mdtail...');
@@ -177,7 +191,11 @@ Examples:
177191
try {
178192
this.validateFiles(files);
179193
} catch (error) {
180-
console.error(`Error: ${error.message}`);
194+
if (error instanceof MdTailError) {
195+
console.error(error.toString());
196+
} else {
197+
console.error(`Error: ${error.message}`);
198+
}
181199
console.log('Run "mdtail --help" for usage information');
182200
process.exit(1);
183201
}

0 commit comments

Comments
 (0)