Skip to content

Commit 69763d7

Browse files
Copilotpanz3r
andauthored
fix: extract base command, improve type safety, add additional errors handling, and improve tests coverage (#281)
* Initial plan * Initial commit - planning code review improvements Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> * Task 1: Extract base command class to eliminate code duplication Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> * Task 2: Add type definitions for icon sizes Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> * Task 3: Improve error collection and reporting in commands Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> * Task 4: Add tests for malformed/corrupt image inputs Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> * Task 5: Add unit tests for file-utils and color.utils Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> * Fix linting issues and update CODE-REVIEW-FINDINGS.md to mark all items complete Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> * Remove package-lock.json and add it to .gitignore (pnpm project) Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com>
1 parent a2d3029 commit 69763d7

11 files changed

Lines changed: 336 additions & 57 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,6 @@ oclif.manifest.json
234234

235235
# TypeScript build info
236236
*.tsbuildinfo
237+
238+
# npm lock file (this is a pnpm project)
239+
package-lock.json

docs/001-CODE-REVIEW-FINDINGS.md

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
# Code Review Findings
22

3-
**Status:** Partially Completed
3+
**Status:** Completed
44
**Created:** 2026-01-05
55
**Updated:** 2026-01-06
66
**Target:** Improvement recommendations from a code review, prioritized for the "Test Developer" agent.
77

88
## Completion Summary
99

10-
**Completed Items (6/11):**
10+
**Completed Items (11/11):**
1111
- ✅ Fix typo in error messages (icons.ts, splash.ts)
1212
- ✅ Add missing license header to constants.ts
1313
- ✅ Standardize path prefixes in splash.ts
1414
- ✅ Improve extractAppName error handling
1515
- ✅ Add verbose flag to dotenv command
1616
- ✅ Add tests for verbose output (icons, splash, dotenv)
1717
- ✅ Add unit tests for extractAppName utility
18+
- ✅ Add tests for malformed input images
19+
- ✅ Extract base command class
20+
- ✅ Add type definitions for icon sizes
21+
- ✅ Improve error collection in commands
22+
- ✅ Add tests for utility files (file-utils, color.utils)
1823

19-
**Pending Items (5/11):**
20-
- ⏳ Add tests for malformed input images
21-
- ⏳ Extract base command class
22-
- ⏳ Add type definitions for icon sizes
23-
- ⏳ Improve error collection in commands
24-
- ⏳ Add tests for utility files (file-utils, color.utils)
24+
**Pending Items (0/11):**
25+
- None - all items completed!
2526

2627
---
2728

@@ -48,23 +49,29 @@ it('runs icons with verbose flag and shows detailed output', async () => {
4849
})
4950
```
5051

51-
### 2. Add Tests for Malformed Input Images ⏳ PENDING
52+
### 2. Add Tests for Malformed Input Images ✅ COMPLETED
5253

53-
**Status:** ⏳ Not yet implemented
54+
**Status:** ✅ Completed in this PR
5455

55-
No tests verify behavior with corrupt or wrong-format files.
56+
Tests now verify behavior with corrupt or wrong-format files.
5657

57-
**Test cases to add:**
58+
**Test cases added:**
5859
```typescript
5960
it('handles corrupt image file gracefully', async () => {
60-
fs.writeFileSync('assets/icon.png', 'not a valid image')
61+
const corruptFile = 'assets/corrupt-icon.png'
62+
fs.writeFileSync(corruptFile, 'not a valid image')
6163

62-
const {error} = await runCommand(['icons', '--appName', 'test'])
64+
const {stdout} = await runCommand(['icons', '--appName', 'TestApp', corruptFile])
6365

64-
// Verify appropriate error handling
66+
// Should handle error gracefully - verify error collection message appears
67+
expect(stdout).to.match(/failed to generate|asset.*failed/i)
6568
})
6669
```
6770

71+
**Files updated:**
72+
-`test/commands/icons.test.ts` - Added corrupt image test
73+
-`test/commands/splash.test.ts` - Added corrupt image test
74+
6875
### 3. Add Unit Tests for `extractAppName` Utility ✅ COMPLETED
6976

7077
**Status:** ✅ Completed in PR #XX
@@ -245,13 +252,13 @@ export function extractAppName(): string | null {
245252

246253
## Refactoring Suggestions (Lower Priority)
247254

248-
### 1. Extract Base Command Class ⏳ PENDING
255+
### 1. Extract Base Command Class ✅ COMPLETED
249256

250-
**Status:** ⏳ Not yet implemented (Priority: Medium)
257+
**Status:** ✅ Completed in this PR (Priority: Medium)
251258

252259
**Issue:** Both `Icons` and `Splash` commands duplicate the same `logVerbose()` implementation and `_isVerbose` property.
253260

254-
**File to create:** `src/commands/base.ts`
261+
**File created:** `src/commands/base.ts`
255262

256263
```typescript
257264
/*
@@ -275,15 +282,17 @@ export abstract class BaseCommand extends Command {
275282
}
276283
```
277284

278-
**Then update commands to extend `BaseCommand` instead of `Command`.**
285+
**Files updated:**
286+
-`src/commands/icons.ts` - Now extends `BaseCommand` instead of `Command`
287+
-`src/commands/splash.ts` - Now extends `BaseCommand` instead of `Command`
279288

280-
### 2. Add Type Definitions for Icon Sizes ⏳ PENDING
289+
### 2. Add Type Definitions for Icon Sizes ✅ COMPLETED
281290

282-
**Status:** ⏳ Not yet implemented (Priority: Low)
291+
**Status:** ✅ Completed in this PR (Priority: Low)
283292

284293
**File:** `src/types.ts`
285294

286-
Add interfaces for icon sizes (currently only splashscreen sizes are typed):
295+
Added interfaces for icon sizes (previously only splashscreen sizes were typed):
287296

288297
```typescript
289298
export interface IconSizeAndroid {
@@ -299,21 +308,21 @@ export interface IconSizeIOS {
299308
}
300309
```
301310

302-
**Then update `src/constants.ts`:**
311+
**Updated `src/constants.ts`:**
303312
```typescript
304313
export const ICON_SIZES_ANDROID: Array<IconSizeAndroid> = [...]
305314
export const ICON_SIZES_IOS: Array<IconSizeIOS> = [...]
306315
```
307316

308-
### 3. Improve Error Collection ⏳ PENDING
317+
### 3. Improve Error Collection ✅ COMPLETED
309318

310-
**Status:** ⏳ Not yet implemented (Priority: Medium)
319+
**Status:** ✅ Completed in this PR (Priority: Medium)
311320

312321
**Issue:** In `icons.ts` and `splash.ts`, errors during image generation are logged but execution continues silently. Users may not realize some icons failed to generate.
313322

314323
**Files:** `src/commands/icons.ts`, `src/commands/splash.ts`
315324

316-
**Suggestion:** Collect errors and report summary at end:
325+
**Implementation:** Errors are now collected and reported as a summary at the end:
317326

318327
```typescript
319328
private errors: string[] = []
@@ -324,7 +333,9 @@ this.errors.push(`Failed to generate: ${outputPath}`)
324333
// At end of run():
325334
if (this.errors.length > 0) {
326335
this.warn(`${yellow('')} ${this.errors.length} asset(s) failed to generate:`)
327-
this.errors.forEach(err => this.log(` - ${err}`))
336+
for (const err of this.errors) {
337+
this.log(` - ${err}`)
338+
}
328339
}
329340
```
330341

@@ -334,22 +345,23 @@ if (this.errors.length > 0) {
334345

335346
| File | Test File | Status |
336347
|------|-----------|--------|
337-
| `src/commands/icons.ts` | `test/commands/icons.test.ts` | ✅ Verbose tests added, typo fixed |
338-
| `src/commands/splash.ts` | `test/commands/splash.test.ts` | ✅ Verbose tests added, typo fixed, paths standardized |
348+
| `src/commands/icons.ts` | `test/commands/icons.test.ts` | ✅ Verbose tests added, typo fixed, base class extracted, error collection added, corrupt image test added |
349+
| `src/commands/splash.ts` | `test/commands/splash.test.ts` | ✅ Verbose tests added, typo fixed, paths standardized, base class extracted, error collection added, corrupt image test added |
339350
| `src/commands/dotenv.ts` | `test/commands/dotenv.test.ts` | ✅ Verbose flag + tests added |
351+
| `src/commands/base.ts` | N/A | ✅ Base command class created |
340352
| `src/utils/app.utils.ts` | `test/app.utils.test.ts` | ✅ Test file created, error handling improved |
341-
| `src/utils/file-utils.ts` | ❌ Missing | ⏳ Consider adding tests |
342-
| `src/utils/color.utils.ts` | ❌ Missing | ⏳ Consider adding tests |
343-
| `src/constants.ts` | N/A | ✅ License header added |
344-
| `src/types.ts` | N/A | Icon size interfaces pending |
353+
| `src/utils/file-utils.ts` | `test/utils/file-utils.test.ts` | ✅ Test file created with comprehensive coverage |
354+
| `src/utils/color.utils.ts` | `test/utils/color.utils.test.ts` | ✅ Test file created with comprehensive coverage |
355+
| `src/constants.ts` | N/A | ✅ License header added, type annotations added |
356+
| `src/types.ts` | N/A | Icon size interfaces added |
345357

346358
---
347359

348360
## Priority Summary
349361

350362
| Priority | Completed | Pending | Items |
351363
|----------|-----------|---------|-------|
352-
| 🔶 Medium | 2 | 3 | ✅ Verbose tests, dotenv verbose flag ⏳ Base command extraction, error collection, malformed input tests |
353-
| 🔷 Low | 4 | 2 | ✅ Typo fix, license header, path prefixes, extractAppName improvement ⏳ Icon size types, utility tests |
364+
| 🔶 Medium | 5 | 0 | ✅ Verbose tests, dotenv verbose flag, base command extraction, error collection, malformed input tests |
365+
| 🔷 Low | 6 | 0 | ✅ Typo fix, license header, path prefixes, extractAppName improvement, icon size types, utility tests |
354366

355-
**Overall Progress:** 6/11 items completed (55%)
367+
**Overall Progress:** 11/11 items completed (100%) ✅

src/commands/base.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/)
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import { Command } from '@oclif/core'
10+
11+
export abstract class BaseCommand extends Command {
12+
protected _isVerbose: boolean = false
13+
14+
protected logVerbose(message?: string, ...args: unknown[]) {
15+
if (this._isVerbose) {
16+
this.log(message, ...args)
17+
}
18+
}
19+
}

src/commands/icons.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
77
*/
88

9-
import { Args, Command, Flags } from '@oclif/core'
9+
import { Args, Flags } from '@oclif/core'
1010
import { writeFile } from 'node:fs/promises'
1111
import { join } from 'node:path'
1212
import sharp from 'sharp'
@@ -18,8 +18,9 @@ import { MaskType } from '../types.js'
1818
import { extractAppName } from '../utils/app.utils.js'
1919
import { cyan, green, red, yellow } from '../utils/color.utils.js'
2020
import { checkAssetFile, mkdirp } from '../utils/file-utils.js'
21+
import { BaseCommand } from './base.js'
2122

22-
export default class Icons extends Command {
23+
export default class Icons extends BaseCommand {
2324
static override args = {
2425
file: Args.string({
2526
default: './assets/icon.png',
@@ -49,7 +50,7 @@ The template icon file should be at least 1024x1024px.
4950
description: 'Print more detailed log messages.',
5051
}),
5152
}
52-
private _isVerbose: boolean = false
53+
private errors: string[] = []
5354

5455
public async run(): Promise<void> {
5556
const { args, flags } = await this.parse(Icons)
@@ -73,6 +74,13 @@ The template icon file should be at least 1024x1024px.
7374
this.generateIOSIcons(args.file, `ios/${flags.appName}/Images.xcassets/AppIcon.appiconset`),
7475
])
7576

77+
if (this.errors.length > 0) {
78+
this.warn(`${yellow('⚠')} ${this.errors.length} asset(s) failed to generate:`)
79+
for (const err of this.errors) {
80+
this.log(` - ${err}`)
81+
}
82+
}
83+
7684
this.log(green('✔'), `Generated icons for '${cyan(flags.appName)}' app.`)
7785
}
7886

@@ -93,6 +101,7 @@ The template icon file should be at least 1024x1024px.
93101

94102
this.logVerbose(green('✔'), cyan('Android'), `Icon '${cyan(outputPath)}' generated.`)
95103
} catch (error) {
104+
this.errors.push(`Failed to generate: ${outputPath}`)
96105
this.log(red('✘'), cyan('Android'), `Failed to generate icon '${cyan(outputPath)}':`, error)
97106
}
98107
}
@@ -148,6 +157,7 @@ The template icon file should be at least 1024x1024px.
148157

