Skip to content

Commit 9ff2ef6

Browse files
committed
test(amplify-category-custom): add tests for custom build script support
Add tests for the hasBuildScript() function and the new logic that allows custom resources to define their own build script in package.json. New test cases: - Run install and tsc separately when no build script defined - Run install and tsc when package.json has scripts but no build script - Run build script when package.json has scripts.build defined - Not call install or tsc separately when build script is defined - Fall back to install+tsc when package.json does not exist
1 parent f53d40d commit 9ff2ef6

1 file changed

Lines changed: 119 additions & 14 deletions

File tree

packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { $TSContext } from '@aws-amplify/amplify-cli-core';
1+
import { $TSContext, JSONUtilities } from '@aws-amplify/amplify-cli-core';
22
import execa from 'execa';
3+
import * as fs from 'fs-extra';
34
import { buildCustomResources } from '../../utils/build-custom-resources';
45

56
jest.mock('@aws-amplify/amplify-cli-core');
@@ -8,15 +9,10 @@ jest.mock('../../utils/dependency-management-utils');
89
jest.mock('../../utils/generate-cfn-from-cdk');
910
jest.mock('execa');
1011
jest.mock('ora');
12+
jest.mock('fs-extra');
1113

12-
jest.mock('fs-extra', () => ({
13-
readFileSync: jest.fn().mockReturnValue('mockCode'),
14-
existsSync: jest.fn().mockReturnValue(true),
15-
ensureDirSync: jest.fn().mockReturnValue(true),
16-
ensureDir: jest.fn(),
17-
writeFileSync: jest.fn().mockReturnValue(true),
18-
writeFile: jest.fn(),
19-
}));
14+
const fs_mock = fs as jest.Mocked<typeof fs>;
15+
const JSONUtilities_mock = JSONUtilities as jest.Mocked<typeof JSONUtilities>;
2016

