@mui/internal-code-infra 0.0.4-canary.72 → 0.0.4-canary.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/build/utils/git.d.mts +7 -0
  2. package/package.json +5 -3
  3. package/src/cli/cmdPublish.mjs +40 -5
  4. package/src/utils/git.mjs +11 -0
  5. package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +0 -20
  6. package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +0 -22
  7. package/src/brokenLinksChecker/__fixtures__/static-site/example.md +0 -20
  8. package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +0 -21
  9. package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +0 -17
  10. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +0 -29
  11. package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +0 -15
  12. package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +0 -5
  13. package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +0 -21
  14. package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +0 -20
  15. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +0 -20
  16. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +0 -24
  17. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +0 -28
  18. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +0 -19
  19. package/src/brokenLinksChecker/__fixtures__/static-site/unclosed-tags.html +0 -1
  20. package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +0 -20
  21. package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +0 -31
  22. package/src/brokenLinksChecker/index.test.ts +0 -301
  23. package/src/changelog/categorizeCommits.test.ts +0 -319
  24. package/src/changelog/filterCommits.test.ts +0 -363
  25. package/src/changelog/parseCommitLabels.test.ts +0 -509
  26. package/src/changelog/renderChangelog.test.ts +0 -378
  27. package/src/changelog/sortSections.test.ts +0 -199
  28. package/src/cli/cmdVale.test.mjs +0 -644
  29. package/src/eslint/mui/rules/add-undef-to-optional.test.mjs +0 -361
  30. package/src/eslint/mui/rules/consistent-production-guard.test.mjs +0 -162
  31. package/src/eslint/mui/rules/disallow-active-elements-as-key-event-target.test.mjs +0 -66
  32. package/src/eslint/mui/rules/disallow-react-api-in-server-components.test.mjs +0 -305
  33. package/src/eslint/mui/rules/docgen-ignore-before-comment.test.mjs +0 -52
  34. package/src/eslint/mui/rules/flatten-parentheses.test.mjs +0 -245
  35. package/src/eslint/mui/rules/mui-name-matches-component-name.test.mjs +0 -247
  36. package/src/eslint/mui/rules/no-empty-box.test.mjs +0 -40
  37. package/src/eslint/mui/rules/no-floating-cleanup.test.mjs +0 -141
  38. package/src/eslint/mui/rules/no-guarded-throw.test.mjs +0 -206
  39. package/src/eslint/mui/rules/no-presentation-role.test.mjs +0 -33
  40. package/src/eslint/mui/rules/no-styled-box.test.mjs +0 -73
  41. package/src/eslint/mui/rules/require-dev-wrapper.test.mjs +0 -265
  42. package/src/eslint/mui/rules/rules-of-use-theme-variants.test.mjs +0 -149
  43. package/src/eslint/mui/rules/straight-quotes.test.mjs +0 -67
  44. package/src/remark/firstBlockHeading.test.mjs +0 -107
  45. package/src/remark/gitDiff.test.mjs +0 -45
  46. package/src/remark/noSpaceInLinks.test.mjs +0 -22
  47. package/src/remark/straightQuotes.test.mjs +0 -25
  48. package/src/remark/tableAlignment.test.mjs +0 -28
  49. package/src/remark/terminalLanguage.test.mjs +0 -17
  50. package/src/utils/build.test.mjs +0 -1263
  51. package/src/utils/pnpm.test.mjs +0 -731
  52. package/src/utils/template.test.mjs +0 -133
  53. package/src/utils/typescript.test.mjs +0 -341