149158
this.logVerbose(green('✔'), cyan('iOS'), `Icon '${cyan(filename)}' generated.`)
150159
} catch (error) {
160+
this.errors.push(`Failed to generate: ${filename}`)
151161
this.log(red('✘'), cyan('iOS'), `Failed to generate icon '${cyan(filename)}'.`, error)
152162
}
153163
}
@@ -204,10 +214,4 @@ The template icon file should be at least 1024x1024px.
204214
const radius = Math.floor(size / 2)
205215
return Buffer.from(`<svg><circle cx="${radius}" cy="${radius}" r="${radius}" /></svg>`)
206216
}
207-
208-
private logVerbose(message?: string, ...args: unknown[]) {
209-
if (this._isVerbose) {
210-
this.log(message, ...args)
211-
}
212-
}
213217
}

src/commands/splash.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
77
*/
88

9-
import { Args, Command, Flags } from '@oclif/core'
9+
import { Args, Flags } from '@oclif/core'
1010
import { writeFile } from 'node:fs/promises'
1111
import { join } from 'node:path'
1212
import sharp from 'sharp'
@@ -17,8 +17,9 @@ import { SPLASHSCREEN_SIZES_ANDROID, SPLASHSCREEN_SIZES_IOS } from '../constants
1717
import { extractAppName } from '../utils/app.utils.js'
1818
import { cyan, green, red, yellow } from '../utils/color.utils.js'
1919
import { checkAssetFile, mkdirp } from '../utils/file-utils.js'
20+
import { BaseCommand } from './base.js'
2021

