Repository navigation
Expand file tree
/
Copy pathindex.test.js
More file actions
254 lines (215 loc) · 9.84 KB
/
Copy pathindex.test.js
File metadata and controls
254 lines (215 loc) · 9.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
'use strict';
const config = require('./index');
describe('@linchpinagency/commitlint-config', () => {
test('exports an object', () => {
expect(typeof config).toBe('object');
});
test('extends @commitlint/config-conventional', () => {
expect(config.extends).toContain('@commitlint/config-conventional');
});
test('type-enum rule is set to error level', () => {
const [level] = config.rules['type-enum'];
expect(level).toBe(2);
});
test('type-enum includes expected types', () => {
const [, , types] = config.rules['type-enum'];
const expected = ['add', 'improve', 'build', 'chore', 'ci', 'docs', 'feat', 'feature', 'fix', 'perf', 'refactor', 'remove', 'revert', 'style', 'test', 'update'];
expect(types).toEqual(expect.arrayContaining(expected));
});
// Every type in type-enum must also be reachable through headerPattern. The two are
// separate lists, so a type added to one and not the other passes type-enum and is
// then rejected by the parser — the failure mode this guards against.
test('every allowed type is accepted by headerPattern', () => {
const [, , types] = config.rules['type-enum'];
const pattern = config.parserPreset.parserOpts.headerPattern;
for (const type of types) {
expect(`${type}(PROJ-123): Some subject`).toMatch(pattern);
}
});
test('subject-case rule is set to warning level', () => {
const [level] = config.rules['subject-case'];
expect(level).toBe(1);
});
test('parserPreset is defined with headerPattern and headerCorrespondence', () => {
expect(config.parserPreset).toBeDefined();
expect(config.parserPreset.parserOpts.headerPattern).toBeInstanceOf(RegExp);
expect(config.parserPreset.parserOpts.headerCorrespondence).toEqual(['type', 'scope', 'subject']);
});
test('headerPattern matches valid commit message', () => {
const pattern = config.parserPreset.parserOpts.headerPattern;
expect('feat(PROJ-123): Add new feature').toMatch(pattern);
expect('feature(NO-TASK): Add another feature').toMatch(pattern);
expect('fix(NO-TASK): Fix a bug').toMatch(pattern);
expect('docs(#42): Update readme').toMatch(pattern);
expect('add(PROJ-123): Add a thing').toMatch(pattern);
expect('remove(PROJ-123): Remove a thing').toMatch(pattern);
});
// release-please owns the format of its own release commit, so linting it can only
// ever fail the release PR. Regressing this blocks every release in every repo.
describe('release-please commits are ignored', () => {
const ignored = (message) => config.ignores.some((fn) => fn(message));
test.each([
'chore(main): release 1.1.0',
'chore(master): release 2.10.2',
'chore(main): release v1.0.0',
'chore(commitlint-config): release 1.2.3',
])('ignores %s', (message) => {
expect(ignored(message)).toBe(true);
});
test.each([
'chore(NO-TASK): Release the hounds',
'feat(PROJ-123): Add release notes',
'chore(main): Update something else',
])('does not ignore %s', (message) => {
expect(ignored(message)).toBe(false);
});
});
test('helpUrl points to conventionalcommits.org', () => {
expect(config.helpUrl).toBe('https://www.conventionalcommits.org');
});
// The previous config carried a top-level `messages` key. That is not a commitlint
// option and never reached the user; diagnostics now come from the linchpin-header rule.
test('does not rely on a non-existent messages option', () => {
expect(config.messages).toBeUndefined();
});
describe('linchpin-header explains which part failed', () => {
const { explain } = config;
test.each([
['feat(PROJ-123): Add new feature'],
['fix(NO-TASK): Fix a bug'],
['docs(#42): Update readme'],
['build(NO-TASK): Update npm dependency npm-run-all2 to v9.0.3'],
])('accepts %s', (header) => {
expect(explain(header)).toBeNull();
});
test('names the offending type, not an empty subject', () => {
const out = explain('nope(PROJ-123): Bad type here');
expect(out).toContain('"nope" is not a valid type');
expect(out).not.toContain('subject may not be empty');
});
test('names the offending scope', () => {
expect(explain('feat(nonsense): Update something')).toContain('"nonsense" is not a valid scope');
});
test('suggests the uppercase form of a lowercase task key', () => {
expect(explain('feat(proj-123): Lowercase key')).toContain('try "PROJ-123"');
});
test('catches the NOTASK typo specifically', () => {
expect(explain('feat(NOTASK): Typo scope')).toContain('exactly as NO-TASK');
});
test('reports a missing scope', () => {
expect(explain('feat: No scope at all')).toContain('scope is missing');
});
test('reports a missing subject', () => {
expect(explain('feat(NO-TASK):')).toContain('subject is missing');
});
test('reports an empty header', () => {
expect(explain('')).toContain('empty');
});
});
// Dependency updates carry no task, so the scope slot names the kind of dependency.
// wp-plugin / wp-theme keep WordPress updates legible in a mostly automated log.
describe('dependency scopes', () => {
const { explain } = config;
const pattern = config.parserPreset.parserOpts.headerPattern;
test.each([
'update(wp-plugin): Update translatepress-multilingual to v3.2.4',
'update(wp-theme): Update ollie-pro to v2.6.1',
'remove(wp-plugin): Remove akismet',
'remove(wp-theme): Remove twentytwentyfour',
'update(wporg): Update a public package',
'update(linchpin): Update a packagist.linchpin.com package',
'build(deps): Update npm-run-all2 to v9.0.3',
'build(deps-dev): Update svgo to v3.3.4',
'build(composer): Update humbug/php-scoper to v0.18.19',
'build(npm): Update webpack to v5.94.0',
'chore(actions): Update actions/checkout to v7',
])('accepts %s', (header) => {
expect(explain(header)).toBeNull();
expect(header).toMatch(pattern);
});
// deps-dev must win over deps, or the scope parses as `deps` and the header breaks.
test('parses deps-dev as a whole scope', () => {
const [, , scope] = 'build(deps-dev): Update svgo to v3'.match(pattern);
expect(scope).toBe('deps-dev');
});
test.each([
'feat(nonsense): Bad scope',
'feat(DEP): Old uppercase scope',
'feat(PLUGIN): Old uppercase scope',
])('still rejects %s', (header) => {
expect(explain(header)).not.toBeNull();
});
});
// 1.3.0 allowed wp-plugin and wp-theme as types so release-please could give WordPress
// updates a dedicated changelog section. The team settled on the scope form instead, so the
// type form is gone. Asserted rather than assumed: it is a one-word edit to put it back, and
// the two forms are indistinguishable at a glance in a config file.
describe('wp-plugin and wp-theme are scopes, not types', () => {
const { explain } = config;
const pattern = config.parserPreset.parserOpts.headerPattern;
test.each([
'wp-plugin(wporg): Update akismet to v5.3',
'wp-plugin(linchpin): Update some-plugin to v3.0',
'wp-theme(deps): Update twentytwentyfour to v2.0',
])('rejects the type form %s', (header) => {
expect(explain(header)).toContain('is not a valid type');
expect(header).not.toMatch(pattern);
});
test.each(['wp-plugin', 'wp-theme'])('%s is absent from type-enum', (type) => {
const [, , types] = config.rules['type-enum'];
expect(types).not.toContain(type);
});
test('the scope form is what replaces it', () => {
expect(explain('update(wp-plugin): Update translatepress-multilingual to v3.2.4')).toBeNull();
expect(explain('update(wp-theme): Update ollie-pro to v2.6.1')).toBeNull();
});
});
// An optional bracketed tag carries what the scope slot no longer can - which registry a
// WordPress package came from. linchpin/renovatebot-config emits `[.org]` and `[packagist]`.
describe('subject tags', () => {
const { explain } = config;
const pattern = config.parserPreset.parserOpts.headerPattern;
test.each([
'update(wp-plugin): [.org] Update akismet to v5.3',
'update(wp-plugin): [packagist] Update gravityforms to v3',
'update(wp-theme): [.org] Update Themes wordpress.org',
'update(wp-theme): [packagist] Update Themes packagist.linchpin.com - Major',
'fix(PROJ-123): [hotfix] Correct the redirect',
'update(wp-plugin): [wp-packages] Update akismet to v5',
])('accepts %s', (header) => {
expect(explain(header)).toBeNull();
expect(header).toMatch(pattern);
});
// The whole reason the tag is a non-capturing group. If it leaked into the subject then
// subject-case would be judging the label rather than the sentence.
test('a tag is not part of the captured subject', () => {
const [, type, scope, subject] = 'update(wp-plugin): [.org] Update akismet to v5'.match(pattern);
expect(type).toBe('update');
expect(scope).toBe('wp-plugin');
expect(subject).toBe('Update akismet to v5');
});
test('an untagged subject is unaffected', () => {
const [, , , subject] = 'update(wp-plugin): Update akismet to v5'.match(pattern);
expect(subject).toBe('Update akismet to v5');
});
test('a malformed tag is reported as a tag problem', () => {
expect(explain('update(wp-plugin): [] Update akismet')).toContain('is not a valid tag');
expect(explain('update(wp-plugin): [a b] Update akismet')).toContain('is not a valid tag');
});
test('an unclosed tag names the missing bracket', () => {
expect(explain('update(wp-plugin): [unclosed Update akismet')).toContain('closing bracket');
});
// Each of these used to collapse into "The subject must start with a letter or number",
// which is true of a leading `[` and no help at all in fixing it.
test.each([
'update(wp-plugin): [] Update akismet',
'update(wp-plugin): [a b] Update akismet',
'update(wp-plugin): [unclosed Update akismet',
])('does not misreport %s as a subject problem', (header) => {
expect(explain(header)).not.toContain('must start with a letter');
});
test('a tag with no subject after it still reports a missing subject', () => {
expect(explain('update(wp-plugin): [.org]')).toContain('subject is missing');
});
});
});