-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathbump-size-limits.test.ts
More file actions
241 lines (207 loc) · 8.68 KB
/
bump-size-limits.test.ts
File metadata and controls
241 lines (207 loc) · 8.68 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
import * as fs from 'fs';
import * as path from 'path';
import { describe, expect, it } from 'vitest';
// @ts-expect-error -- .mjs source has no declarations under `moduleResolution: "node"`
import * as bumpSizeLimits from './bump-size-limits.mjs';
const {
BYTES_PER_KB,
BYTES_PER_KIB,
computeNewLimit,
extractCurrentLimit,
HEADROOM_BYTES,
parseSizeLimitOutput,
renderSummary,
rewriteSizeLimitFile,
sanitizeMarkdownCell,
} = bumpSizeLimits;
const FIXTURE_PATH = path.join(__dirname, '__fixtures__', 'size-limit-sample.js');
function readFixture(): string {
return fs.readFileSync(FIXTURE_PATH, 'utf8');
}
describe('constants', () => {
it('exports the documented thresholds', () => {
expect(HEADROOM_BYTES).toBe(5000);
expect(BYTES_PER_KB).toBe(1000);
expect(BYTES_PER_KIB).toBe(1024);
});
});
describe('computeNewLimit', () => {
it('always returns currentSize + 5 KB, rounded up to the next full KB', () => {
// current 27_500 → +5000 = 32_500 → ceil to 33_000
expect(computeNewLimit(27_500)).toBe(33_000);
// current 21_000 → +5000 = 26_000 → already round → 26_000
expect(computeNewLimit(21_000)).toBe(26_000);
});
it('rounds up to next full KB', () => {
// current 27_001 → +5000 = 32_001 → ceil to 33_000
expect(computeNewLimit(27_001)).toBe(33_000);
// current 27_999 → +5000 = 32_999 → ceil to 33_000
expect(computeNewLimit(27_999)).toBe(33_000);
// current 28_000 → +5000 = 33_000 → already round → 33_000
expect(computeNewLimit(28_000)).toBe(33_000);
});
it('handles zero-size measurements safely', () => {
expect(computeNewLimit(0)).toBe(5_000);
});
});
describe('parseSizeLimitOutput', () => {
it('accepts well-formed input and returns name/size/sizeLimit triples', () => {
const raw = JSON.stringify([
{ name: '@sentry/browser', size: 27_500, sizeLimit: 27_000, passed: false },
{ name: 'CDN Bundle', size: 28_000, sizeLimit: 29_000, passed: true },
]);
expect(parseSizeLimitOutput(raw)).toEqual([
{ name: '@sentry/browser', size: 27_500, sizeLimit: 27_000 },
{ name: 'CDN Bundle', size: 28_000, sizeLimit: 29_000 },
]);
});
it('rejects non-array root', () => {
expect(() => parseSizeLimitOutput('{}')).toThrow(/expected array/i);
expect(() => parseSizeLimitOutput('null')).toThrow(/expected array/i);
});
it('rejects malformed JSON', () => {
expect(() => parseSizeLimitOutput('not json')).toThrow(SyntaxError);
});
it('rejects entries missing required fields', () => {
expect(() => parseSizeLimitOutput(JSON.stringify([{ name: 'x', size: 1 }]))).toThrow(/sizeLimit/);
expect(() => parseSizeLimitOutput(JSON.stringify([{ size: 1, sizeLimit: 2 }]))).toThrow(/name/);
});
it('rejects entries with non-string name', () => {
expect(() => parseSizeLimitOutput(JSON.stringify([{ name: 42, size: 1, sizeLimit: 2 }]))).toThrow(/name/);
});
it('rejects entries with non-finite numbers', () => {
expect(() => parseSizeLimitOutput(JSON.stringify([{ name: 'x', size: 'one', sizeLimit: 2 }]))).toThrow(/size/);
expect(() => parseSizeLimitOutput('[{"name":"x","size":1e500,"sizeLimit":2}]')).toThrow(/size/);
});
it('ignores extra fields without complaint', () => {
const raw = JSON.stringify([{ name: 'x', size: 1, sizeLimit: 2, passed: true, extra: 'ok' }]);
expect(parseSizeLimitOutput(raw)).toEqual([{ name: 'x', size: 1, sizeLimit: 2 }]);
});
});
describe('sanitizeMarkdownCell', () => {
it('passes plain text through unchanged', () => {
expect(sanitizeMarkdownCell('@sentry/browser')).toBe('@sentry/browser');
});
it('escapes pipes', () => {
expect(sanitizeMarkdownCell('a|b')).toBe('a\\|b');
});
it('escapes backticks', () => {
expect(sanitizeMarkdownCell('a`b')).toBe('a\\`b');
});
it('replaces newlines with spaces', () => {
expect(sanitizeMarkdownCell('a\nb')).toBe('a b');
expect(sanitizeMarkdownCell('a\r\nb')).toBe('a b');
});
it('preserves parentheses, commas, periods', () => {
expect(sanitizeMarkdownCell('CDN Bundle (incl. Tracing, Replay)')).toBe('CDN Bundle (incl. Tracing, Replay)');
});
});
describe('renderSummary', () => {
it('renders an empty header when there are no changes', () => {
const out = renderSummary([]);
expect(out).toContain('## Size limit auto-bump');
expect(out).toContain('All size limits already provide ≥5 KB headroom. No changes needed.');
});
it('renders a markdown table for one change', () => {
const out = renderSummary([
{ name: '@sentry/browser', oldLimit: '27 KB', newLimit: '28 KB', delta: 1, unit: 'KB' },
]);
expect(out).toContain('| Entry | Old limit | New limit | Δ |');
expect(out).toContain('| @sentry/browser | 27 KB | 28 KB | +1 KB |');
});
it('formats negative deltas with a minus', () => {
const out = renderSummary([
{ name: '@sentry/node', oldLimit: '177 KB', newLimit: '175 KB', delta: -2, unit: 'KB' },
]);
expect(out).toContain('| @sentry/node | 177 KB | 175 KB | -2 KB |');
});
it('uses the entry unit for the delta column (KiB)', () => {
const out = renderSummary([
{
name: '@sentry/cloudflare (withSentry)',
oldLimit: '420 KiB',
newLimit: '425 KiB',
delta: 5,
unit: 'KiB',
},
]);
expect(out).toContain('| @sentry/cloudflare (withSentry) | 420 KiB | 425 KiB | +5 KiB |');
});
it('escapes pipes in entry names', () => {
const out = renderSummary([{ name: 'evil|name', oldLimit: '1 KB', newLimit: '2 KB', delta: 1, unit: 'KB' }]);
expect(out).toContain('evil\\|name');
});
});
describe('rewriteSizeLimitFile', () => {
it('updates a single entry, preserving KB unit', () => {
const src = readFixture();
const out = rewriteSizeLimitFile(src, [{ name: '@sentry/browser', newLimitKb: 28, unit: 'KB' }]);
expect(out).toMatch(/name: '@sentry\/browser',[\s\S]*?limit: '28 KB',/);
expect(out).toMatch(/name: '@sentry\/browser - with treeshaking flags',[\s\S]*?limit: '25 KB',/);
});
it('updates entries with name-prefix collision correctly', () => {
const src = readFixture();
const out = rewriteSizeLimitFile(src, [
{ name: '@sentry/browser - with treeshaking flags', newLimitKb: 30, unit: 'KB' },
]);
expect(out).toMatch(/name: '@sentry\/browser',[\s\S]*?limit: '27 KB',/);
expect(out).toMatch(/name: '@sentry\/browser - with treeshaking flags',[\s\S]*?limit: '30 KB',/);
});
it('preserves KiB unit', () => {
const src = readFixture();
const out = rewriteSizeLimitFile(src, [{ name: '@sentry/cloudflare (withSentry)', newLimitKb: 425, unit: 'KiB' }]);
expect(out).toMatch(/name: '@sentry\/cloudflare \(withSentry\)',[\s\S]*?limit: '425 KiB',/);
});
it('handles names with parentheses and decimals in original limit', () => {
const src = readFixture();
const out = rewriteSizeLimitFile(src, [{ name: 'CDN Bundle (incl. Tracing)', newLimitKb: 50, unit: 'KB' }]);
expect(out).toMatch(/name: 'CDN Bundle \(incl\. Tracing\)',[\s\S]*?limit: '50 KB',/);
expect(out).not.toContain("limit: '46.5 KB'");
});
it('applies multiple changes', () => {
const src = readFixture();
const out = rewriteSizeLimitFile(src, [
{ name: '@sentry/browser', newLimitKb: 28, unit: 'KB' },
{ name: 'CDN Bundle (incl. Tracing)', newLimitKb: 50, unit: 'KB' },
]);
expect(out).toContain("limit: '28 KB'");
expect(out).toContain("limit: '50 KB'");
});
it('throws if a name does not match any entry', () => {
const src = readFixture();
expect(() => rewriteSizeLimitFile(src, [{ name: '@sentry/nonexistent', newLimitKb: 1, unit: 'KB' }])).toThrow(
/@sentry\/nonexistent/,
);
});
it('returns unchanged source when changes is empty', () => {
const src = readFixture();
expect(rewriteSizeLimitFile(src, [])).toBe(src);
});
it('does not modify the input string in-place', () => {
const src = readFixture();
const before = src;
rewriteSizeLimitFile(src, [{ name: '@sentry/browser', newLimitKb: 28, unit: 'KB' }]);
expect(src).toBe(before);
});
});
describe('extractCurrentLimit', () => {
const FIXTURE_SRC = `module.exports = [
{ name: '@sentry/browser', limit: '27 KB' },
{ name: '@sentry/cloudflare (withSentry)', limit: '420 KiB' },
];`;
it('extracts the limit value and unit by name', () => {
expect(extractCurrentLimit(FIXTURE_SRC, '@sentry/browser')).toEqual({
value: 27,
unit: 'KB',
raw: '27 KB',
});
expect(extractCurrentLimit(FIXTURE_SRC, '@sentry/cloudflare (withSentry)')).toEqual({
value: 420,
unit: 'KiB',
raw: '420 KiB',
});
});
it('returns null when the name is not present', () => {
expect(extractCurrentLimit(FIXTURE_SRC, '@sentry/missing')).toBeNull();
});
});