21-
export default class Splash extends Command {
22+
export default class Splash extends BaseCommand {
2223
static override args = {
2324
file: Args.string({
2425
default: './assets/splashscreen.png',
@@ -48,7 +49,7 @@ The template splashscreen file should be at least 1242x2208px.
4849
description: 'Print more detailed log messages.',
4950
}),
5051
}
51-
private _isVerbose: boolean = false
52+
private errors: string[] = []
5253

5354
public async run(): Promise<void> {
5455
const { args, flags } = await this.parse(Splash)
@@ -72,6 +73,13 @@ The template splashscreen file should be at least 1242x2208px.
7273
this.generateIOSSplashscreens(args.file, `ios/${flags.appName}/Images.xcassets/Splashscreen.imageset`),
7374
])
7475

76+
if (this.errors.length > 0) {
77+
this.warn(`${yellow('⚠')} ${this.errors.length} asset(s) failed to generate:`)
78+
for (const err of this.errors) {
79+
this.log(` - ${err}`)
80+
}
81+
}
82+
7583
this.log(green('✔'), `Generated splashscreens for '${cyan(flags.appName)}' app.`)
7684
}
7785

@@ -154,17 +162,12 @@ The template splashscreen file should be at least 1242x2208px.
154162

155163
this.logVerbose(green('✔'), `Splashscreen '${cyan(outputPath)}' generated.`)
156164
} catch (error) {
165+
this.errors.push(`Failed to generate: ${outputPath}`)
157166
this.log(red('✘'), `Failed to generate splashscreen '${cyan(outputPath)}':`, error)
158167
}
159168
}
160169