2117
jest.mock('ora', () => () => ({
2218
start: jest.fn(),
@@ -34,7 +30,15 @@ jest.mock('../../utils/generate-cfn-from-cdk', () => ({
3430
}));
3531

3632
jest.mock('@aws-amplify/amplify-cli-core', () => ({
37-
getPackageManager: jest.fn().mockResolvedValue('npm'),
33+
getPackageManager: jest.fn().mockResolvedValue({
34+
packageManager: 'npm',
35+
executable: 'npm',
36+
runner: 'npx',
37+
lockFile: 'package-lock.json',
38+
displayValue: 'NPM',
39+
getRunScriptArgs: jest.fn(),
40+
getInstallArgs: jest.fn(),
41+
}),
3842
pathManager: {
3943
getBackendDirPath: jest.fn().mockReturnValue('mockTargetDir'),
4044
},
@@ -44,13 +48,31 @@ jest.mock('@aws-amplify/amplify-cli-core', () => ({
4448
stringify: jest.fn(),
4549
},
4650
skipHooks: jest.fn().mockReturnValue(false),
51+
AmplifyError: class AmplifyError extends Error {
52+
constructor(name: string, options: { message: string }) {
53+
super(options.message);
54+
this.name = name;
55+
}
56+
},
4757
}));
4858

4959
describe('build custom resources scenarios', () => {
5060
let mockContext: $TSContext;
5161

5262
beforeEach(() => {
5363
jest.clearAllMocks();
64+
65+
// Default fs mocks
66+
(fs_mock.existsSync as jest.Mock).mockReturnValue(true);
67+
(fs_mock.readFileSync as jest.Mock).mockReturnValue('mockCode');
68+
(fs_mock.ensureDirSync as jest.Mock).mockReturnValue(undefined);
69+
(fs_mock.ensureDir as jest.Mock).mockResolvedValue(undefined);
70+
(fs_mock.writeFileSync as jest.Mock).mockReturnValue(undefined);
71+
(fs_mock.writeFile as jest.Mock).mockResolvedValue(undefined);
72+
73+
// Default: no build script in package.json
74+
JSONUtilities_mock.readJson.mockReturnValue({});
75+
5476
mockContext = {
5577
amplify: {
5678
openEditor: jest.fn(),
@@ -72,10 +94,93 @@ describe('build custom resources scenarios', () => {
7294
} as unknown as $TSContext;
7395
});
7496

75-
it('build all resources', async () => {
76-
await buildCustomResources(mockContext);
97+
describe('default behavior (no build script)', () => {
98+
it('should run install and tsc separately when no build script defined', async () => {
99+
// No build script in package.json
100+
JSONUtilities_mock.readJson.mockReturnValue({});
101+
102+
await buildCustomResources(mockContext);
103+
104+
// 2 for npm install and 2 for tsc build (1 per resource)
105+
expect(execa.sync).toBeCalledTimes(4);
106+
107+
// First resource: install then tsc
108+
expect(execa.sync).toHaveBeenNthCalledWith(1, 'npm', ['install'], expect.objectContaining({ stdio: 'pipe' }));
109+
expect(execa.sync).toHaveBeenNthCalledWith(2, 'npx', ['tsc'], expect.objectContaining({ stdio: 'pipe' }));
110+
111+
// Second resource: install then tsc
112+
expect(execa.sync).toHaveBeenNthCalledWith(3, 'npm', ['install'], expect.objectContaining({ stdio: 'pipe' }));
113+
expect(execa.sync).toHaveBeenNthCalledWith(4, 'npx', ['tsc'], expect.objectContaining({ stdio: 'pipe' }));
114+
});
115+
116+
it('should run install and tsc when package.json has scripts but no build script', async () => {
117+
// package.json with scripts but no build script
118+
JSONUtilities_mock.readJson.mockReturnValue({
119+
scripts: {
120+
test: 'jest',
121+
lint: 'eslint .',
122+
},
123+
});
124+
125+
await buildCustomResources(mockContext);
126+
127+
// Should still run install + tsc for each resource
128+
expect(execa.sync).toBeCalledTimes(4);
129+
expect(execa.sync).toHaveBeenNthCalledWith(1, 'npm', ['install'], expect.objectContaining({ stdio: 'pipe' }));
130+
});
131+
});
132+
133+
describe('custom build script behavior', () => {
134+
it('should run build script when package.json has scripts.build defined', async () => {
135+
// package.json with build script
136+
JSONUtilities_mock.readJson.mockReturnValue({
137+
scripts: {
138+
build: 'pnpm install --ignore-workspace && tsc',
139+
},
140+
});
141+
142+
await buildCustomResources(mockContext);
143+
144+
// Only 2 calls (1 per resource) for 'npm run build'
145+
expect(execa.sync).toBeCalledTimes(2);
146+
147+
expect(execa.sync).toHaveBeenNthCalledWith(1, 'npm', ['run', 'build'], expect.objectContaining({ stdio: 'pipe' }));
148+
expect(execa.sync).toHaveBeenNthCalledWith(2, 'npm', ['run', 'build'], expect.objectContaining({ stdio: 'pipe' }));
149+
});
150+
151+
it('should not call install or tsc separately when build script is defined', async () => {
152+
JSONUtilities_mock.readJson.mockReturnValue({
153+
scripts: {
154+
build: 'custom-build-command',
155+
},
156+
});
157+
158+
await buildCustomResources(mockContext);
159+
160+
// Verify install and tsc are NOT called
161+
const calls = (execa.sync as jest.Mock).mock.calls;
162+
const hasInstallCall = calls.some((call) => call[1]?.[0] === 'install');
163+
const hasTscCall = calls.some((call) => call[1]?.[0] === 'tsc');
164+
165+
expect(hasInstallCall).toBe(false);
166+
expect(hasTscCall).toBe(false);
167+
});
168+
});
169+
170+
describe('hasBuildScript edge cases', () => {
171+
it('should fall back to install+tsc when package.json does not exist', async () => {
172+
// package.json doesn't exist for the custom resource
173+
(fs_mock.existsSync as jest.Mock).mockImplementation((filePath: unknown) => {
174+
if (typeof filePath === 'string' && filePath.includes('package.json')) {
175+
return false;
176+
}
177+
return true;
178+
});
179+
180+
await buildCustomResources(mockContext);
77181

78-
// 2 for npm install and 2 for tsc build (1 per resource)
79-
expect(execa.sync).toBeCalledTimes(4);
182+
// Should run install + tsc (default behavior)
183+
expect(execa.sync).toBeCalledTimes(4);
184+
});
80185
});
81186
});

0 commit comments

Comments
 (0)