Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
"codebot.templatesFolderPath": {
"type": "string",
"default": "templates",
"scope": "resource",
"markdownDescription": "Path to the templates folder, relative to the workspace root."
},
"codebot.templateSettings": {
"type": "object",
"default": {},
"scope": "resource",
"markdownDescription": "Per-template settings. Keys must match template folder names.",
"additionalProperties": {
"type": "object",
Expand Down
14 changes: 14 additions & 0 deletions src/__mocks__/vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ const workspace = {
index: 0,
},
],
getWorkspaceFolder(uri) {
const folders = workspace.workspaceFolders;
if (!folders || folders.length === 0) return undefined;
const sep = require('node:path').sep;
return (
[...folders]
.sort((a, b) => b.uri.fsPath.length - a.uri.fsPath.length)
.find(
f =>
uri.fsPath === f.uri.fsPath ||
uri.fsPath.startsWith(f.uri.fsPath + sep),
) ?? undefined
);
},
};

const window = {
Expand Down
120 changes: 120 additions & 0 deletions src/__tests__/commands/createComponent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,126 @@ describe('createComponent', () => {
});
});

describe('multi-root workspace', () => {
const appClickedUri = vscode.Uri.file('/test/packages/app/src/components');
const libClickedUri = vscode.Uri.file('/test/packages/lib/src/components');

beforeEach(() => {
(
vscode.workspace as unknown as Record<string, unknown>
).workspaceFolders = [
{ uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 },
{ uri: { fsPath: '/test/packages/lib' }, name: 'lib', index: 1 },
];

const configFactory = (
_section?: string,
scope?: vscode.ConfigurationScope | null,
): vscode.WorkspaceConfiguration => {
const fsPath =
scope != null && 'fsPath' in scope
? (scope as vscode.Uri).fsPath
: undefined;
return {
get: jest.fn().mockImplementation((key: string) => {
if (key === 'templatesFolderPath') return 'templates';
if (key === 'templateSettings') {
return fsPath?.startsWith('/test/packages/app')
? { ReactPackage: { nameFormat: 'kebab-case' } }
: {};
}
return undefined;
}),
update: jest.fn(),
has: jest.fn(),
inspect: jest.fn(),
} as unknown as vscode.WorkspaceConfiguration;
};

mockGetConfiguration.mockImplementation(configFactory);
});

afterEach(() => {
(
vscode.workspace as unknown as Record<string, unknown>
).workspaceFolders = [
{ uri: { fsPath: '/test/workspace' }, name: 'workspace', index: 0 },
];
});

const setupTemplate = () => {
mockFs.readDirectory
.mockResolvedValueOnce([['ReactPackage', 2]] as never)
.mockResolvedValueOnce([['index.hbs', 1]] as never);
mockFs.readFile.mockResolvedValue(encode('// {{name}}') as never);
mockFs.stat.mockRejectedValue(new Error('not found'));
mockFs.createDirectory.mockResolvedValue(undefined);
mockFs.writeFile.mockResolvedValue(undefined);
};

it('applies nameFormat from the workspace folder containing the clicked path', async () => {
setupTemplate();
mockWindow.showInputBox.mockResolvedValue('my-package');

await createComponent(appClickedUri);

expect(mockWindow.showInformationMessage).toHaveBeenCalledWith(
expect.stringContaining("'my-package' created"),
);
});

it('applies a different config when clicking inside a different workspace folder', async () => {
setupTemplate();
mockWindow.showInputBox.mockResolvedValue('my-package');

await createComponent(libClickedUri);

// lib has no nameFormat → falls back to pascal-case
expect(mockWindow.showInformationMessage).toHaveBeenCalledWith(
expect.stringContaining("'MyPackage' created"),
);
});

it('passes clickedUri to getConfiguration, not the workspace folder root', async () => {
setupTemplate();
mockWindow.showInputBox.mockResolvedValue('my-package');

await createComponent(appClickedUri);

expect(mockGetConfiguration).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ fsPath: appClickedUri.fsPath }),
);
});

it('does not confuse a folder whose name is a prefix of another folder', async () => {
(
vscode.workspace as unknown as Record<string, unknown>
).workspaceFolders = [
{ uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 },
{
uri: { fsPath: '/test/packages/application' },
name: 'application',
index: 1,
},
];

const applicationUri = vscode.Uri.file(
'/test/packages/application/src/components',
);
setupTemplate();
mockWindow.showInputBox.mockResolvedValue('my-package');

await createComponent(applicationUri);

// templates must be looked up under /test/packages/application, not /test/packages/app
const templatesDirCall = mockFs.readDirectory.mock
.calls[0][0] as vscode.Uri;
expect(templatesDirCall.fsPath).toContain('/test/packages/application');
expect(templatesDirCall.fsPath).not.toMatch(/\/test\/packages\/app\b/);
});
});

