Skip to content

Commit d4e4ed7

Browse files
committed
more tests, publish logging
1 parent 0f37dbc commit d4e4ed7

14 files changed

Lines changed: 876 additions & 58 deletions

File tree

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"runtimeExecutable": "npm",
1212
"cwd": "${workspaceFolder}",
1313
"runtimeArgs": ["run-script", "test"],
14-
"args": ["--", "--runInBand", "--watch", "--testTimeout=1000000", "${fileBasenameNoExtension}"],
14+
"args": ["--", "--runInBand", "--watch", "--testTimeout=1000000", "${file}"],
1515
"sourceMaps": true,
1616
"outputCapture": "std",
1717
"console": "integratedTerminal",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Refine publish logging",
4+
"packageName": "beachball",
5+
"email": "elcraig@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

src/__e2e__/publishE2E.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ describe('publish command (e2e)', () => {
8888
generateChangeFiles(['foo'], options);
8989
repo.push();
9090

91-
let fetchCount = 0;
9291
addGitObserver(args => {
93-
args[0] === 'fetch' && fetchCount++;
92+
// no fetch when flag set to false
93+
expect(args[0]).not.toBe('fetch');
9494
});
9595

9696
await publishWrapper(parsedOptions);
@@ -101,9 +101,6 @@ describe('publish command (e2e)', () => {
101101
'dist-tags': { latest: '1.1.0' },
102102
});
103103

104-
// no fetch when flag set to false
105-
expect(fetchCount).toBe(0);
106-
107104
repo.checkout(defaultBranchName);
108105
repo.pull();
109106
expect(repo.getCurrentTags()).toEqual(['foo_v1.1.0']);

src/__e2e__/publishGit.test.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { getParsedOptions } from '../options/getOptions';
1616
import { readJson } from '../object/readJson';
1717
import { createCommandContext } from '../monorepo/createCommandContext';
1818
import type { RepoOptions } from '../types/BeachballOptions';
19-
import { addGitObserver, clearGitObservers } from 'workspace-tools';
2019

2120
describe('publish command (git)', () => {
2221
let repositoryFactory: RepositoryFactory | undefined;
@@ -44,7 +43,6 @@ describe('publish command (git)', () => {
4443
}
4544

4645
afterEach(() => {
47-
clearGitObservers();
4846
repositoryFactory?.cleanUp();
4947
repositoryFactory = undefined;
5048
repo = undefined;
@@ -129,26 +127,4 @@ describe('publish command (git)', () => {
129127
// changes from publish process were committed
130128
expect(fs.existsSync(txtPath)).toBe(true);
131129
});
132-
133-
it('specifies fetch depth when depth param is defined', async () => {
134-
repositoryFactory = new RepositoryFactory('single');
135-
repo = repositoryFactory.cloneRepository();
136-
137-
const { options, parsedOptions } = getOptions({
138-
depth: 10,
139-
});
140-
141-
generateChangeFiles(['foo'], options);
142-
repo.push();
143-
144-
const gitObserver = jest.fn((args: string[]) => {
145-
if (args[0] === 'fetch') {
146-
expect(args).toContain('--depth=10');
147-
}
148-
});
149-
addGitObserver(gitObserver);
150-
151-
await publish(options, createCommandContext(parsedOptions));
152-
expect(gitObserver).toHaveBeenCalled();
153-
});
154130
});

src/__fixtures__/mockLogs.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { jest, afterEach, beforeAll, afterAll } from '@jest/globals';
1+
import { jest, afterEach, beforeAll, afterAll, beforeEach } from '@jest/globals';
22

33
/** Methods that will be mocked. More could be added later if needed. */
44
type MockLogMethod = 'log' | 'warn' | 'error';
@@ -10,6 +10,12 @@ type MockLogsOptions = {
1010
* All logging can be enabled by setting the VERBOSE env var.
1111
*/
1212
alsoLog?: boolean | MockLogMethod[];
13+
14+
/**
15+
* Instead of setting up mocks in `beforeAll`, do it in `beforeEach`.
16+
* This allows calling `jest.resetAllMocks()` in tests without breaking the logging mocks.
17+
*/
18+
mockBeforeEach?: boolean;
1319
};
1420

1521
export type MockLogs = {
@@ -49,7 +55,7 @@ export type MockLogs = {
4955
* of any lifecycle hooks or tests because it calls lifecycle hooks internally for setup and teardown.
5056
*/
5157
export function initMockLogs(options: MockLogsOptions = {}): MockLogs {
52-
const { alsoLog } = options;
58+
const { alsoLog, mockBeforeEach } = options;
5359
let allLines: unknown[][] = [];
5460
let overrideOptions: MockLogsOptions | undefined;
5561
const jestConsole = { ...console };
@@ -102,7 +108,7 @@ export function initMockLogs(options: MockLogsOptions = {}): MockLogs {
102108
},
103109
};
104110

105-
beforeAll(() => {
111+
(mockBeforeEach ? beforeEach : beforeAll)(() => {
106112
for (const method of mockedMethods) {
107113
const mainShouldLog = shouldLog(method, alsoLog);
108114

@@ -121,7 +127,7 @@ export function initMockLogs(options: MockLogsOptions = {}): MockLogs {
121127
logs.clear();
122128
});
123129

124-
afterAll(() => {
130+
(mockBeforeEach ? afterEach : afterAll)(() => {
125131
Object.values(logs.mocks).forEach(mock => mock.mockRestore());
126132
});
127133

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`publish command logs expected output for a standard publish flow 1`] = `
4+
"[log]
5+
Validating options and change files...
6+
[log]
7+
Validating package dependencies...
8+
[log] Validating no private package among package dependencies
9+
[log] OK!
10+
11+
[log]
12+
[log] Preparing to publish
13+
14+
[log] Publishing with the following configuration:
15+
16+
registry: fake
17+
18+
current branch: master
19+
current hash: <commit>
20+
target branch: origin/master
21+
npm dist-tag: latest
22+
23+
bumps versions before publishing: yes
24+
publishes to npm registry: yes
25+
pushes bumps and changelogs to remote git repo: no
26+
27+
28+
[log] Creating temporary publish branch publish_<timestamp>
29+
30+
[log] Bumping versions and publishing packages to npm registry
31+
32+
[log]
33+
[log] Skipping git push and tagging
34+
35+
[log] Cleaning up
36+
37+
[log] git checkout master
38+
[log] deleting temporary publish branch publish_<timestamp>"
39+
`;
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
2+
import { getBranchName, getCurrentHash } from 'workspace-tools';
3+
import { generateChangeFiles } from '../../__fixtures__/changeFiles';
4+
import { defaultRemoteBranchName } from '../../__fixtures__/gitDefaults';
5+
import { initMockLogs } from '../../__fixtures__/mockLogs';
6+
import { deepFreezeProperties } from '../../__fixtures__/object';
7+
import type { Repository } from '../../__fixtures__/repository';
8+
import { RepositoryFactory } from '../../__fixtures__/repositoryFactory';
9+
import { bumpInMemory } from '../../bump/bumpInMemory';
10+
import { publish } from '../../commands/publish';
11+
import { createCommandContext } from '../../monorepo/createCommandContext';
12+
import { getParsedOptions } from '../../options/getOptions';
13+
import { bumpAndPush } from '../../publish/bumpAndPush';
14+
import { publishToRegistry } from '../../publish/publishToRegistry';
15+
import type { ParsedOptions, RepoOptions } from '../../types/BeachballOptions';
16+
import { validate } from '../../validation/validate';
17+
import { getNewPackages } from '../../publish/getNewPackages';
18+
19+
//
20+
// These tests focus on functionality of the publish() function itself, not its major helpers
21+
// such as publishToRegistry or bumpAndPush, which have their own dedicated tests.
22+
// Also, basics like logging and verifying which child functions are called per options should be
23+
// covered in the unit test publish.mock.test.ts, since tests using git are more expensive.
24+
//
25+
jest.mock('../../publish/publishToRegistry');
26+
jest.mock('../../publish/bumpAndPush');
27+
jest.mock('../../publish/getNewPackages');
28+
29+
describe('publish command', () => {
30+
// this test uses resetAllMocks, so mock the log methods in beforeEach
31+
const logs = initMockLogs({ mockBeforeEach: true });
32+
33+
const mockPublishToRegistry = publishToRegistry as jest.MockedFunction<typeof publishToRegistry>;
34+
const mockBumpAndPush = bumpAndPush as jest.MockedFunction<typeof bumpAndPush>;
35+
const mockGetNewPackages = getNewPackages as jest.MockedFunction<typeof getNewPackages>;
36+
37+
// These tests reuse factories, so they must NOT actually push changes
38+
// (even if push is true, bumpAndPush is mocked)
39+
let singleRepoFactory: RepositoryFactory;
40+
let monorepoFactory: RepositoryFactory;
41+
let repo: Repository | undefined;
42+
43+
function getOptions(repoOptions?: Partial<RepoOptions>) {
44+
const parsedOptions = getParsedOptions({
45+
cwd: repo!.rootPath,
46+
argv: ['node', 'beachball', 'publish', '--yes'],
47+
testRepoOptions: {
48+
branch: defaultRemoteBranchName,
49+
registry: 'fake',
50+
message: 'apply package updates',
51+
fetch: false,
52+
push: false,
53+
gitTags: false,
54+
tag: 'latest',
55+
access: 'public',
56+
...repoOptions,
57+
},
58+
});
59+
return { options: parsedOptions.options, parsedOptions };
60+
}
61+
62+
/**
63+
* For more realistic testing, call `validate()` like the CLI command does, then call `publish()`.
64+
* This helps catch any new issues with double bumps or context mutation.
65+
*/
66+
async function publishWrapper(parsedOptions: ParsedOptions) {
67+
// This does an initial bump
68+
const { context } = validate(parsedOptions, { checkDependencies: true });
69+
// Ensure the later bump process does not modify the context
70+
deepFreezeProperties(context.bumpInfo);
71+
deepFreezeProperties(context.originalPackageInfos);
72+
await publish(parsedOptions.options, context);
73+
return context;
74+
}
75+
76+
beforeAll(() => {
77+
singleRepoFactory = new RepositoryFactory('single');
78+
monorepoFactory = new RepositoryFactory('monorepo');
79+
});
80+
81+
beforeEach(() => {
82+
mockPublishToRegistry.mockImplementation(() => Promise.resolve());
83+
mockBumpAndPush.mockImplementation(() => Promise.resolve());
84+
mockGetNewPackages.mockImplementation(() => Promise.resolve([]));
85+
});
86+
87+
afterEach(() => {
88+
jest.resetAllMocks();
89+
repo = undefined;
90+
});
91+
92+
afterAll(() => {
93+
jest.restoreAllMocks();
94+
});
95+
96+
it('bumps and pushes when enabled', async () => {
97+
repo = singleRepoFactory.cloneRepository();
98+
99+
const { options, parsedOptions } = getOptions({ bump: true, push: true });
100+
generateChangeFiles(['foo'], options);
101+
logs.clear();
102+
103+
await publishWrapper(parsedOptions);
104+
105+
expect(mockPublishToRegistry).toHaveBeenCalledTimes(1);
106+
expect(mockBumpAndPush).toHaveBeenCalledTimes(1);
107+
108+
// Verify bumpAndPush received a publish branch name and the options
109+
expect(mockBumpAndPush).toHaveBeenCalledWith(
110+
expect.objectContaining({ modifiedPackages: expect.any(Set) }),
111+
expect.stringMatching(/^publish_\d+$/),
112+
expect.objectContaining({ bump: true, push: true })
113+
);
114+
});
115+
116+
it('calls publishToRegistry when packToPath is set even if publish is false', async () => {
117+
repo = singleRepoFactory.cloneRepository();
118+
119+
const { options, parsedOptions } = getOptions({ publish: false, packToPath: '/tmp/fake-pack' });
120+
generateChangeFiles(['foo'], options);
121+
logs.clear();
122+
123+
await publishWrapper(parsedOptions);
124+
125+
expect(mockPublishToRegistry).toHaveBeenCalledTimes(1);
126+
});
127+
128+
it('returns to original branch and deletes publish branch after completion', async () => {
129+
repo = singleRepoFactory.cloneRepository();
130+
131+
const { options, parsedOptions } = getOptions();
132+
generateChangeFiles(['foo'], options);
133+
logs.clear();
134+
135+
const branchBefore = getBranchName({ cwd: repo.rootPath });
136+
137+
await publishWrapper(parsedOptions);
138+
139+
expect(getBranchName({ cwd: repo.rootPath })).toBe(branchBefore);
140+
expect(repo.git(['branch']).stdout).not.toMatch(/publish_/);
141+
});
142+
143+
it('returns to correct hash from detached HEAD', async () => {
144+
repo = singleRepoFactory.cloneRepository();
145+
146+
const { options, parsedOptions } = getOptions();
147+
generateChangeFiles(['foo'], options);
148+
logs.clear();
149+
150+
repo.checkout('--detach');
151+
const hashBefore = getCurrentHash({ cwd: repo.rootPath });
152+
153+
await publishWrapper(parsedOptions);
154+
155+
expect(getCurrentHash({ cwd: repo.rootPath })).toBe(hashBefore);
156+
expect(repo.git(['branch']).stdout).not.toMatch(/publish_/);
157+
});
158+
159+
it('populates bumpInfo with correct change types', async () => {
160+
repo = singleRepoFactory.cloneRepository();
161+
162+
const { options, parsedOptions } = getOptions();
163+
generateChangeFiles(['foo'], options);
164+
logs.clear();
165+
166+
const context = createCommandContext(parsedOptions);
167+
expect(context.bumpInfo).toBeUndefined();
168+
169+
await publish(options, context);
170+
171+
expect(context.bumpInfo).toBeDefined();
172+
expect(context.bumpInfo!.calculatedChangeTypes).toHaveProperty('foo', 'minor');
173+
expect(context.bumpInfo!.modifiedPackages).toContain('foo');
174+
expect(context.bumpInfo!.packageInfos.foo.version).toBe('1.1.0');
175+
});
176+
177+
it('passes correct bumpInfo to publishToRegistry in a monorepo', async () => {
178+
repo = monorepoFactory.cloneRepository();
179+
180+
const { options, parsedOptions } = getOptions({ bumpDeps: true });
181+
// baz has a minor change; bar depends on baz, foo depends on bar
182+
generateChangeFiles(['baz'], options);
183+
logs.clear();
184+
185+
await publishWrapper(parsedOptions);
186+
187+
expect(mockPublishToRegistry).toHaveBeenCalledTimes(1);
188+
const bumpInfo = mockPublishToRegistry.mock.calls[0][0];
189+
190+
expect(bumpInfo.calculatedChangeTypes.baz).toBe('minor');
191+
expect(bumpInfo.packageInfos.baz.version).toBe('1.4.0');
192+
193+
expect(bumpInfo.calculatedChangeTypes.bar).toBe('patch');
194+
expect(bumpInfo.packageInfos.bar.version).toBe('1.3.5');
195+
196+
expect(bumpInfo.calculatedChangeTypes.foo).toBe('patch');
197+
expect(bumpInfo.packageInfos.foo.version).toBe('1.0.1');
198+
199+
expect(bumpInfo.modifiedPackages).toEqual(new Set(['foo', 'bar', 'baz']));
200+
});
201+
202+
it('reuses pre-calculated bumpInfo from context', async () => {
203+
repo = singleRepoFactory.cloneRepository();
204+
205+
const { options, parsedOptions } = getOptions();
206+
generateChangeFiles(['foo'], options);
207+
logs.clear();
208+
209+
const context = createCommandContext(parsedOptions);
210+
context.bumpInfo = bumpInMemory(options, context);
211+
const originalBumpInfo = context.bumpInfo;
212+
213+
await publish(options, context);
214+
215+
// Should be the same object reference (not recalculated)
216+
expect(context.bumpInfo).toBe(originalBumpInfo);
217+
expect(mockPublishToRegistry.mock.calls[0][0]).toBe(originalBumpInfo);
218+
});
219+
220+
it('logs expected output for a standard publish flow', async () => {
221+
repo = singleRepoFactory.cloneRepository();
222+
223+
const { options, parsedOptions } = getOptions();
224+
generateChangeFiles(['foo'], options);
225+
logs.clear();
226+
227+
await publishWrapper(parsedOptions);
228+
229+
expect(logs.getMockLines('all', { root: repo.rootPath, sanitize: true })).toMatchSnapshot();
230+
});
231+
});

0 commit comments

Comments
 (0)