@@ -1,301 +0,0 @@
1
- import path from 'node:path';
2
- import getPort from 'get-port';
3
- import { describe, expect, it } from 'vitest';
4
-
5
- import {
6
- crawl,
7
- // eslint-disable-next-line import/extensions
8
- } from './index.mjs';
9
- import type {
10
- BrokenLinkIssue,
11
- HtmlValidateIssue,
12
- Issue,
13
- Link,
14
- // eslint-disable-next-line import/extensions
15
- } from './index.mjs';
16
-
17
- type ExpectedBrokenLinkIssue = Omit<Partial<BrokenLinkIssue>, 'link'> & { link?: Partial<Link> };
18
-
19
- function objectMatchingIssue(expectedIssue: ExpectedBrokenLinkIssue) {
20
- return expect.objectContaining({
21
- ...expectedIssue,
22
- ...(expectedIssue.link ? { link: expect.objectContaining(expectedIssue.link) } : {}),
23
- });
24
- }
25
-
26
- /**
27
- * Helper to assert that a broken link issue with matching properties exists in the issues array
28
- */
29
- function expectIssue(issues: Issue[], expectedIssue: ExpectedBrokenLinkIssue) {
30
- expect(issues).toEqual(expect.arrayContaining([objectMatchingIssue(expectedIssue)]));
31
- }
32
-
33
- /**
34
- * Helper to assert that no broken link issue with matching properties exists in the issues array
35
- */
36
- function expectNotIssue(issues: Issue[], notExpectedIssue: ExpectedBrokenLinkIssue) {
37
- expect(issues).not.toEqual(expect.arrayContaining([objectMatchingIssue(notExpectedIssue)]));
38
- }
39
-
40
- describe('Broken Links Checker', () => {
41
- const fixtureDir = path.join(import.meta.dirname, '__fixtures__', 'static-site');
42
- const servePath = path.join(import.meta.dirname, '..', '..', 'node_modules', '.bin', 'serve');
43
-
44
- it('should detect all broken links and targets across the entire site', async () => {
45
- const port = await getPort();
46
- const host = `http://localhost:${port}`;
47
-
48
- const result = await crawl({
49
- startCommand: `${servePath} ${fixtureDir} -p ${port}`,
50
- host,
51
- ignoredPaths: [/ignored-page\.html$/],
52
- ignoredContent: ['.sidebar'],
53
- ignoredTargets: new Set(['__should-be-ignored']),
54
- knownTargets: new Map([['/external-page.html', new Set(['#valid-target'])]]),
55
- knownTargetsDownloadUrl: [`${host}/known-targets.json`],
56
- seedUrls: ['/', '/orphaned-page.html'],
57
- // Test ignores with new array syntax and various property combinations
58
- ignores: [
59
- // Backward compatible: single values still work
60
- { path: '/broken-links.html', href: '/does-not-exist.html' },
61
- // Test array syntax: multiple hrefs in one rule (OR logic within property)
62
- { path: '/broken-links.html', href: [/another-missing/, '../broken-relative-html.html'] },
63
- // Test contentType: ignore broken links from markdown files
64
- { contentType: 'text/markdown', href: '/broken-from-markdown.html' },
65
- // Test href-only rule (matches from any page) - note: matches the actual href value
66
- { href: 'broken-relative.html' },
67
- ],
68
- // Exercise the array form with union semantics: every matching entry
69
- // contributes to the page's config. The baseline entry (no `path`)
70
- // turns off `no-dup-id` everywhere; the path-specific entry turns off
71
- // `no-raw-characters` only on /invalid-html.html. Both rules are
72
- // silenced on that page because the configs are merged, not replaced.
73
- //
74
- // This also guards against the path-specific entry clobbering the
75
- // baseline: the path entry only names `no-raw-characters`, so it must
76
- // not re-introduce the recommended ruleset and re-enable the
77
- // `no-dup-id` that the baseline silenced (which /invalid-html.html
78
- // violates). If it did, that page would report `no-dup-id` below.
79
- htmlValidate: [
80
- { config: { rules: { 'no-dup-id': 'off' } } },
81
- { path: '/invalid-html.html', config: { rules: { 'no-raw-characters': 'off' } } },
82
- ],
83
- });
84
-
85
- expect(result.links).toHaveLength(67);
86
- // Broken link issue count: original 11, minus ignored ones (broken-from-markdown via contentType,
87
- // broken-relative via href-only rule)
88
- const brokenLinkIssues = result.issues.filter(
89
- (issue) => issue.type === 'broken-link' || issue.type === 'broken-target',
90
- );
91
- expect(brokenLinkIssues).toHaveLength(9);
92
-
93
- // Test ignores: these broken links should be ignored (not in issues)
94
- expectNotIssue(result.issues, {
95
- type: 'broken-link',
96
- link: {
97
- src: '/broken-links.html',
98
- href: '/does-not-exist.html',
99
- },
100
- });
101
-
102
- expectNotIssue(result.issues, {
103
- type: 'broken-link',
104
- link: {
105
- src: '/broken-links.html',
106
- href: '/another-missing-page.html',
107
- },
108
- });
109
-
110
- // Check broken-target type issues
111
- expectIssue(result.issues, {
112
- type: 'broken-target',
113
- link: {
114
- src: '/broken-targets.html',
115
- href: '/with-anchors.html#nonexistent',
116
- text: 'Non-existent anchor',
117
- },
118
- });
119
-
120
- expectIssue(result.issues, {
121
- type: 'broken-target',
122
- link: {
123
- src: '/broken-targets.html',
124
- href: '/valid.html#missing-target',
125
- text: 'Valid page, missing target',
126
- },
127
- });
128
-
129
- expectIssue(result.issues, {
130
- type: 'broken-target',
131
- link: {
132
- src: '/broken-targets.html',
133
- href: '/with-anchors.html#also-missing',
134
- text: 'Also missing',
135
- },
136
- });
137
-
138
- // Verify that valid links are not reported
139
- expectNotIssue(result.issues, { link: { href: '/' } });
140
- expectNotIssue(result.issues, { link: { href: '/valid.html' } });
141
- expectNotIssue(result.issues, { link: { href: '/with-anchors.html' } });
142
- expectNotIssue(result.issues, { link: { href: '/with-anchors.html#section1' } });
143
- expectNotIssue(result.issues, { link: { href: '/with-anchors.html#section2' } });
144
- expectNotIssue(result.issues, { link: { href: '/with-anchors.html#section3' } });
145
- expectNotIssue(result.issues, { link: { href: '/nested/page.html' } });
146
-
147
- // Verify that external links are not reported
148
- expectNotIssue(result.issues, { link: { href: 'https://example.com' } });
149
- expectNotIssue(result.issues, { link: { href: 'https://github.com/mui' } });
150
-
151
- // Test ignoredPaths: ignored-page.html should not be crawled
152
- expectNotIssue(result.issues, { link: { src: '/ignored-page.html' } });
153
- expectNotIssue(result.issues, { link: { href: '/this-link-should-not-be-checked.html' } });
154
-
155
- // Test ignoredContent: links in .sidebar should be ignored
156
- expectNotIssue(result.issues, { link: { href: '/sidebar-broken-link.html' } });
157
-
158
- // Test ignoredTargets: IDs that shouldn't be tracked as valid link destinations
159
- // (e.g., framework-specific IDs like '__next'). Links to these should be reported as broken.
160
- expectIssue(result.issues, {
161
- type: 'broken-target',
162
- link: {
163
- src: '/page-with-custom-targets.html',
164
- href: '#__should-be-ignored',
165
- text: 'Link to ignored ID',
166
- },
167
- });
168
-
169
- // Test that non-ignored custom target is valid
170
- expectNotIssue(result.issues, { link: { href: '/page-with-custom-targets.html#custom-id' } });
171
-
172
- // Test knownTargets: valid-target is known and should not cause issues
173
- expectNotIssue(result.issues, { link: { href: '/external-page.html#valid-target' } });
174
-
175
- // Test knownTargets: invalid-target is not in knownTargets and should cause an issue
176
- expectIssue(result.issues, {
177
- type: 'broken-target',
178
- link: {
179
- src: '/page-with-known-target-links.html',
180
- href: '/external-page.html#invalid-target',
181
- text: 'Invalid external target',
182
- },
183
- });
184
-
185
- // Test knownTargetsDownloadUrl: method1 and method2 are in downloaded known targets
186
- expectNotIssue(result.issues, { link: { href: '/api-page.html#method1' } });
187
- expectNotIssue(result.issues, { link: { href: '/api-page.html#method2' } });
188
-
189
- // Test knownTargetsDownloadUrl: unknown-method is not in downloaded known targets and should cause an issue
190
- expectIssue(result.issues, {
191
- type: 'broken-target',
192
- link: {
193
- src: '/page-with-api-links.html',
194
- href: '/api-page.html#unknown-method',
195
- text: 'Unknown API method',
196
- },
197
- });
198
-
199
- // Test seedUrls: orphaned-page.html should be crawled even though it's not linked from anywhere
200
- expect(result.pages.has('/orphaned-page.html')).toBe(true);
201
-
202
- // Test seedUrls: broken link from orphaned page should be detected
203
- expectIssue(result.issues, {
204
- type: 'broken-link',
205
- link: {
206
- src: '/orphaned-page.html',
207
- href: '/orphaned-broken-link.html',
208
- text: 'Broken link from orphaned page',
209
- },
210
- });
211
-
212
- // Test trailing slash normalization: /valid.html and /valid.html/ should be treated as the same page
213
- // The orphaned page has both links, but they should not cause duplicate page crawls
214
- expectNotIssue(result.issues, { link: { href: '/valid.html/' } });
215
-
216
- // Test contentType ignores: broken link from markdown should be ignored via contentType rule
217
- expectNotIssue(result.issues, {
218
- type: 'broken-link',
219
- link: {
220
- src: '/example.md',
221
- href: '/broken-from-markdown.html',
222
- },
223
- });
224
-
225
- // Valid links from markdown should not cause issues
226
- expectNotIssue(result.issues, { link: { href: '/valid.html', src: '/example.md' } });
227
- expectNotIssue(result.issues, {
228
- link: { href: '/with-anchors.html#section1', src: '/example.md' },
229
- });
230
-
231
- // Links inside code blocks should NOT be extracted (they're text, not <a> tags)
232
- expectNotIssue(result.issues, { link: { href: '/this-should-not-be-checked.html' } });
233
-
234
- // Markdown file itself should be crawlable without issues
235
- expectNotIssue(result.issues, { link: { href: '/example.md' } });
236
-
237
- // Test that markdown heading anchors are discovered (rehype-slug)
238
- expect(result.pages.get('/example.md')?.targets.has('#example-markdown-file')).toBe(true);
239
- expect(result.pages.get('/example.md')?.targets.has('#markdown-section')).toBe(true);
240
-
241
- // Test href-only ignores: broken-relative.html should be ignored from any page via href-only rule
242
- expectNotIssue(result.issues, {
243
- type: 'broken-link',
244
- link: {
245
- href: 'broken-relative.html',
246
- },
247
- });
248
-
249
- // Valid relative links from markdown should not cause issues
250
- expectNotIssue(result.issues, { link: { href: 'valid.html', src: '/example.md' } });
251
- expectNotIssue(result.issues, { link: { href: './with-anchors.html', src: '/example.md' } });
252
-
253
- // Test relative links in HTML
254
- // Also verifies that the ignore pattern { path: /^\/broken-links\.html$/, href: '../broken-relative-html.html' }
255
- // does NOT match this link since the path regex doesn't match /nested/page.html
256
- expectIssue(result.issues, {
257
- type: 'broken-link',
258
- link: {
259
- src: '/nested/page.html',
260
- href: '../broken-relative-html.html',
261
- text: 'Relative broken link from HTML',
262
- },
263
- });
264
-
265
- // Valid relative links from HTML should not cause issues
266
- expectNotIssue(result.issues, { link: { href: '../valid.html', src: '/nested/page.html' } });
267
-
268
- // Test unclosed tags: links inside unclosed <main> tags should still be detected
269
- // (regression test for node-html-parser parseNoneClosedTags option)
270
- expectIssue(result.issues, {
271
- type: 'broken-link',
272
- link: {
273
- src: '/unclosed-tags.html',
274
- href: '/broken-inside-unclosed-main.html',
275
- text: 'Broken link inside unclosed main',
276
- },
277
- });
278
-
279
- // Valid links inside unclosed tags should not cause issues
280
- expectNotIssue(result.issues, {
281
- link: { href: '/valid.html', src: '/unclosed-tags.html' },
282
- });
283
-
284
- // Test contentType is stored on pageData
285
- expect(result.pages.get('/example.md')?.contentType).toBe('text/markdown');
286
- expect(result.pages.get('/')?.contentType).toBe('text/html');
287
-
288
- // Test htmlValidate union semantics: invalid-html.html has both a duplicate
289
- // ID (no-dup-id) and a raw `&` (no-raw-characters). The baseline entry
290
- // silences no-dup-id; the path-specific entry silences no-raw-characters.
291
- // Under union semantics both apply, so the page reports zero issues — and
292
- // the path-specific entry must not clobber the baseline's no-dup-id.
293
- const htmlValidateIssues = result.issues.filter(
294
- (issue): issue is HtmlValidateIssue => issue.type === 'html-validate',
295
- );
296
- const invalidHtmlIssues = htmlValidateIssues.filter(
297
- (issue) => issue.pageUrl === '/invalid-html.html',
298
- );
299
- expect(invalidHtmlIssues).toEqual([]);
300
- }, 30000);
301
- });
@@ -1,319 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- // eslint-disable-next-line import/extensions
3
- import { categorizeCommits } from './categorizeCommits.mjs';
4
- import type { FetchedCommitDetails, CategorizationConfig, LabelConfig } from './types';
5
-
6
- const baseLabelConfig: LabelConfig = {
7
- plan: {
8
- values: ['pro', 'premium'],
9
- },
10
- flags: {
11
- 'breaking change': { name: 'breaking change' },
12
- },
13
- };
14
-
15
- function createCommit(
16
- prNumber: number,
17
- labels: string[],
18
- overrides?: Partial<FetchedCommitDetails>,
19
- ): FetchedCommitDetails {
20
- return {
21
- sha: `sha-${prNumber}`,
22
- message: `feat: feature for PR #${prNumber}`,
23
- labels,
24
- prNumber,
25
- html_url: `https://github.com/mui/mui-x/pull/${prNumber}`,
26
- author: { login: 'dev', association: 'team' },
27
- createdAt: null,
28
- mergedAt: null,
29
- ...overrides,
30
- };
31
- }
32
-
33
- describe('categorizeCommits', () => {
34
- describe('component strategy', () => {
35
- const componentConfig: CategorizationConfig = {
36
- strategy: 'component',
37
- labels: baseLabelConfig,
38
- sections: {
39
- fallbackSection: 'Other',
40
- },
41
- };
42
-
43
- it('should categorize commits by component label', () => {
44
- const commits = [
45
- createCommit(1, ['component: Button']),
46
- createCommit(2, ['component: Checkbox']),
47
- createCommit(3, ['component: Button']),
48
- ];
49
-
50
- const result = categorizeCommits(commits, componentConfig);
51
-
52
- expect(result.size).toBe(2);
53
- // Component labels are normalized to lowercase by parseCommitLabels
54
- expect(result.get('Button')).toHaveLength(2);
55
- expect(result.get('Checkbox')).toHaveLength(1);
56
- });
57
-
58
- it('should place commits without component labels in fallback section', () => {
59
- const commits = [createCommit(1, []), createCommit(2, ['breaking change'])];
60
-
61
- const result = categorizeCommits(commits, componentConfig);
62
-
63
- expect(result.size).toBe(1);
64
- expect(result.get('Other')).toHaveLength(2);
65
- });
66
-
67
- it('should place commits with multiple component labels in multiple sections', () => {
68
- const commits = [createCommit(1, ['component: Button', 'component: Checkbox'])];
69
-
70
- const result = categorizeCommits(commits, componentConfig);
71
-
72
- expect(result.size).toBe(2);
73
- // Component labels are normalized to lowercase by parseCommitLabels
74
- expect(result.get('Button')).toHaveLength(1);
75
- expect(result.get('Checkbox')).toHaveLength(1);
76
- // Same commit object in both categories
77
- expect(result.get('Button')?.[0].prNumber).toBe(1);
78
- expect(result.get('Checkbox')?.[0].prNumber).toBe(1);
79
- });
80
-
81
- it('should use categoryOverride when present', () => {
82
- const configWithOverrides: CategorizationConfig = {
83
- ...componentConfig,
84
- labels: {
85
- ...baseLabelConfig,
86
- categoryOverrides: {
87
- 'scope: all components': 'General changes',
88
- },
89
- },
90
- };
91
-
92
- const commits = [
93
- createCommit(1, ['component: Button', 'scope: all components']),
94
- createCommit(2, ['component: Checkbox']),
95
- createCommit(3, ['component: Button', 'scope: all components']),
96
- ];
97
-
98
- const result = categorizeCommits(commits, configWithOverrides);
99
-
100
- expect(result.size).toBe(2);
101
- expect(result.get('General changes')).toHaveLength(2);
102
- expect(result.get('General changes')?.[0].prNumber).toBe(1);
103
- expect(result.get('General changes')?.[1].prNumber).toBe(3);
104
- // Component labels are normalized to lowercase by parseCommitLabels
105
- expect(result.get('Checkbox')).toHaveLength(1);
106
- });
107
- });
108
-
109
- describe('package strategy', () => {
110
- const packageConfig: CategorizationConfig = {
111
- strategy: 'package',
112
- labels: baseLabelConfig,
113
- packageNaming: {
114
- mappings: {
115
- 'data grid': '@mui/x-data-grid',
116
- charts: '@mui/x-charts',
117
- pickers: '@mui/x-date-pickers',
118
- },
119
- genericScopes: ['docs', 'code-infra'],
120
- plans: {
121
- pro: {
122
- '@mui/x-data-grid': '@mui/x-data-grid-pro',
123
- '@mui/x-charts': '@mui/x-charts-pro',
124
- '@mui/x-date-pickers': '@mui/x-date-pickers-pro',
125
- },
126
- premium: {
127
- '@mui/x-data-grid': '@mui/x-data-grid-premium',
128
- },
129
- },
130
- },
131
- sections: {
132
- fallbackSection: 'Other',
133
- },
134
- };
135
-
136
- it('should categorize commits by scope to package mapping', () => {
137
- const commits = [
138
- createCommit(1, ['scope: data grid']),
139
- createCommit(2, ['scope: charts']),
140
- createCommit(3, ['scope: data grid']),
141
- ];
142
-
143
- const result = categorizeCommits(commits, packageConfig);
144
-
145
- expect(result.size).toBe(2);
146
- expect(result.get('@mui/x-data-grid')).toHaveLength(2);
147
- expect(result.get('@mui/x-charts')).toHaveLength(1);
148
- });
149
-
150
- it('should place commits without scope in fallback section', () => {
151
- const commits = [createCommit(1, []), createCommit(2, ['breaking change'])];
152
-
153
- const result = categorizeCommits(commits, packageConfig);
154
-
155
- expect(result.size).toBe(1);
156
- expect(result.get('Other')).toHaveLength(2);
157
- });
158
-
159
- it('should place commits with multiple scope labels in multiple sections', () => {
160
- const commits = [createCommit(1, ['scope: data grid', 'scope: charts'])];
161
-
162
- const result = categorizeCommits(commits, packageConfig);
163
-
164
- expect(result.size).toBe(2);
165
- expect(result.get('@mui/x-data-grid')).toHaveLength(1);
166
- expect(result.get('@mui/x-charts')).toHaveLength(1);
167
- });
168
-
169
- it('should use generic scopes directly as section names', () => {
170
- const commits = [createCommit(1, ['scope: docs']), createCommit(2, ['scope: code-infra'])];
171
-
172
- const result = categorizeCommits(commits, packageConfig);
173
-
174
- expect(result.size).toBe(2);
175
- expect(result.get('docs')).toHaveLength(1);
176
- expect(result.get('code-infra')).toHaveLength(1);
177
- });
178
-
179
- it('should apply plan to package name when plan label is present', () => {
180
- const commits = [
181
- createCommit(1, ['scope: data grid', 'plan: pro']),
182
- createCommit(2, ['scope: data grid', 'plan: premium']),
183
- createCommit(3, ['scope: data grid']),
184
- ];
185
-
186
- const result = categorizeCommits(commits, packageConfig);
187
-
188
- expect(result.size).toBe(3);
189
- expect(result.get('@mui/x-data-grid-pro')).toHaveLength(1);
190
- expect(result.get('@mui/x-data-grid-premium')).toHaveLength(1);
191
- expect(result.get('@mui/x-data-grid')).toHaveLength(1);
192
- });
193
-
194
- it('should throw error when scope mapping is not found', () => {
195
- const commits = [createCommit(42, ['scope: unknown-package'])];
196
-
197
- expect(() => categorizeCommits(commits, packageConfig)).toThrow(
198
- 'No package mapping found for scope "unknown-package" in commit #42',
199
- );
200
- });
201
-
202
- it('should throw error when plan mapping is not found for package', () => {
203
- const commits = [createCommit(42, ['scope: charts', 'plan: premium'])];
204
-
205
- expect(() => categorizeCommits(commits, packageConfig)).toThrow(
206
- 'No premium plan package mapping found for base package "@mui/x-charts" in commit #42',
207
- );
208
- });
209
-
210
- it('should throw error when package naming config is missing', () => {
211
- const configWithoutNaming: CategorizationConfig = {
212
- strategy: 'package',
213
- labels: baseLabelConfig,
214
- sections: {
215
- fallbackSection: 'Other',
216
- },
217
- };
218
-
219
- const commits = [createCommit(1, ['scope: data grid'])];
220
-
221
- expect(() => categorizeCommits(commits, configWithoutNaming)).toThrow(
222
- 'Package naming configuration is required for package-first strategy',
223
- );
224
- });
225
-
226
- it('should use categoryOverride when present', () => {
227
- const configWithOverrides: CategorizationConfig = {
228
- ...packageConfig,
229
- labels: {
230
- ...baseLabelConfig,
231
- categoryOverrides: {
232
- 'scope: all components': 'General changes',
233
- },
234
- },
235
- };
236
-
237
- const commits = [
238
- createCommit(1, ['scope: data grid', 'scope: all components']),
239
- createCommit(2, ['scope: charts']),
240
- ];
241
-
242
- const result = categorizeCommits(commits, configWithOverrides);
243
-
244
- expect(result.size).toBe(2);
245
- expect(result.get('General changes')).toHaveLength(1);
246
- expect(result.get('@mui/x-charts')).toHaveLength(1);
247
- });
248
- });
249
-
250
- describe('unknown strategy', () => {
251
- it('should throw error for unknown strategy', () => {
252
- const invalidConfig = {
253
- strategy: 'invalid' as 'component',
254
- labels: baseLabelConfig,
255
- sections: {
256
- fallbackSection: 'Other',
257
- },
258
- };
259
-
260
- const commits = [createCommit(1, [])];
261
-
262
- expect(() => categorizeCommits(commits, invalidConfig)).toThrow(
263
- 'Unknown categorization strategy: invalid',
264
- );
265
- });
266
- });
267
-
268
- describe('parsed labels', () => {
269
- const config: CategorizationConfig = {
270
- strategy: 'component',
271
- labels: {
272
- ...baseLabelConfig,
273
- flags: {
274
- 'breaking change': { name: 'breaking change' },
275
- enhancement: { name: 'enhancement' },
276
- },
277
- },
278
- sections: {
279
- fallbackSection: 'Other',
280
- },
281
- };
282
-
283
- it('should include parsed labels in categorized commits', () => {
284
- const commits = [createCommit(1, ['component: Button', 'breaking change', 'enhancement'])];
285
-
286
- const result = categorizeCommits(commits, config);
287
-
288
- const categorizedCommit = result.get('Button')?.[0];
289
- expect(categorizedCommit?.parsed.components).toEqual(['Button']);
290
- expect(categorizedCommit?.parsed.flags).toEqual(['breaking change', 'enhancement']);
291
- });
292
-
293
- it('should include plan in parsed labels', () => {
294
- const commits = [createCommit(1, ['component: Button', 'plan: pro'])];
295
-
296
- const result = categorizeCommits(commits, config);
297
-
298
- // Component labels are normalized to lowercase by parseCommitLabels
299
- const categorizedCommit = result.get('Button')?.[0];
300
- expect(categorizedCommit?.parsed.plan).toBe('pro');
301
- });
302
- });
303
-
304
- describe('empty input', () => {
305
- const config: CategorizationConfig = {
306
- strategy: 'component',
307
- labels: baseLabelConfig,
308
- sections: {
309
- fallbackSection: 'Other',
310
- },
311
- };
312
-
313
- it('should return empty map for empty commits array', () => {
314
- const result = categorizeCommits([], config);
315
-
316
- expect(result.size).toBe(0);
317
- });
318
- });
319
- });