describe('nameFormat per template', () => {
const setupTemplateWithFormat = (
templateName: string,
Expand Down
121 changes: 121 additions & 0 deletions src/__tests__/commands/updateComponent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,127 @@ describe('updateComponent', () => {
});
});

describe('multi-root workspace', () => {
const appClickedUri = vscode.Uri.file(
'/test/packages/app/src/components/MyButton',
);
const libClickedUri = vscode.Uri.file(
'/test/packages/lib/src/components/MyButton',
);

beforeEach(() => {
(
vscode.workspace as unknown as Record<string, unknown>
).workspaceFolders = [
{ uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 },
{ uri: { fsPath: '/test/packages/lib' }, name: 'lib', index: 1 },
];

const configFactory = (
_section?: string,
scope?: vscode.ConfigurationScope | null,
): vscode.WorkspaceConfiguration => {
const fsPath =
scope != null && 'fsPath' in scope
? (scope as vscode.Uri).fsPath
: undefined;
return {
get: jest.fn().mockImplementation((key: string) => {
if (key === 'templatesFolderPath') return 'templates';
if (key === 'templateSettings') {
return fsPath?.startsWith('/test/packages/app')
? { ReactPackage: { nameFormat: 'kebab-case' } }
: {};
}
return undefined;
}),
update: jest.fn(),
has: jest.fn(),
inspect: jest.fn(),
} as unknown as vscode.WorkspaceConfiguration;
};

mockGetConfiguration.mockImplementation(configFactory);
});

afterEach(() => {
(
vscode.workspace as unknown as Record<string, unknown>
).workspaceFolders = [
{ uri: { fsPath: '/test/workspace' }, name: 'workspace', index: 0 },
];
});

const setupTemplate = () => {
mockFs.readDirectory
.mockResolvedValueOnce([['ReactPackage', 2]] as never)
.mockResolvedValueOnce([['index.hbs', 1]] as never);
mockFs.readFile.mockResolvedValue(encode('// {{name}}') as never);
mockFs.stat.mockRejectedValue(new Error('not found'));
mockFs.createDirectory.mockResolvedValue(undefined);
mockFs.writeFile.mockResolvedValue(undefined);
};

it('applies nameFormat from the workspace folder containing the clicked path', async () => {
setupTemplate();

await updateComponent(appClickedUri);

// app has kebab-case → 'MyButton' becomes 'my-button'
expect(mockWindow.showInformationMessage).toHaveBeenCalledWith(
expect.stringContaining("'my-button' updated"),
);
});

it('applies a different config when clicking inside a different workspace folder', async () => {
setupTemplate();

await updateComponent(libClickedUri);

// lib has no nameFormat → falls back to pascal-case → 'MyButton' stays
expect(mockWindow.showInformationMessage).toHaveBeenCalledWith(
expect.stringContaining("'MyButton' updated"),
);
});

it('passes clickedUri to getConfiguration, not the workspace folder root', async () => {
setupTemplate();

await updateComponent(appClickedUri);

expect(mockGetConfiguration).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ fsPath: appClickedUri.fsPath }),
);
});

it('does not confuse a folder whose name is a prefix of another folder', async () => {
(
vscode.workspace as unknown as Record<string, unknown>
).workspaceFolders = [
{ uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 },
{
uri: { fsPath: '/test/packages/application' },
name: 'application',
index: 1,
},
];

const applicationUri = vscode.Uri.file(
'/test/packages/application/src/components/MyButton',
);
setupTemplate();

await updateComponent(applicationUri);

// templates must be looked up under /test/packages/application, not /test/packages/app
const templatesDirCall = mockFs.readDirectory.mock
.calls[0][0] as vscode.Uri;
expect(templatesDirCall.fsPath).toContain('/test/packages/application');
expect(templatesDirCall.fsPath).not.toMatch(/\/test\/packages\/app\b/);
});
});

describe('nameFormat per template', () => {
const setupTemplateWithFormat = (
templateName: string,
Expand Down
73 changes: 0 additions & 73 deletions src/__tests__/helpers/fs/createFile.test.ts

This file was deleted.

Loading
Loading