From add470f6e00f6debeef0ddd750bfdb82a52ad8ee Mon Sep 17 00:00:00 2001 From: Jayden Ong Date: Sun, 19 Jul 2026 11:27:14 -0400 Subject: [PATCH 1/7] feat/tests:Added feature that allows users to create a project with their specified programming language (js, py, java, ts) --- .../initModuleProjectController.ts | 6 +- .../initProject/initProjectController.ts | 6 +- .../initModuleProjectInputBoundary.ts | 4 +- .../initModuleProjectInputData.ts | 7 + .../initModuleProjectInteractor.ts | 19 ++- .../initProject/initProjectInputBoundary.ts | 4 +- .../initProject/initProjectInputData.ts | 7 + .../initProject/initProjectInteractor.ts | 17 ++- .../use_cases/initModuleProject.test.ts | 101 +++++++++++++- .../backend/use_cases/intiProject.test.ts | 123 +++++++++++++++++- 10 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts create mode 100644 clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts diff --git a/clean-architecture-visualizer/src/interface_adapter/initModuleProject/initModuleProjectController.ts b/clean-architecture-visualizer/src/interface_adapter/initModuleProject/initModuleProjectController.ts index f3ae9a2..e63f8ea 100644 --- a/clean-architecture-visualizer/src/interface_adapter/initModuleProject/initModuleProjectController.ts +++ b/clean-architecture-visualizer/src/interface_adapter/initModuleProject/initModuleProjectController.ts @@ -1,9 +1,11 @@ import type { InitModuleProjectInputBoundary } from '../../use_case/initModuleProject/initModuleProjectInputBoundary.js'; +import { InitModuleProjectInputData } from '../../use_case/initModuleProject/initModuleProjectInputData.js'; export class InitModuleProjectController { constructor(private readonly inputBoundary: InitModuleProjectInputBoundary) {} - async execute() { - await this.inputBoundary.execute(); + async execute(language: string) { + const initModuleProjectInputData = new InitModuleProjectInputData(language); + await this.inputBoundary.execute(initModuleProjectInputData); } } diff --git a/clean-architecture-visualizer/src/interface_adapter/initProject/initProjectController.ts b/clean-architecture-visualizer/src/interface_adapter/initProject/initProjectController.ts index f8fe94b..06149ed 100644 --- a/clean-architecture-visualizer/src/interface_adapter/initProject/initProjectController.ts +++ b/clean-architecture-visualizer/src/interface_adapter/initProject/initProjectController.ts @@ -1,9 +1,11 @@ import type { InitProjectInputBoundary } from '../../use_case/initProject/initProjectInputBoundary.js'; +import { InitProjectInputData } from '../../use_case/initProject/initProjectInputData.js'; export class InitProjectController { constructor(private readonly inputBoundary: InitProjectInputBoundary) {} - async execute(): Promise { - await this.inputBoundary.execute(); + async execute(language: string): Promise { + const initProjectInputData = new InitProjectInputData(language); + await this.inputBoundary.execute(initProjectInputData); } } diff --git a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts index c1267ab..10b976f 100644 --- a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts +++ b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts @@ -1,3 +1,5 @@ +import { InitModuleProjectInputData } from "./initModuleProjectInputData.js"; + export interface InitModuleProjectInputBoundary { - execute(): Promise; + execute(initModuleProjectInputData: InitModuleProjectInputData): Promise; } diff --git a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts new file mode 100644 index 0000000..89746ac --- /dev/null +++ b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts @@ -0,0 +1,7 @@ +export class InitModuleProjectInputData { + constructor(private readonly language: string){} + + getLanguage(): string { + return this.language; + } +} \ No newline at end of file diff --git a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts index 56f0b8c..4cdfdba 100644 --- a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts @@ -1,6 +1,7 @@ import type { FileAccessInterface } from '../../data_access/fileAccessInterface.js'; import type { InitModuleProjectInputBoundary } from './initModuleProjectInputBoundary.js'; import { InitModuleProjectOutputData } from './initModuleProjectOutputData.js'; +import { InitModuleProjectInputData } from './initModuleProjectInputData.js'; import path from 'path'; export class InitModuleProjectInteractor implements InitModuleProjectInputBoundary { @@ -14,8 +15,14 @@ export class InitModuleProjectInteractor implements InitModuleProjectInputBounda this.outputData = outputData; } - async execute(): Promise { + async execute(initModuleProjectInputData: InitModuleProjectInputData): Promise { try { + const acceptedLanguage = ['typescript', 'javascript', 'java', 'python']; + const language = initModuleProjectInputData.getLanguage(); + if(!acceptedLanguage.includes(language.toLowerCase())){ + throw new Error('You must enter a valid programming language.'); + } + let currPath = await this.fileAccess.getCurrentPath(); currPath = path.join(currPath, 'src'); // Create the src directory if it does not exist. @@ -31,10 +38,10 @@ export class InitModuleProjectInteractor implements InitModuleProjectInputBounda throw new Error('project already initialized.'); } - // The project does support TS, JS, Python, and Java, but will do Java for now for consistency - const javaPath = path.join(currPath, 'main', 'java'); - const testPath = path.join(currPath, 'test', 'java'); - await this.fileAccess.createDirectory(javaPath); + // The project does support TS, JS, Python, and Java + const programmingPath = path.join(currPath, 'main', language.toLowerCase()); + const testPath = path.join(currPath, 'test', language.toLowerCase()); + await this.fileAccess.createDirectory(programmingPath); await this.fileAccess.createDirectory(testPath); // Create code structure for packaging by module. @@ -48,7 +55,7 @@ export class InitModuleProjectInteractor implements InitModuleProjectInputBounda ]; for (const directoryName of subDirectories) { await this.fileAccess.createDirectory( - path.join(javaPath, directoryName) + path.join(programmingPath, directoryName) ); } diff --git a/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts index 21bde71..f9df22d 100644 --- a/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts +++ b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts @@ -1,3 +1,5 @@ +import { InitProjectInputData } from "./initProjectInputData.js"; + export interface InitProjectInputBoundary { - execute(): Promise; + execute(initProjectInputData: InitProjectInputData): Promise; } diff --git a/clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts new file mode 100644 index 0000000..af3798d --- /dev/null +++ b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts @@ -0,0 +1,7 @@ +export class InitProjectInputData { + constructor(private readonly language: string){} + + getLanguage(): string { + return this.language; + } +} \ No newline at end of file diff --git a/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts b/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts index bc242e0..f7f46d1 100644 --- a/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts @@ -1,5 +1,6 @@ import type { FileAccessInterface } from '../../data_access/fileAccessInterface.js'; import type { InitProjectInputBoundary } from './initProjectInputBoundary.js'; +import { InitProjectInputData } from './initProjectInputData.js'; import { InitProjectOutputData } from './initProjectOutputData.js'; import path from 'path'; @@ -14,8 +15,14 @@ export class InitProjectInteractor implements InitProjectInputBoundary { ((this.fileAccess = fileAccess), (this.outputData = outputData)); } - async execute(): Promise { + async execute(initProjectInputData: InitProjectInputData): Promise { try { + const acceptedLanguage = ['typescript', 'javascript', 'java', 'python']; + const language = initProjectInputData.getLanguage(); + if(!acceptedLanguage.includes(language.toLowerCase())){ + throw new Error('You must enter a valid programming language.'); + } + let currPath = await this.fileAccess.getCurrentPath(); currPath = path.join(currPath, 'src'); const res = await this.fileAccess.exists(currPath); @@ -32,8 +39,8 @@ export class InitProjectInteractor implements InitProjectInputBoundary { } // 1. Define base paths using path.join for cross-platform support - const javaPath = path.join(currPath, 'main', 'java'); - const testPath = path.join(currPath, 'test', 'java'); + const programmingPath = path.join(currPath, 'main', language.toLowerCase()); + const testPath = path.join(currPath, 'test', language.toLowerCase()); // 2. Define sub-directories within the java path const subDirs = [ @@ -46,11 +53,11 @@ export class InitProjectInteractor implements InitProjectInputBoundary { 'database', ]; - await this.fileAccess.createDirectory(javaPath); + await this.fileAccess.createDirectory(programmingPath); await this.fileAccess.createDirectory(testPath); for (const dirName of subDirs) { - const fullPath = path.join(javaPath, dirName); + const fullPath = path.join(programmingPath, dirName); await this.fileAccess.createDirectory(fullPath); } diff --git a/clean-architecture-visualizer/tests/backend/use_cases/initModuleProject.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/initModuleProject.test.ts index e16a798..2e23b8f 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/initModuleProject.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/initModuleProject.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import type { FileAccessInterface } from '../../../src/data_access/fileAccessInterface.js'; import type { InitModuleProjectOutputData } from '../../../src/use_case/initModuleProject/initModuleProjectOutputData.js'; import { InitModuleProjectInteractor } from '../../../src/use_case/initModuleProject/initModuleProjectInteractor.js'; +import { InitModuleProjectInputData } from '../../../src/use_case/initModuleProject/initModuleProjectInputData.js'; describe('InitModuleProjectInteractor', () => { let mockFileAccess: jest.Mocked; @@ -29,10 +30,10 @@ describe('InitModuleProjectInteractor', () => { ); }); - it('Creates CA directory that is packaged by module (src exists).', async () => { + it('Creates CA directory that is packaged by module (src exists) in java.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); mockFileAccess.exists.mockResolvedValue(true); - await interactor.execute(); + await interactor.execute(new InitModuleProjectInputData('Java')); const expectedDirs = [ `${ROOT_PATH}/src/main/java`, @@ -56,10 +57,96 @@ describe('InitModuleProjectInteractor', () => { expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); }); + it('Creates CA directory that is packaged by module (src exists) in python.', async () => { + mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); + mockFileAccess.exists.mockResolvedValue(true); + await interactor.execute(new InitModuleProjectInputData('python')); + + const expectedDirs = [ + `${ROOT_PATH}/src/main/python`, + `${ROOT_PATH}/src/test/python`, + `${ROOT_PATH}/src/main/python/features`, + `${ROOT_PATH}/src/main/python/data_access`, + `${ROOT_PATH}/src/main/python/entity`, + `${ROOT_PATH}/src/main/python/app`, + `${ROOT_PATH}/src/main/python/views`, + `${ROOT_PATH}/src/main/python/database`, + ]; + + expectedDirs.forEach((dir) => { + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith(dir); + }); + + expect(mockFileAccess.createDirectory).toHaveBeenCalledTimes( + expectedDirs.length + ); + + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); + }); + + it('Creates CA directory that is packaged by module (src exists) in javascript.', async () => { + mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); + mockFileAccess.exists.mockResolvedValue(true); + await interactor.execute(new InitModuleProjectInputData('javascript')); + + const expectedDirs = [ + `${ROOT_PATH}/src/main/javascript`, + `${ROOT_PATH}/src/test/javascript`, + `${ROOT_PATH}/src/main/javascript/features`, + `${ROOT_PATH}/src/main/javascript/data_access`, + `${ROOT_PATH}/src/main/javascript/entity`, + `${ROOT_PATH}/src/main/javascript/app`, + `${ROOT_PATH}/src/main/javascript/views`, + `${ROOT_PATH}/src/main/javascript/database`, + ]; + + expectedDirs.forEach((dir) => { + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith(dir); + }); + + expect(mockFileAccess.createDirectory).toHaveBeenCalledTimes( + expectedDirs.length + ); + + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); + }); + + it('Creates CA directory that is packaged by module (src exists) in typescript.', async () => { + mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); + mockFileAccess.exists.mockResolvedValue(true); + await interactor.execute(new InitModuleProjectInputData('typescript')); + + const expectedDirs = [ + `${ROOT_PATH}/src/main/typescript`, + `${ROOT_PATH}/src/test/typescript`, + `${ROOT_PATH}/src/main/typescript/features`, + `${ROOT_PATH}/src/main/typescript/data_access`, + `${ROOT_PATH}/src/main/typescript/entity`, + `${ROOT_PATH}/src/main/typescript/app`, + `${ROOT_PATH}/src/main/typescript/views`, + `${ROOT_PATH}/src/main/typescript/database`, + ]; + + expectedDirs.forEach((dir) => { + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith(dir); + }); + + expect(mockFileAccess.createDirectory).toHaveBeenCalledTimes( + expectedDirs.length + ); + + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); + }); + + it('Error occurs because the programming language is invalid.', async () => { + await interactor.execute(new InitModuleProjectInputData('blah blah blah')); + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); + }); + it('Error occurs because src directory does not exist.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); mockFileAccess.exists.mockResolvedValue(false); - await interactor.execute(); + await interactor.execute(new InitModuleProjectInputData('java')); expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); }); @@ -69,13 +156,13 @@ describe('InitModuleProjectInteractor', () => { new Error('Permission denied') ); - await interactor.execute(); + await interactor.execute(new InitModuleProjectInputData('java')); expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); }); it('Handles errors if getCurrentPath fails.', async () => { mockFileAccess.getCurrentPath.mockRejectedValue(new Error('Path unknown')); - await interactor.execute(); + await interactor.execute(new InitModuleProjectInputData('java')); // Assert expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); @@ -87,7 +174,7 @@ describe('InitModuleProjectInteractor', () => { mockFileAccess.bfsFindDir.mockResolvedValue(`${ROOT_PATH}/src/main`); mockFileAccess.exists.mockResolvedValue(true); - await interactor.execute(); + await interactor.execute(new InitModuleProjectInputData('java')); expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); expect(mockFileAccess.createDirectory).not.toHaveBeenCalled(); }); @@ -97,7 +184,7 @@ describe('InitModuleProjectInteractor', () => { mockFileAccess.bfsFindDir.mockResolvedValue(`${ROOT_PATH}/src/test`); mockFileAccess.exists.mockResolvedValue(true); - await interactor.execute(); + await interactor.execute(new InitModuleProjectInputData('java')); expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); expect(mockFileAccess.createDirectory).not.toHaveBeenCalled(); }); diff --git a/clean-architecture-visualizer/tests/backend/use_cases/intiProject.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/intiProject.test.ts index 4420a43..4fbbb70 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/intiProject.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/intiProject.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import { InitProjectInteractor } from '../../../src/use_case/initProject/initProjectInteractor.js'; import type { FileAccessInterface } from '../../../src/data_access/fileAccessInterface.js'; import type { InitProjectOutputData } from '../../../src/use_case/initProject/initProjectOutputData.js'; +import { InitProjectInputData } from '../../../src/use_case/initProject/initProjectInputData.js'; describe('InitProjectInteractor', () => { let mockFileAccess: jest.Mocked; @@ -28,13 +29,13 @@ describe('InitProjectInteractor', () => { interactor = new InitProjectInteractor(mockFileAccess, mockOutputData); }); - it('successfully creates the entire Clean Architecture directory structure', async () => { + it('successfully creates the entire Clean Architecture directory structure in java', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); mockFileAccess.exists.mockResolvedValue(true); // Act - await interactor.execute(); + await interactor.execute(new InitProjectInputData('java')); // Assert const expectedDirs = [ @@ -63,6 +64,111 @@ describe('InitProjectInteractor', () => { expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); }); + it('successfully creates the entire Clean Architecture directory structure in python', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); + mockFileAccess.exists.mockResolvedValue(true); + + // Act + await interactor.execute(new InitProjectInputData('python')); + + // Assert + const expectedDirs = [ + `${ROOT_PATH}/src/main/python`, + `${ROOT_PATH}/src/test/python`, + `${ROOT_PATH}/src/main/python/app`, + `${ROOT_PATH}/src/main/python/use_case`, + `${ROOT_PATH}/src/main/python/entity`, + `${ROOT_PATH}/src/main/python/interface_adapter`, + `${ROOT_PATH}/src/main/python/data_access`, + `${ROOT_PATH}/src/main/python/database`, + `${ROOT_PATH}/src/main/python/view`, + ]; + + // Check that each directory was called + expectedDirs.forEach((dir) => { + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith(dir); + }); + + // Verify total number of calls matches the number of directories + expect(mockFileAccess.createDirectory).toHaveBeenCalledTimes( + expectedDirs.length + ); + + // Verify success signal + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); + }); + + it('successfully creates the entire Clean Architecture directory structure in javascript', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); + mockFileAccess.exists.mockResolvedValue(true); + + // Act + await interactor.execute(new InitProjectInputData('Javascript')); + + // Assert + const expectedDirs = [ + `${ROOT_PATH}/src/main/javascript`, + `${ROOT_PATH}/src/test/javascript`, + `${ROOT_PATH}/src/main/javascript/app`, + `${ROOT_PATH}/src/main/javascript/use_case`, + `${ROOT_PATH}/src/main/javascript/entity`, + `${ROOT_PATH}/src/main/javascript/interface_adapter`, + `${ROOT_PATH}/src/main/javascript/data_access`, + `${ROOT_PATH}/src/main/javascript/database`, + `${ROOT_PATH}/src/main/javascript/view`, + ]; + + // Check that each directory was called + expectedDirs.forEach((dir) => { + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith(dir); + }); + + // Verify total number of calls matches the number of directories + expect(mockFileAccess.createDirectory).toHaveBeenCalledTimes( + expectedDirs.length + ); + + // Verify success signal + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); + }); + + it('successfully creates the entire Clean Architecture directory structure in typescript', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); + mockFileAccess.exists.mockResolvedValue(true); + + // Act + await interactor.execute(new InitProjectInputData('typescript')); + + // Assert + const expectedDirs = [ + `${ROOT_PATH}/src/main/typescript`, + `${ROOT_PATH}/src/test/typescript`, + `${ROOT_PATH}/src/main/typescript/app`, + `${ROOT_PATH}/src/main/typescript/use_case`, + `${ROOT_PATH}/src/main/typescript/entity`, + `${ROOT_PATH}/src/main/typescript/interface_adapter`, + `${ROOT_PATH}/src/main/typescript/data_access`, + `${ROOT_PATH}/src/main/typescript/database`, + `${ROOT_PATH}/src/main/typescript/view`, + ]; + + // Check that each directory was called + expectedDirs.forEach((dir) => { + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith(dir); + }); + + // Verify total number of calls matches the number of directories + expect(mockFileAccess.createDirectory).toHaveBeenCalledTimes( + expectedDirs.length + ); + + // Verify success signal + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(true); + }); + it('sets output data to false if the file system fails', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); @@ -72,7 +178,7 @@ describe('InitProjectInteractor', () => { ); // Act - await interactor.execute(); + await interactor.execute(new InitProjectInputData('java')); // Assert expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); @@ -83,7 +189,7 @@ describe('InitProjectInteractor', () => { mockFileAccess.getCurrentPath.mockRejectedValue(new Error('Path unknown')); // Act - await interactor.execute(); + await interactor.execute(new InitProjectInputData('java')); // Assert expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); @@ -97,7 +203,7 @@ describe('InitProjectInteractor', () => { mockFileAccess.bfsFindDir.mockResolvedValue(`${ROOT_PATH}/src/main`); // Act - await interactor.execute(); + await interactor.execute(new InitProjectInputData('java')); // Assert expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); @@ -107,7 +213,12 @@ describe('InitProjectInteractor', () => { it('Error occurs because src directory does not exist.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue(ROOT_PATH); mockFileAccess.exists.mockResolvedValue(false); - await interactor.execute(); + await interactor.execute(new InitProjectInputData('java')); + expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); + }); + + it('Error occurs because programming language is invalid..', async () => { + await interactor.execute(new InitProjectInputData('blah blah blah')); expect(mockOutputData.setOutputData).toHaveBeenCalledWith(false); }); }); From 09a857f78c6cb3ffb493e3dd066eee5d289df5dc Mon Sep 17 00:00:00 2001 From: Jayden Ong Date: Sun, 19 Jul 2026 12:22:18 -0400 Subject: [PATCH 2/7] feat:modified cave initialization command to require a language parameter --- clean-architecture-visualizer/src/app/appBuilder.ts | 8 ++++---- clean-architecture-visualizer/src/app/index.ts | 12 ++++++------ .../src/server/routes/template.ts | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/clean-architecture-visualizer/src/app/appBuilder.ts b/clean-architecture-visualizer/src/app/appBuilder.ts index aef24a1..bae9663 100644 --- a/clean-architecture-visualizer/src/app/appBuilder.ts +++ b/clean-architecture-visualizer/src/app/appBuilder.ts @@ -305,8 +305,8 @@ export class AppBuilder { await this.graphVerificationController?.execute(formatForCLI); } - async runInitProject() { - await this.initProjectController?.execute(); + async runInitProject(language: string) { + await this.initProjectController?.execute(language); if (this.initProjectOutputData?.getOutputData()) { console.log(chalk.green('Your project has been initialized.')); } else { @@ -318,8 +318,8 @@ export class AppBuilder { } } - async runInitModuleProject() { - await this.initModuleProjectController?.execute(); + async runInitModuleProject(language: string) { + await this.initModuleProjectController?.execute(language); if (this.initModuleProjectOutputData?.getOutputData()) { console.log( chalk.green('Your project packaged by module has been initialized.') diff --git a/clean-architecture-visualizer/src/app/index.ts b/clean-architecture-visualizer/src/app/index.ts index 62b36ad..d805d5d 100755 --- a/clean-architecture-visualizer/src/app/index.ts +++ b/clean-architecture-visualizer/src/app/index.ts @@ -64,19 +64,19 @@ program }); program - .command('init') + .command('init ') .description('Create the template for a new CSC207 project') - .action(async () => { - app.runInitProject(); + .action(async (language: string) => { + app.runInitProject(language); }); program - .command('module_init') + .command('module_init ') .description( 'Create the template for a new CSC207 project, packaged by module.' ) - .action(async () => { - app.runInitModuleProject(); + .action(async (language: string) => { + app.runInitModuleProject(language); }); program diff --git a/clean-architecture-visualizer/src/server/routes/template.ts b/clean-architecture-visualizer/src/server/routes/template.ts index 073cd95..7ec361c 100644 --- a/clean-architecture-visualizer/src/server/routes/template.ts +++ b/clean-architecture-visualizer/src/server/routes/template.ts @@ -22,13 +22,13 @@ const router = Router(); const fileAccess = new FileAccess(); -router.post('/template/generate', async (_req, res) => { +router.post('/template/generate/:language', async (req, res) => { const outputData = new InitProjectOutputData(); const interactor = new InitProjectInteractor(fileAccess, outputData); const controller = new InitProjectController(interactor); const presenter = new InitProjectPresenter(outputData); - await controller.execute(); + await controller.execute(req.params.language); const result = presenter.getOutputData(); if (!result) { @@ -39,13 +39,13 @@ router.post('/template/generate', async (_req, res) => { res.status(201).json({ message: `Project initiated successfully` }); }); -router.post('/template/module_generate', async (_req, res) => { +router.post('/template/module_generate/:language', async (req, res) => { const outputData = new InitModuleProjectOutputData(); const interactor = new InitModuleProjectInteractor(fileAccess, outputData); const controller = new InitModuleProjectController(interactor); const presenter = new InitModuleProjectPresenter(outputData); - await controller.execute(); + await controller.execute(req.params.language); const result = presenter.getOutputData(); if (!result) { From 5dfcc83957341251bf22fd6da497064f042a3295 Mon Sep 17 00:00:00 2001 From: Jayden Ong Date: Sun, 19 Jul 2026 15:34:15 -0400 Subject: [PATCH 3/7] feat/tests:Added ability for users to add use cases depending on the programming language they selected when first creating project --- .../createModuleUseCaseInteractor.ts | 33 +++- .../createUseCase/createUseCaseInteractor.ts | 39 ++++- .../use_cases/createModuleUseCase.test.ts | 158 +++++++++++++++++- .../backend/use_cases/createUseCase.test.ts | 153 ++++++++++++++++- 4 files changed, 362 insertions(+), 21 deletions(-) diff --git a/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts b/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts index 4c61fc0..6303516 100644 --- a/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts @@ -16,8 +16,29 @@ export class CreateModuleUseCaseInteractor implements CreateModuleUseCaseInputBo // remove all spaces from use case name and feature name const feature = inputData.getFeatureName().split(' ').join(''); const usecase = inputData.getUseCaseName().split(' ').join(''); - // check if features directory exists const currPath = await this.fileAccess.getCurrentPath(); + // Find the language directory -- makes assumption only one directory is named after language + let extension: string | undefined = undefined; + const languageToExtension = new Map([ + ['python', 'py'], + ['java', 'java'], + ['javascript', 'js'], + ['typescript', 'ts'], + + ]) + for(const [language, ext] of languageToExtension){ + if(await this.fileAccess.bfsFindDir(currPath, language)){ + extension = ext; + break; + } + } + + if(extension === undefined) { + this.presenter.showFailView('Your project does not have a specified programming language.'); + return; + } + + // check if features directory exists const featuresDirectory = await this.fileAccess.bfsFindDir( currPath, 'features' @@ -57,8 +78,8 @@ export class CreateModuleUseCaseInteractor implements CreateModuleUseCaseInputBo await this.fileAccess.createDirectory(ucPath); // Create all files. - const createJavaFile = async (dir: string, suffix: string) => { - const fileName = `${usecase}${suffix}.java`; + const createFile = async (dir: string, suffix: string) => { + const fileName = `${usecase}${suffix}.${extension}`; const fullPath = path.join(dir, fileName); return await this.fileAccess.createFile(fullPath); }; @@ -72,11 +93,11 @@ export class CreateModuleUseCaseInteractor implements CreateModuleUseCaseInputBo ]; for (const ucFile of ucFiles) { - await createJavaFile(ucPath, ucFile); + await createFile(ucPath, ucFile); } - await createJavaFile(iaPath, 'Controller'); - await createJavaFile(iaPath, 'Presenter'); + await createFile(iaPath, 'Controller'); + await createFile(iaPath, 'Presenter'); const outputData = new CreateModuleUseCaseOutputData(feature, usecase); this.presenter.showSuccessView(outputData); diff --git a/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts b/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts index b0e97e4..a2dab9e 100644 --- a/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts @@ -19,6 +19,27 @@ export class CreateUseCaseInteractor implements CreateUseCaseInputBoundary { .join(''); const currPath = await this.fileAccess.getCurrentPath(); + // Find the language directory -- makes assumption only one directory is named after language + let extension: string | undefined = undefined; + const languageToExtension = new Map([ + ['python', 'py'], + ['java', 'java'], + ['javascript', 'js'], + ['typescript', 'ts'], + + ]) + for(const [language, ext] of languageToExtension){ + if(await this.fileAccess.bfsFindDir(currPath, language)){ + extension = ext; + break; + } + } + + if(extension === undefined) { + this.presenter.showFailView('Your project does not have a specified programming language.'); + return; + } + // Find base directories const useCaseDir = await this.fileAccess.bfsFindDir(currPath, 'use_case'); const interfaceAdapterDir = await this.fileAccess.bfsFindDir( @@ -48,22 +69,22 @@ export class CreateUseCaseInteractor implements CreateUseCaseInputBoundary { await this.fileAccess.createDirectory(targetUseCasePath); await this.fileAccess.createDirectory(targetInterfacePath); - const createJavaFile = async (dir: string, suffix: string) => { - const fileName = `${useCaseName}${suffix}.java`; + const createFile = async (dir: string, suffix: string) => { + const fileName = `${useCaseName}${suffix}.${extension}`; const fullPath = path.join(dir, fileName); return await this.fileAccess.createFile(fullPath); }; // Use Case Layer Files - await createJavaFile(targetUseCasePath, 'InputBoundary'); - await createJavaFile(targetUseCasePath, 'InputData'); - await createJavaFile(targetUseCasePath, 'UseCaseInteractor'); - await createJavaFile(targetUseCasePath, 'OutputData'); - await createJavaFile(targetUseCasePath, 'OutputBoundary'); + await createFile(targetUseCasePath, 'InputBoundary'); + await createFile(targetUseCasePath, 'InputData'); + await createFile(targetUseCasePath, 'UseCaseInteractor'); + await createFile(targetUseCasePath, 'OutputData'); + await createFile(targetUseCasePath, 'OutputBoundary'); // Interface Adapter Layer Files - await createJavaFile(targetInterfacePath, 'Controller'); - await createJavaFile(targetInterfacePath, 'Presenter'); + await createFile(targetInterfacePath, 'Controller'); + await createFile(targetInterfacePath, 'Presenter'); const createUseCaseOutputData = new CreateUseCaseOutputData(useCaseName); this.presenter.showSuccessView(createUseCaseOutputData); diff --git a/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts index 79bbb97..f9b9c89 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts @@ -29,10 +29,11 @@ describe('CreateFeatureInteractor', () => { ); }); - it('Successfully creates files in specified directories in specified use case in specified feature.', async () => { + it('Successfully creates files in specified directories in specified use case in specified feature in java.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue('/root'); - // bfsFindDir runs twice and will need two different implementations - // The first will find the features directory and the second will find the specific feature + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('/root/src/java'); mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) .mockImplementationOnce( @@ -68,10 +69,134 @@ describe('CreateFeatureInteractor', () => { expect(mockPresenter.showFailView).not.toHaveBeenCalled(); }); + it('Successfully creates files in specified directories in specified use case in specified feature in python.', async () => { + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce('/root/src/python'); + mockFileAccess.bfsFindDir + .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) + .mockImplementationOnce( + async (_, dirName) => `/root/src/features/${dirName}` + ) + .mockImplementationOnce(async (_, dirName) => null); + const inputData = new CreateModuleUseCaseInputData( + 'newFeature', + 'newUseCase' + ); + await interactor.execute(inputData); + + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/features/newFeature/newUseCase/interface_adapter' + ); + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/features/newFeature/newUseCase/use_case' + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseInputBoundary.py') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseUseCaseInteractor.py') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseController.py') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledTimes(7); + + expect(mockPresenter.showSuccessView).toHaveBeenCalledWith( + new CreateModuleUseCaseOutputData('newFeature', 'newUseCase') + ); + expect(mockPresenter.showFailView).not.toHaveBeenCalled(); + }); + + it('Successfully creates files in specified directories in specified use case in specified feature in javascript.', async () => { + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('/root/src/javascript'); + mockFileAccess.bfsFindDir + .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) + .mockImplementationOnce( + async (_, dirName) => `/root/src/features/${dirName}` + ) + .mockImplementationOnce(async (_, dirName) => null); + const inputData = new CreateModuleUseCaseInputData( + 'newFeature', + 'newUseCase' + ); + await interactor.execute(inputData); + + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/features/newFeature/newUseCase/interface_adapter' + ); + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/features/newFeature/newUseCase/use_case' + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseInputBoundary.js') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseUseCaseInteractor.js') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseController.js') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledTimes(7); + + expect(mockPresenter.showSuccessView).toHaveBeenCalledWith( + new CreateModuleUseCaseOutputData('newFeature', 'newUseCase') + ); + expect(mockPresenter.showFailView).not.toHaveBeenCalled(); + }); + + it('Successfully creates files in specified directories in specified use case in specified feature in typescript.', async () => { + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('/root/src/typescript'); + mockFileAccess.bfsFindDir + .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) + .mockImplementationOnce( + async (_, dirName) => `/root/src/features/${dirName}` + ) + .mockImplementationOnce(async (_, dirName) => null); + const inputData = new CreateModuleUseCaseInputData( + 'newFeature', + 'newUseCase' + ); + await interactor.execute(inputData); + + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/features/newFeature/newUseCase/interface_adapter' + ); + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/features/newFeature/newUseCase/use_case' + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseInputBoundary.ts') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseUseCaseInteractor.ts') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('newUseCaseController.ts') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledTimes(7); + + expect(mockPresenter.showSuccessView).toHaveBeenCalledWith( + new CreateModuleUseCaseOutputData('newFeature', 'newUseCase') + ); + expect(mockPresenter.showFailView).not.toHaveBeenCalled(); + }); + it('Fails to create files because features directory does not exist.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue('/root'); // bfsFindDir will only run once when trying to find features - mockFileAccess.bfsFindDir.mockResolvedValue(null); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce('/root/src/python') + .mockResolvedValueOnce(null); const inputData = new CreateModuleUseCaseInputData( 'newFeature', 'newUseCase' @@ -85,6 +210,12 @@ describe('CreateFeatureInteractor', () => { it('Fails to create files because input feature does not exist.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('root/src/typescript'); + mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) .mockImplementationOnce(async (_, dirName) => null); @@ -101,6 +232,7 @@ describe('CreateFeatureInteractor', () => { it('Fails to create files because use case already exists.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('/root/src/python'); mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) .mockImplementationOnce( @@ -121,6 +253,7 @@ describe('CreateFeatureInteractor', () => { }); it('Fails to create files because use case already exists in another directory.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('/root/src/python'); mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) .mockImplementationOnce( @@ -143,6 +276,7 @@ describe('CreateFeatureInteractor', () => { it('Fails if an unexpected error occurs during directory creation.', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('/root/src/python'); mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) .mockImplementationOnce( @@ -160,6 +294,7 @@ describe('CreateFeatureInteractor', () => { it('Fails if an unexpected error occurs during file creation.', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('/root/src/python'); mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) .mockImplementationOnce( @@ -175,4 +310,19 @@ describe('CreateFeatureInteractor', () => { expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); expect(mockPresenter.showFailView).toHaveBeenCalled(); }); + + it('Fails if there is no specified language.', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValue(null) + .mockResolvedValue(null) + .mockResolvedValue(null) + .mockResolvedValue(null); + await interactor.execute( + new CreateModuleUseCaseInputData('newFeature', 'newUseCase') + ); + expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); + expect(mockPresenter.showFailView).toHaveBeenCalled(); + }); }); diff --git a/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts index 9f9d221..9bf4df5 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts @@ -30,9 +30,12 @@ describe('CreateUseCaseInteractor', () => { // TODO: Add a separate test case checking if use case names with spaces wer properly sanitized - it('successfully creates directories and files for a valid use case name', async () => { + it('successfully creates directories and files for a valid use case name in java', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('root/src/java'); mockFileAccess.bfsFindDir.mockImplementation( async (path, dirName) => `/root/src/${dirName}` ); @@ -68,9 +71,135 @@ describe('CreateUseCaseInteractor', () => { expect(mockPresenter.showFailView).not.toHaveBeenCalled(); }); + it('successfully creates directories and files for a valid use case name in python', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce('root/src/python'); + mockFileAccess.bfsFindDir.mockImplementation( + async (path, dirName) => `/root/src/${dirName}` + ); + + // Act + const inputData = new CreateUseCaseInputData('LoginUser'); + await interactor.execute(inputData); + + // Assert + // Verify directory creation + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/use_case/LoginUser' + ); + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/interface_adapter/LoginUser' + ); + + // Verify key files were created + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserInputBoundary.py') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserUseCaseInteractor.py') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserController.py') + ); + + // Verify success signal + expect(mockPresenter.showSuccessView).toHaveBeenCalledWith( + new CreateUseCaseOutputData('LoginUser') + ); + expect(mockPresenter.showFailView).not.toHaveBeenCalled(); + }); + + it('successfully creates directories and files for a valid use case name in javascript', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('root/src/javascript'); + mockFileAccess.bfsFindDir.mockImplementation( + async (path, dirName) => `/root/src/${dirName}` + ); + + // Act + const inputData = new CreateUseCaseInputData('LoginUser'); + await interactor.execute(inputData); + + // Assert + // Verify directory creation + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/use_case/LoginUser' + ); + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/interface_adapter/LoginUser' + ); + + // Verify key files were created + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserInputBoundary.js') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserUseCaseInteractor.js') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserController.js') + ); + + // Verify success signal + expect(mockPresenter.showSuccessView).toHaveBeenCalledWith( + new CreateUseCaseOutputData('LoginUser') + ); + expect(mockPresenter.showFailView).not.toHaveBeenCalled(); + }); + + it('successfully creates directories and files for a valid use case name in typescript', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('root/src/typescript'); + mockFileAccess.bfsFindDir.mockImplementation( + async (path, dirName) => `/root/src/${dirName}` + ); + + // Act + const inputData = new CreateUseCaseInputData('LoginUser'); + await interactor.execute(inputData); + + // Assert + // Verify directory creation + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/use_case/LoginUser' + ); + expect(mockFileAccess.createDirectory).toHaveBeenCalledWith( + '/root/src/interface_adapter/LoginUser' + ); + + // Verify key files were created + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserInputBoundary.ts') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserUseCaseInteractor.ts') + ); + expect(mockFileAccess.createFile).toHaveBeenCalledWith( + expect.stringContaining('LoginUserController.ts') + ); + + // Verify success signal + expect(mockPresenter.showSuccessView).toHaveBeenCalledWith( + new CreateUseCaseOutputData('LoginUser') + ); + expect(mockPresenter.showFailView).not.toHaveBeenCalled(); + }); + it('fails and sets output data to false if directories are not found', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('/root/src/python'); // Simulate missing directory mockFileAccess.bfsFindDir.mockResolvedValue(null); @@ -89,6 +218,7 @@ describe('CreateUseCaseInteractor', () => { it('fails and sets output data to false if directories are already present', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('/root/src/python'); mockFileAccess.bfsFindDir.mockImplementation( async (path, dirName) => `/root/src/${dirName}` ); @@ -111,7 +241,9 @@ describe('CreateUseCaseInteractor', () => { it('fails if an unexpected error occurs during file creation', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue('/root'); - mockFileAccess.bfsFindDir.mockResolvedValue('/root/dir'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce('/root/src/python') + .mockResolvedValueOnce('/root/dir'); mockFileAccess.createFile.mockRejectedValue(new Error('Disk Full')); // Act @@ -121,4 +253,21 @@ describe('CreateUseCaseInteractor', () => { expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); expect(mockPresenter.showFailView).toHaveBeenCalled(); }); + + it('fails if there is no programming language directory', async () => { + // Arrange + mockFileAccess.getCurrentPath.mockResolvedValue('/root'); + mockFileAccess.bfsFindDir + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + + // Act + await interactor.execute(new CreateUseCaseInputData('Test')); + + // Assert + expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); + expect(mockPresenter.showFailView).toHaveBeenCalled(); + }); }); From 0fe9aaaae90fcb1179d730d2f3886f411c8c4e9f Mon Sep 17 00:00:00 2001 From: Jayden Ong Date: Sun, 19 Jul 2026 17:45:28 -0400 Subject: [PATCH 4/7] fix/testChanged endpoints in the front to contact the updated backend endpoints: --- .../frontend/src/actions/useTemplate.ts | 4 +- .../frontend/src/api/template.api.ts | 8 +- .../src/pages/ProjectStarter/index.tsx | 3 +- .../tests/unit/actions/useTemplate.test.tsx | 150 +++++++++++++++++- .../tests/unit/api/template.api.test.ts | 95 ++++++++++- .../tests/unit/pages/ProjectStarter.test.tsx | 4 +- 6 files changed, 243 insertions(+), 21 deletions(-) diff --git a/clean-architecture-visualizer/frontend/src/actions/useTemplate.ts b/clean-architecture-visualizer/frontend/src/actions/useTemplate.ts index 239852c..572837c 100644 --- a/clean-architecture-visualizer/frontend/src/actions/useTemplate.ts +++ b/clean-architecture-visualizer/frontend/src/actions/useTemplate.ts @@ -11,7 +11,7 @@ export const useGenerateProject = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: generateProject, + mutationFn: (language: string) => generateProject(language), onSuccess: () => { // invalidate data here queryClient.invalidateQueries({ queryKey: ['file-tree'] }); @@ -23,7 +23,7 @@ export const useGenerateModuleProject = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: generateModuleProject, + mutationFn: (language: string) => generateModuleProject(language), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['file-tree'] }); }, diff --git a/clean-architecture-visualizer/frontend/src/api/template.api.ts b/clean-architecture-visualizer/frontend/src/api/template.api.ts index 54e08d6..9c8093e 100644 --- a/clean-architecture-visualizer/frontend/src/api/template.api.ts +++ b/clean-architecture-visualizer/frontend/src/api/template.api.ts @@ -3,9 +3,9 @@ import apiClient from './apiClient'; /** * Initiates the base folder structure for a Clean Architecture project. */ -export const generateProject = async () => { +export const generateProject = async (language: string) => { const { data } = await apiClient.post<{ message: string }>( - '/template/generate' + `/template/generate/${encodeURIComponent(language)}` ); return data; }; @@ -13,9 +13,9 @@ export const generateProject = async () => { /** * Initiates the base folder structure for a Clean Architecture project packaged by module. */ -export const generateModuleProject = async () => { +export const generateModuleProject = async (language: string) => { const { data } = await apiClient.post<{ message: string }>( - '/template/module_generate' + `/template/module_generate/${encodeURIComponent(language)}` ); return data; }; diff --git a/clean-architecture-visualizer/frontend/src/pages/ProjectStarter/index.tsx b/clean-architecture-visualizer/frontend/src/pages/ProjectStarter/index.tsx index b5b0832..1d67948 100644 --- a/clean-architecture-visualizer/frontend/src/pages/ProjectStarter/index.tsx +++ b/clean-architecture-visualizer/frontend/src/pages/ProjectStarter/index.tsx @@ -46,8 +46,9 @@ const ProjectStarter = () => { const handleCloseSnackbar = () => setSnackbar({ ...snackbar, open: false }); + // The 'java' is temporary until an option to select programming language is added const handleCreateProject = () => { - triggerGenerate(undefined, { + triggerGenerate('java', { onSuccess: (data) => { setSnackbar({ open: true, diff --git a/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx b/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx index 93d6511..88c05de 100644 --- a/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx +++ b/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx @@ -16,13 +16,13 @@ describe('Template Hooks', () => { vi.restoreAllMocks(); }); - it('useGenerateProject resolves the template response and invalidates file-tree queries', async () => { + it('useGenerateProject resolves the template response and invalidates file-tree queries for java', async () => { const invalidateSpy = vi .spyOn(QueryClient.prototype, 'invalidateQueries') .mockResolvedValue(undefined); server.use( - http.post('*/api/template/generate', () => { + http.post(`*/api/template/generate/${encodeURIComponent('java')}`, () => { return HttpResponse.json( { message: 'Project initiated successfully' }, { status: 201 } @@ -32,20 +32,158 @@ describe('Template Hooks', () => { const { result } = renderHook(() => useGenerateProject()); - result.current.mutate(); + result.current.mutate('java'); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.message).toBe('Project initiated successfully'); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['file-tree'] }); }); - it('useGenerateModuleProject resolves the template response and invalidates file-tree queries', async () => { + it('useGenerateProject resolves the template response and invalidates file-tree queries for python', async () => { const invalidateSpy = vi .spyOn(QueryClient.prototype, 'invalidateQueries') .mockResolvedValue(undefined); server.use( - http.post('*/api/template/module_generate', () => { + http.post(`*/api/template/generate/${encodeURIComponent('python')}`, () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + }) + ); + + const { result } = renderHook(() => useGenerateProject()); + + result.current.mutate('python'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.message).toBe('Project initiated successfully'); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['file-tree'] }); + }); + + it('useGenerateProject resolves the template response and invalidates file-tree queries for javascript', async () => { + const invalidateSpy = vi + .spyOn(QueryClient.prototype, 'invalidateQueries') + .mockResolvedValue(undefined); + + server.use( + http.post(`*/api/template/generate/${encodeURIComponent('javascript')}`, () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + }) + ); + + const { result } = renderHook(() => useGenerateProject()); + + result.current.mutate('javascript'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.message).toBe('Project initiated successfully'); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['file-tree'] }); + }); + + it('useGenerateProject resolves the template response and invalidates file-tree queries for typescript', async () => { + const invalidateSpy = vi + .spyOn(QueryClient.prototype, 'invalidateQueries') + .mockResolvedValue(undefined); + + server.use( + http.post(`*/api/template/generate/${encodeURIComponent('typescript')}`, () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + }) + ); + + const { result } = renderHook(() => useGenerateProject()); + + result.current.mutate('typescript'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.message).toBe('Project initiated successfully'); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['file-tree'] }); + }); + + it('useGenerateModuleProject resolves the template response and invalidates file-tree queries for java', async () => { + const invalidateSpy = vi + .spyOn(QueryClient.prototype, 'invalidateQueries') + .mockResolvedValue(undefined); + + server.use( + http.post(`*/api/template/module_generate/${encodeURIComponent('java')}`, () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + }) + ); + + const { result } = renderHook(() => useGenerateModuleProject()); + + result.current.mutate('java'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.message).toBe('Project initiated successfully'); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['file-tree'] }); + }); + + it('useGenerateModuleProject resolves the template response and invalidates file-tree queries for python', async () => { + const invalidateSpy = vi + .spyOn(QueryClient.prototype, 'invalidateQueries') + .mockResolvedValue(undefined); + + server.use( + http.post(`*/api/template/module_generate/${encodeURIComponent('python')}`, () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + }) + ); + + const { result } = renderHook(() => useGenerateModuleProject()); + + result.current.mutate('python'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.message).toBe('Project initiated successfully'); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['file-tree'] }); + }); + + it('useGenerateModuleProject resolves the template response and invalidates file-tree queries for javascript', async () => { + const invalidateSpy = vi + .spyOn(QueryClient.prototype, 'invalidateQueries') + .mockResolvedValue(undefined); + + server.use( + http.post(`*/api/template/module_generate/${encodeURIComponent('javascript')}`, () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + }) + ); + + const { result } = renderHook(() => useGenerateModuleProject()); + + result.current.mutate('javascript'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.message).toBe('Project initiated successfully'); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['file-tree'] }); + }); + + it('useGenerateModuleProject resolves the template response and invalidates file-tree queries for typescript', async () => { + const invalidateSpy = vi + .spyOn(QueryClient.prototype, 'invalidateQueries') + .mockResolvedValue(undefined); + + server.use( + http.post(`*/api/template/module_generate/${encodeURIComponent('typescript')}`, () => { return HttpResponse.json( { message: 'Project initiated successfully' }, { status: 201 } @@ -55,7 +193,7 @@ describe('Template Hooks', () => { const { result } = renderHook(() => useGenerateModuleProject()); - result.current.mutate(); + result.current.mutate('typescript'); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.message).toBe('Project initiated successfully'); diff --git a/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts b/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts index 248ed94..f65e26d 100644 --- a/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts +++ b/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts @@ -10,30 +10,113 @@ import { } from '@/api/template.api'; describe('Template API', () => { - it('generateProject posts to /template/generate and returns the response data', async () => { + it('generateProject posts to /template/generate/java and returns the response data', async () => { + const mockData = { message: 'Project initiated successfully' }; + server.use( + http.post(`*/api/template/generate/${encodeURIComponent('java')}`, () => + HttpResponse.json(mockData, { status: 201 }) + ) + ); + + const result = await generateProject('java'); + + expect(result).toEqual(mockData); + }); + + it('generateProject posts to /template/generate/python and returns the response data', async () => { + const mockData = { message: 'Project initiated successfully' }; + + server.use( + http.post(`*/api/template/generate/${encodeURIComponent('python')}`, () => + HttpResponse.json(mockData, { status: 201 }) + ) + ); + + const result = await generateProject('python'); + + expect(result).toEqual(mockData); + }); + + it('generateProject posts to /template/generate/javascript and returns the response data', async () => { + const mockData = { message: 'Project initiated successfully' }; + + server.use( + http.post(`*/api/template/generate/${encodeURIComponent('javascript')}`, () => + HttpResponse.json(mockData, { status: 201 }) + ) + ); + + const result = await generateProject('javascript'); + + expect(result).toEqual(mockData); + }); + + it('generateProject posts to /template/generate/typescript and returns the response data', async () => { + const mockData = { message: 'Project initiated successfully' }; + + server.use( + http.post(`*/api/template/generate/${encodeURIComponent('typescript')}`, () => + HttpResponse.json(mockData, { status: 201 }) + ) + ); + + const result = await generateProject('typescript'); + + expect(result).toEqual(mockData); + }); + + it('generateModuleProject posts to /template/module_generate/java and returns the response data', async () => { + const mockData = { message: 'Project initiated successfully' }; + + server.use( + http.post(`*/api/template/module_generate/${encodeURIComponent('java')}`, () => + HttpResponse.json(mockData, { status: 201 }) + ) + ); + + const result = await generateModuleProject('java'); + + expect(result).toEqual(mockData); + }); + + it('generateModuleProject posts to /template/module_generate/python and returns the response data', async () => { + const mockData = { message: 'Project initiated successfully' }; + + server.use( + http.post(`*/api/template/module_generate/${encodeURIComponent('python')}`, () => + HttpResponse.json(mockData, { status: 201 }) + ) + ); + + const result = await generateModuleProject('python'); + + expect(result).toEqual(mockData); + }); + + it('generateModuleProject posts to /template/module_generate/javascript and returns the response data', async () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post('*/api/template/generate', () => + http.post(`*/api/template/module_generate/${encodeURIComponent('javascript')}`, () => HttpResponse.json(mockData, { status: 201 }) ) ); - const result = await generateProject(); + const result = await generateModuleProject('javascript'); expect(result).toEqual(mockData); }); - it('generateModuleProject posts to /template/module_generate and returns the response data', async () => { + it('generateModuleProject posts to /template/module_generate/typescript and returns the response data', async () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post('*/api/template/module_generate', () => + http.post(`*/api/template/module_generate/${encodeURIComponent('typescript')}`, () => HttpResponse.json(mockData, { status: 201 }) ) ); - const result = await generateModuleProject(); + const result = await generateModuleProject('typescript'); expect(result).toEqual(mockData); }); diff --git a/clean-architecture-visualizer/frontend/tests/unit/pages/ProjectStarter.test.tsx b/clean-architecture-visualizer/frontend/tests/unit/pages/ProjectStarter.test.tsx index 962d473..81f175a 100644 --- a/clean-architecture-visualizer/frontend/tests/unit/pages/ProjectStarter.test.tsx +++ b/clean-architecture-visualizer/frontend/tests/unit/pages/ProjectStarter.test.tsx @@ -51,13 +51,13 @@ describe('ProjectStarter Page', () => { ).toBeInTheDocument(); }); - it('shows a success snackbar after generating a project', async () => { + it('shows a success snackbar after generating a project in java', async () => { render(); fireEvent.click(screen.getByRole('button', { name: 'startNew.button' })); expect(templateMock.generateMutation).toHaveBeenCalledWith( - undefined, + 'java', expect.any(Object) ); expect( From 86895364cea9da334b7c6b5952a625e2d0906ca3 Mon Sep 17 00:00:00 2001 From: Jayden Ong Date: Sun, 19 Jul 2026 17:51:48 -0400 Subject: [PATCH 5/7] Ran prettier on all files --- .../tests/unit/actions/useTemplate.test.tsx | 105 +++++++++++------- .../tests/unit/api/template.api.test.ts | 30 +++-- .../createModuleUseCaseInteractor.ts | 15 +-- .../createUseCase/createUseCaseInteractor.ts | 13 ++- .../initModuleProjectInputBoundary.ts | 6 +- .../initModuleProjectInputData.ts | 14 +-- .../initModuleProjectInteractor.ts | 14 ++- .../initProject/initProjectInputBoundary.ts | 2 +- .../initProject/initProjectInputData.ts | 14 +-- .../initProject/initProjectInteractor.ts | 8 +- .../use_cases/createModuleUseCase.test.ts | 11 +- .../backend/use_cases/createUseCase.test.ts | 3 +- 12 files changed, 137 insertions(+), 98 deletions(-) diff --git a/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx b/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx index 88c05de..0e1f2e9 100644 --- a/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx +++ b/clean-architecture-visualizer/frontend/tests/unit/actions/useTemplate.test.tsx @@ -45,12 +45,15 @@ describe('Template Hooks', () => { .mockResolvedValue(undefined); server.use( - http.post(`*/api/template/generate/${encodeURIComponent('python')}`, () => { - return HttpResponse.json( - { message: 'Project initiated successfully' }, - { status: 201 } - ); - }) + http.post( + `*/api/template/generate/${encodeURIComponent('python')}`, + () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + } + ) ); const { result } = renderHook(() => useGenerateProject()); @@ -68,12 +71,15 @@ describe('Template Hooks', () => { .mockResolvedValue(undefined); server.use( - http.post(`*/api/template/generate/${encodeURIComponent('javascript')}`, () => { - return HttpResponse.json( - { message: 'Project initiated successfully' }, - { status: 201 } - ); - }) + http.post( + `*/api/template/generate/${encodeURIComponent('javascript')}`, + () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + } + ) ); const { result } = renderHook(() => useGenerateProject()); @@ -91,12 +97,15 @@ describe('Template Hooks', () => { .mockResolvedValue(undefined); server.use( - http.post(`*/api/template/generate/${encodeURIComponent('typescript')}`, () => { - return HttpResponse.json( - { message: 'Project initiated successfully' }, - { status: 201 } - ); - }) + http.post( + `*/api/template/generate/${encodeURIComponent('typescript')}`, + () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + } + ) ); const { result } = renderHook(() => useGenerateProject()); @@ -114,12 +123,15 @@ describe('Template Hooks', () => { .mockResolvedValue(undefined); server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('java')}`, () => { - return HttpResponse.json( - { message: 'Project initiated successfully' }, - { status: 201 } - ); - }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('java')}`, + () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + } + ) ); const { result } = renderHook(() => useGenerateModuleProject()); @@ -137,12 +149,15 @@ describe('Template Hooks', () => { .mockResolvedValue(undefined); server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('python')}`, () => { - return HttpResponse.json( - { message: 'Project initiated successfully' }, - { status: 201 } - ); - }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('python')}`, + () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + } + ) ); const { result } = renderHook(() => useGenerateModuleProject()); @@ -160,12 +175,15 @@ describe('Template Hooks', () => { .mockResolvedValue(undefined); server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('javascript')}`, () => { - return HttpResponse.json( - { message: 'Project initiated successfully' }, - { status: 201 } - ); - }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('javascript')}`, + () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + } + ) ); const { result } = renderHook(() => useGenerateModuleProject()); @@ -183,12 +201,15 @@ describe('Template Hooks', () => { .mockResolvedValue(undefined); server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('typescript')}`, () => { - return HttpResponse.json( - { message: 'Project initiated successfully' }, - { status: 201 } - ); - }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('typescript')}`, + () => { + return HttpResponse.json( + { message: 'Project initiated successfully' }, + { status: 201 } + ); + } + ) ); const { result } = renderHook(() => useGenerateModuleProject()); diff --git a/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts b/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts index f65e26d..65d2fcb 100644 --- a/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts +++ b/clean-architecture-visualizer/frontend/tests/unit/api/template.api.test.ts @@ -41,8 +41,9 @@ describe('Template API', () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post(`*/api/template/generate/${encodeURIComponent('javascript')}`, () => - HttpResponse.json(mockData, { status: 201 }) + http.post( + `*/api/template/generate/${encodeURIComponent('javascript')}`, + () => HttpResponse.json(mockData, { status: 201 }) ) ); @@ -55,8 +56,9 @@ describe('Template API', () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post(`*/api/template/generate/${encodeURIComponent('typescript')}`, () => - HttpResponse.json(mockData, { status: 201 }) + http.post( + `*/api/template/generate/${encodeURIComponent('typescript')}`, + () => HttpResponse.json(mockData, { status: 201 }) ) ); @@ -69,8 +71,9 @@ describe('Template API', () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('java')}`, () => - HttpResponse.json(mockData, { status: 201 }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('java')}`, + () => HttpResponse.json(mockData, { status: 201 }) ) ); @@ -83,8 +86,9 @@ describe('Template API', () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('python')}`, () => - HttpResponse.json(mockData, { status: 201 }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('python')}`, + () => HttpResponse.json(mockData, { status: 201 }) ) ); @@ -97,8 +101,9 @@ describe('Template API', () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('javascript')}`, () => - HttpResponse.json(mockData, { status: 201 }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('javascript')}`, + () => HttpResponse.json(mockData, { status: 201 }) ) ); @@ -111,8 +116,9 @@ describe('Template API', () => { const mockData = { message: 'Project initiated successfully' }; server.use( - http.post(`*/api/template/module_generate/${encodeURIComponent('typescript')}`, () => - HttpResponse.json(mockData, { status: 201 }) + http.post( + `*/api/template/module_generate/${encodeURIComponent('typescript')}`, + () => HttpResponse.json(mockData, { status: 201 }) ) ); diff --git a/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts b/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts index 6303516..7590bb6 100644 --- a/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts @@ -24,20 +24,21 @@ export class CreateModuleUseCaseInteractor implements CreateModuleUseCaseInputBo ['java', 'java'], ['javascript', 'js'], ['typescript', 'ts'], - - ]) - for(const [language, ext] of languageToExtension){ - if(await this.fileAccess.bfsFindDir(currPath, language)){ + ]); + for (const [language, ext] of languageToExtension) { + if (await this.fileAccess.bfsFindDir(currPath, language)) { extension = ext; break; } } - if(extension === undefined) { - this.presenter.showFailView('Your project does not have a specified programming language.'); + if (extension === undefined) { + this.presenter.showFailView( + 'Your project does not have a specified programming language.' + ); return; } - + // check if features directory exists const featuresDirectory = await this.fileAccess.bfsFindDir( currPath, diff --git a/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts b/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts index a2dab9e..e96df7b 100644 --- a/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts @@ -26,17 +26,18 @@ export class CreateUseCaseInteractor implements CreateUseCaseInputBoundary { ['java', 'java'], ['javascript', 'js'], ['typescript', 'ts'], - - ]) - for(const [language, ext] of languageToExtension){ - if(await this.fileAccess.bfsFindDir(currPath, language)){ + ]); + for (const [language, ext] of languageToExtension) { + if (await this.fileAccess.bfsFindDir(currPath, language)) { extension = ext; break; } } - if(extension === undefined) { - this.presenter.showFailView('Your project does not have a specified programming language.'); + if (extension === undefined) { + this.presenter.showFailView( + 'Your project does not have a specified programming language.' + ); return; } diff --git a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts index 10b976f..e053cea 100644 --- a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts +++ b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputBoundary.ts @@ -1,5 +1,7 @@ -import { InitModuleProjectInputData } from "./initModuleProjectInputData.js"; +import { InitModuleProjectInputData } from './initModuleProjectInputData.js'; export interface InitModuleProjectInputBoundary { - execute(initModuleProjectInputData: InitModuleProjectInputData): Promise; + execute( + initModuleProjectInputData: InitModuleProjectInputData + ): Promise; } diff --git a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts index 89746ac..8307876 100644 --- a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts +++ b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInputData.ts @@ -1,7 +1,7 @@ -export class InitModuleProjectInputData { - constructor(private readonly language: string){} - - getLanguage(): string { - return this.language; - } -} \ No newline at end of file +export class InitModuleProjectInputData { + constructor(private readonly language: string) {} + + getLanguage(): string { + return this.language; + } +} diff --git a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts index 4cdfdba..95e9956 100644 --- a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts @@ -15,14 +15,16 @@ export class InitModuleProjectInteractor implements InitModuleProjectInputBounda this.outputData = outputData; } - async execute(initModuleProjectInputData: InitModuleProjectInputData): Promise { + async execute( + initModuleProjectInputData: InitModuleProjectInputData + ): Promise { try { const acceptedLanguage = ['typescript', 'javascript', 'java', 'python']; const language = initModuleProjectInputData.getLanguage(); - if(!acceptedLanguage.includes(language.toLowerCase())){ + if (!acceptedLanguage.includes(language.toLowerCase())) { throw new Error('You must enter a valid programming language.'); } - + let currPath = await this.fileAccess.getCurrentPath(); currPath = path.join(currPath, 'src'); // Create the src directory if it does not exist. @@ -39,7 +41,11 @@ export class InitModuleProjectInteractor implements InitModuleProjectInputBounda } // The project does support TS, JS, Python, and Java - const programmingPath = path.join(currPath, 'main', language.toLowerCase()); + const programmingPath = path.join( + currPath, + 'main', + language.toLowerCase() + ); const testPath = path.join(currPath, 'test', language.toLowerCase()); await this.fileAccess.createDirectory(programmingPath); await this.fileAccess.createDirectory(testPath); diff --git a/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts index f9df22d..7e68bcf 100644 --- a/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts +++ b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputBoundary.ts @@ -1,4 +1,4 @@ -import { InitProjectInputData } from "./initProjectInputData.js"; +import { InitProjectInputData } from './initProjectInputData.js'; export interface InitProjectInputBoundary { execute(initProjectInputData: InitProjectInputData): Promise; diff --git a/clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts index af3798d..6356e96 100644 --- a/clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts +++ b/clean-architecture-visualizer/src/use_case/initProject/initProjectInputData.ts @@ -1,7 +1,7 @@ -export class InitProjectInputData { - constructor(private readonly language: string){} - - getLanguage(): string { - return this.language; - } -} \ No newline at end of file +export class InitProjectInputData { + constructor(private readonly language: string) {} + + getLanguage(): string { + return this.language; + } +} diff --git a/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts b/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts index f7f46d1..2bd49e7 100644 --- a/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts @@ -19,7 +19,7 @@ export class InitProjectInteractor implements InitProjectInputBoundary { try { const acceptedLanguage = ['typescript', 'javascript', 'java', 'python']; const language = initProjectInputData.getLanguage(); - if(!acceptedLanguage.includes(language.toLowerCase())){ + if (!acceptedLanguage.includes(language.toLowerCase())) { throw new Error('You must enter a valid programming language.'); } @@ -39,7 +39,11 @@ export class InitProjectInteractor implements InitProjectInputBoundary { } // 1. Define base paths using path.join for cross-platform support - const programmingPath = path.join(currPath, 'main', language.toLowerCase()); + const programmingPath = path.join( + currPath, + 'main', + language.toLowerCase() + ); const testPath = path.join(currPath, 'test', language.toLowerCase()); // 2. Define sub-directories within the java path diff --git a/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts index f9b9c89..d8fff55 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts @@ -71,8 +71,7 @@ describe('CreateFeatureInteractor', () => { it('Successfully creates files in specified directories in specified use case in specified feature in python.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue('/root'); - mockFileAccess.bfsFindDir - .mockResolvedValueOnce('/root/src/python'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('/root/src/python'); mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) .mockImplementationOnce( @@ -211,10 +210,10 @@ describe('CreateFeatureInteractor', () => { it('Fails to create files because input feature does not exist.', async () => { mockFileAccess.getCurrentPath.mockResolvedValue('/root'); mockFileAccess.bfsFindDir - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(null) - .mockResolvedValueOnce('root/src/typescript'); + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce('root/src/typescript'); mockFileAccess.bfsFindDir .mockImplementationOnce(async (_, dirName) => `/root/src/${dirName}`) diff --git a/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts index 9bf4df5..1a5e6a2 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts @@ -74,8 +74,7 @@ describe('CreateUseCaseInteractor', () => { it('successfully creates directories and files for a valid use case name in python', async () => { // Arrange mockFileAccess.getCurrentPath.mockResolvedValue('/root'); - mockFileAccess.bfsFindDir - .mockResolvedValueOnce('root/src/python'); + mockFileAccess.bfsFindDir.mockResolvedValueOnce('root/src/python'); mockFileAccess.bfsFindDir.mockImplementation( async (path, dirName) => `/root/src/${dirName}` ); From d3853432bb63f34cf7f60012f9644bc51e6fe0d7 Mon Sep 17 00:00:00 2001 From: Jayden Ong Date: Sun, 19 Jul 2026 18:05:44 -0400 Subject: [PATCH 6/7] docs:Updated docs to match the new feature --- clean-architecture-visualizer/README.md | 18 +++--- docs/users/intro.md | 83 +++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/clean-architecture-visualizer/README.md b/clean-architecture-visualizer/README.md index ad948a4..7d60ecf 100644 --- a/clean-architecture-visualizer/README.md +++ b/clean-architecture-visualizer/README.md @@ -13,15 +13,15 @@ npm install -g clean-architecture-visualizer ## Usage ```bash -cave init # scaffold a new CSC207-style CA project -cave module_init # scaffold a new project, organized by feature module -cave usecase # generate the boilerplate for a new use case -cave feature # add a new feature to the project -cave module_usecase # add a use case to an existing feature -cave verify # check that use cases follow Clean Architecture rules -cave start # start the backend server and frontend visualizer -cave start --backend-only # start only the backend server -cave end # stop the server and clean up temp files +cave init # scaffold a new CSC207-style CA project in the chosen programming language +cave module_init # scaffold a new project, organized by feature module in the chosen language +cave usecase # generate the boilerplate for a new use case +cave feature # add a new feature to the project +cave module_usecase # add a use case to an existing feature +cave verify # check that use cases follow Clean Architecture rules +cave start # start the backend server and frontend visualizer +cave start --backend-only # start only the backend server +cave end # stop the server and clean up temp files ``` `cave start` launches a local web UI for browsing use case diagrams and CA layer violations in your project. diff --git a/docs/users/intro.md b/docs/users/intro.md index 6c008ff..4c03437 100644 --- a/docs/users/intro.md +++ b/docs/users/intro.md @@ -15,12 +15,12 @@ Cave is a CLI (Command Line Interface) tool designed for CSC207 students to help ## Commands -### `cave init` +### `cave init ` -Initializes your CSC207 project with a sample clean architecture directory structure. +Initializes your CSC207 project with a sample clean architecture directory structure in the chosen programming language. ```bash -cave init +cave init ``` The created structure should like like: @@ -39,19 +39,46 @@ src/ └── java/ ``` -Run this first in your project folder. Cave will scaffold the recommended folder layout so you can start developing! These folders will not immediately be tracked by git as they are empty. If you want to +Run this first in your project folder. Cave will scaffold the recommended folder layout so you can start developing! These folders will not immediately be tracked by git as they are empty. + +--- + +### `cave module_init ` + +Initializes your CSC207 project with a sample clean architecture directory structure in the chosen programming language, packaged by module. + +```bash +cave module_init +``` + +The created structure should like like: + +``` +src/ +├── main/ +│ └── java/ +│ ├── app/ +│ ├── entity/ +│ ├── features/ +│ ├── data_access/ +│ └── view/ +└── test/ + └── java/ +``` + +Run this first in your project folder. Cave will scaffold the recommended folder layout so you can start developing! These folders will not immediately be tracked by git as they are empty. --- ### `cave usecase ` -Initializes the folders and Java file structure for a new use case. +Initializes the folders and file structure for a new use case. ```bash cave usecase ``` -Replace `` with the name of your use case. Cave will generate the appropriate directories and boilerplate Java files following clean architecture conventions similar to the structure shown below: +Replace `` with the name of your use case. Cave will generate the appropriate directories and boilerplate files following clean architecture conventions similar to the structure shown below (assume we initialize a Java project): ``` src/main/java/use_case/ @@ -70,6 +97,50 @@ src/main/java/interface_adapter/ --- +### `cave feature ` + +Initializes the folder for a new feature. This only works on projects packaged by module. + +```bash +cave module_usecase +``` + +Replace `` with the name of your feature. Cave will generate the appropriate directory as shown below (assume we initialize a Java project): + +``` +src/main/java/features +└── +``` + +--- + +### `cave module_usecase ` + +Initializes the folders and file structure for a new use case, in the specified feature, in a project packaged by module. + +```bash +cave module_usecase +``` + +Replace `` with the name of your feature. Replace `` with the name of your use case. Cave will generate the appropriate directories and boilerplate files following clean architecture conventions similar to the structure shown below (assume we initialize a Java project): + +``` +src/main/java/features//use_case/ +└── / + ├── InputBoundary.java + ├── InputData.java + ├── Interactor.java + ├── OutputData.java + └── OutputBoundary.java + +src/main/java/features//interface_adapter/ +└── / + ├── Controller.java + └── Presenter.java +``` + +--- + ### `cave start` Starts the front end visualizer so you can explore the interactions in your codebase. From d933e6f918cbadb58bcdb6eaf4798b7fd4899f06 Mon Sep 17 00:00:00 2001 From: Jayden Ong Date: Wed, 22 Jul 2026 12:57:45 -0400 Subject: [PATCH 7/7] feat/tests: Changed init command to make it create a java project if nothing is entered --- clean-architecture-visualizer/src/app/index.ts | 8 ++++---- .../createModuleUseCase/createModuleUseCaseInteractor.ts | 8 +++++--- .../src/use_case/createUseCase/createUseCaseInteractor.ts | 6 ++++-- .../initModuleProject/initModuleProjectInteractor.ts | 6 ++++-- .../src/use_case/initProject/initProjectInteractor.ts | 6 ++++-- .../tests/backend/use_cases/createModuleUseCase.test.ts | 6 +++--- .../tests/backend/use_cases/createUseCase.test.ts | 2 +- 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/clean-architecture-visualizer/src/app/index.ts b/clean-architecture-visualizer/src/app/index.ts index d805d5d..a8f82a0 100755 --- a/clean-architecture-visualizer/src/app/index.ts +++ b/clean-architecture-visualizer/src/app/index.ts @@ -64,18 +64,18 @@ program }); program - .command('init ') + .command('init [language]') .description('Create the template for a new CSC207 project') - .action(async (language: string) => { + .action(async (language: string = 'java') => { app.runInitProject(language); }); program - .command('module_init ') + .command('module_init [language]') .description( 'Create the template for a new CSC207 project, packaged by module.' ) - .action(async (language: string) => { + .action(async (language: string = 'java') => { app.runInitModuleProject(language); }); diff --git a/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts b/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts index 7590bb6..3b1148d 100644 --- a/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/createModuleUseCase/createModuleUseCaseInteractor.ts @@ -34,7 +34,7 @@ export class CreateModuleUseCaseInteractor implements CreateModuleUseCaseInputBo if (extension === undefined) { this.presenter.showFailView( - 'Your project does not have a specified programming language.' + 'Your project does not have a specified programming language. You must create a directory that has the name: java, python, typescript, or javascript.' ); return; } @@ -58,14 +58,16 @@ export class CreateModuleUseCaseInteractor implements CreateModuleUseCaseInputBo ); if (!currFeatureDirectory) { this.presenter.showFailView( - 'The input feature does not exist in the features directory.' + 'The input feature does not exist in the features directory. Please choose a feature that does exist or create this feature.' ); return; } // check if usecase already exists. if (await this.fileAccess.bfsFindDir(featuresDirectory, usecase)) { - this.presenter.showFailView('The input usecase already exists.'); + this.presenter.showFailView( + 'The input usecase already exists. Please choose a different name.' + ); return; } diff --git a/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts b/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts index e96df7b..a31f79a 100644 --- a/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/createUseCase/createUseCaseInteractor.ts @@ -36,7 +36,7 @@ export class CreateUseCaseInteractor implements CreateUseCaseInputBoundary { if (extension === undefined) { this.presenter.showFailView( - 'Your project does not have a specified programming language.' + 'Your project does not have a specified programming language. You must create a directory that has the name: java, python, typescript, or javascript.' ); return; } @@ -62,7 +62,9 @@ export class CreateUseCaseInteractor implements CreateUseCaseInputBoundary { const useCaseExists = await this.fileAccess.exists(targetUseCasePath); const interfaceExists = await this.fileAccess.exists(targetInterfacePath); if (useCaseExists || interfaceExists) { - this.presenter.showFailView(`Usecase ${useCaseName} already exists.`); + this.presenter.showFailView( + `Usecase ${useCaseName} already exists. Please choose a different name.` + ); return; } diff --git a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts index 95e9956..57665fa 100644 --- a/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/initModuleProject/initModuleProjectInteractor.ts @@ -20,9 +20,11 @@ export class InitModuleProjectInteractor implements InitModuleProjectInputBounda ): Promise { try { const acceptedLanguage = ['typescript', 'javascript', 'java', 'python']; - const language = initModuleProjectInputData.getLanguage(); + let language = initModuleProjectInputData.getLanguage().trim(); if (!acceptedLanguage.includes(language.toLowerCase())) { - throw new Error('You must enter a valid programming language.'); + throw new Error( + 'You must enter a valid programming language of: java, python, typescript, or javascript. Blank defaults to Java.' + ); } let currPath = await this.fileAccess.getCurrentPath(); diff --git a/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts b/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts index 2bd49e7..d3f8a61 100644 --- a/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts +++ b/clean-architecture-visualizer/src/use_case/initProject/initProjectInteractor.ts @@ -18,9 +18,11 @@ export class InitProjectInteractor implements InitProjectInputBoundary { async execute(initProjectInputData: InitProjectInputData): Promise { try { const acceptedLanguage = ['typescript', 'javascript', 'java', 'python']; - const language = initProjectInputData.getLanguage(); + let language = initProjectInputData.getLanguage().trim(); if (!acceptedLanguage.includes(language.toLowerCase())) { - throw new Error('You must enter a valid programming language.'); + throw new Error( + 'You must enter a valid programming language of: java, python, typescript, or javascript. Blank defaults to Java.' + ); } let currPath = await this.fileAccess.getCurrentPath(); diff --git a/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts index d8fff55..aea2b78 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/createModuleUseCase.test.ts @@ -225,7 +225,7 @@ describe('CreateFeatureInteractor', () => { await interactor.execute(inputData); expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); expect(mockPresenter.showFailView).toHaveBeenCalledWith( - 'The input feature does not exist in the features directory.' + 'The input feature does not exist in the features directory. Please choose a feature that does exist or create this feature.' ); }); @@ -247,7 +247,7 @@ describe('CreateFeatureInteractor', () => { await interactor.execute(inputData); expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); expect(mockPresenter.showFailView).toHaveBeenCalledWith( - 'The input usecase already exists.' + 'The input usecase already exists. Please choose a different name.' ); }); it('Fails to create files because use case already exists in another directory.', async () => { @@ -268,7 +268,7 @@ describe('CreateFeatureInteractor', () => { await interactor.execute(inputData); expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); expect(mockPresenter.showFailView).toHaveBeenCalledWith( - 'The input usecase already exists.' + 'The input usecase already exists. Please choose a different name.' ); }); diff --git a/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts b/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts index 1a5e6a2..febada7 100644 --- a/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts +++ b/clean-architecture-visualizer/tests/backend/use_cases/createUseCase.test.ts @@ -230,7 +230,7 @@ describe('CreateUseCaseInteractor', () => { // Assert expect(mockPresenter.showSuccessView).not.toHaveBeenCalled(); expect(mockPresenter.showFailView).toHaveBeenCalledWith( - 'Usecase Test already exists.' + 'Usecase Test already exists. Please choose a different name.' ); // Ensure no files or directories were attempted to be created expect(mockFileAccess.createFile).not.toHaveBeenCalled();