From dacaa4fec6cf75b2b8f7c1668e090eae66c0ea5b Mon Sep 17 00:00:00 2001 From: rusackas Date: Tue, 28 Jul 2026 03:03:44 -0700 Subject: [PATCH] test: cover bundle-size-summary.js and fail loudly on missing entrypoints Addresses review feedback: validate every tracked entrypoint is present in stats.json (fail instead of silently skipping) and add unit tests for the summary script. Co-Authored-By: Claude Opus 4.8 --- .../scripts/bundle-size-summary.js | 11 +- .../spec/scripts/bundle-size-summary.test.js | 107 ++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 superset-frontend/spec/scripts/bundle-size-summary.test.js diff --git a/superset-frontend/scripts/bundle-size-summary.js b/superset-frontend/scripts/bundle-size-summary.js index 26561b1e1b2..5f9ef431d65 100644 --- a/superset-frontend/scripts/bundle-size-summary.js +++ b/superset-frontend/scripts/bundle-size-summary.js @@ -59,7 +59,10 @@ function main() { const results = []; TRACKED_ENTRYPOINTS.forEach(name => { const entrypoint = entrypoints[name]; - if (!entrypoint) return; + if (!entrypoint) { + console.error(`stats.json is missing the "${name}" entrypoint`); + process.exit(1); + } results.push({ name: `${name} entrypoint (JS)`, unit: 'bytes', @@ -75,4 +78,8 @@ function main() { console.log(JSON.stringify(results, null, 2)); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { entrypointSizeByExt, main, TRACKED_ENTRYPOINTS }; diff --git a/superset-frontend/spec/scripts/bundle-size-summary.test.js b/superset-frontend/spec/scripts/bundle-size-summary.test.js new file mode 100644 index 00000000000..a9d47e8326e --- /dev/null +++ b/superset-frontend/spec/scripts/bundle-size-summary.test.js @@ -0,0 +1,107 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +const fs = require('fs'); +const { + entrypointSizeByExt, + main, +} = require('../../scripts/bundle-size-summary'); + +function mockStats(entrypoints) { + jest + .spyOn(fs, 'readFileSync') + .mockReturnValue(JSON.stringify({ entrypoints })); +} + +function mockExit() { + return jest.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); +} + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('entrypointSizeByExt sums only assets matching the given extension', () => { + const entrypoint = { + assets: [ + { name: 'spa.entry.js', size: 100 }, + { name: 'spa.entry.js.map', size: 500 }, + { name: 'spa.entry.css', size: 20 }, + ], + }; + expect(entrypointSizeByExt(entrypoint, '.js')).toBe(100); + expect(entrypointSizeByExt(entrypoint, '.css')).toBe(20); +}); + +test('entrypointSizeByExt returns 0 when the entrypoint has no assets', () => { + expect(entrypointSizeByExt({}, '.js')).toBe(0); +}); + +test('main prints byte totals for every tracked entrypoint', () => { + mockStats({ + spa: { assets: [{ name: 'spa.js', size: 100 }] }, + embedded: { assets: [{ name: 'embedded.js', size: 50 }] }, + }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + process.argv = ['node', 'bundle-size-summary.js', 'stats.json']; + + main(); + + const printed = JSON.parse(logSpy.mock.calls[0][0]); + expect(printed).toEqual([ + { name: 'spa entrypoint (JS)', unit: 'bytes', value: 100 }, + { name: 'spa entrypoint (CSS)', unit: 'bytes', value: 0 }, + { name: 'embedded entrypoint (JS)', unit: 'bytes', value: 50 }, + { name: 'embedded entrypoint (CSS)', unit: 'bytes', value: 0 }, + ]); +}); + +test('main exits with an error when a tracked entrypoint is missing from stats.json', () => { + mockStats({ spa: { assets: [] } }); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockExit(); + process.argv = ['node', 'bundle-size-summary.js', 'stats.json']; + + expect(main).toThrow('process.exit called'); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('missing the "embedded" entrypoint'), + ); +}); + +test('main exits with an error when stats.json has no `entrypoints` key', () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({})); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockExit(); + process.argv = ['node', 'bundle-size-summary.js', 'stats.json']; + + expect(main).toThrow('process.exit called'); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('no `entrypoints` key'), + ); +}); + +test('main prints a usage message and exits when no stats path is given', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockExit(); + process.argv = ['node', 'bundle-size-summary.js']; + + expect(main).toThrow('process.exit called'); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Usage:')); +});