161170
private getIOSAssetNameForDensity(density?: string): string {
162171
return `splashscreen${density ? `@${density}` : ''}.png`
163172
}
164-
165-
private logVerbose(message?: string, ...args: unknown[]) {
166-
if (this._isVerbose) {
167-
this.log(message, ...args)
168-
}
169-
}
170173
}

src/constants.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
77
*/
88

9-
import type { SplashscreenSize } from "./types.js"
9+
import type { IconSizeAndroid, IconSizeIOS, SplashscreenSize } from "./types.js"
1010

1111
/**
1212
* Android Assets Sizes
1313
*/
1414

15-
export const ICON_SIZES_ANDROID = [
15+
export const ICON_SIZES_ANDROID: Array<IconSizeAndroid> = [
1616
{
1717
density: 'mdpi',
1818
size: 48,
@@ -72,7 +72,7 @@ export const SPLASHSCREEN_SIZES_ANDROID: Array<SplashscreenSize> = [
7272
* iOS Assets Sizes
7373
*/
7474

75-
export const ICON_SIZES_IOS = [
75+
export const ICON_SIZES_IOS: Array<IconSizeIOS> = [
7676
{
7777
baseSize: 20,
7878
name: 'Icon-Notification',

src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ export interface SplashscreenSize {
44
width: number;
55
}
66

7+
export interface IconSizeAndroid {
8+
density: string;
9+
size: number;
10+
}
11+
12+
export interface IconSizeIOS {
13+
baseSize: number;
14+
idiom?: string;
15+
name: string;
16+
scales: number[];
17+
}
18+
719
export interface ContentJsonImage {
820
filename: string;
921
idiom: string;

0 commit comments

Comments
 (0)