Bootstrap uses Vitest in browser mode, which runs each spec in a real Chromium through Playwright. Each plugin has a file dedicated to its tests in js/tests/unit/<plugin-name>.spec.js.
visual/ contains “visual” tests which are run interactively in real browsers and require manual verification by humans.types/ contains compile-time type tests. They are type-checked, never executed.vitest.config.mts holds the runner config, and vitest-setup.js runs before every spec.To run the unit test suite, run npm run js-test. That also builds the two integration bundles. To run only the unit specs, run npm run js-test-unit. To run the specs in a visible browser and debug them, run npm run js-debug.
Playwright needs its browser once per machine. If the run fails because Chromium is missing, run npx playwright install chromium.
The specs predate Vitest and are written against the Jasmine API. vitest-setup.js maps that API onto Vitest, so spyOn, spyOnProperty, jasmine.clock(), toHaveClass, toBeTrue, toBeFalse and toHaveSize all keep working. Two differences are worth knowing when you write a new spec.
spyOn stubs the method, the way Jasmine does. Vitest's own vi.spyOn calls the real method instead. Add .and.callThrough() when you want the real one.done callback, so it('…', done => …) no longer works.You can use the plain Vitest API in new specs. Import vi from vitest and prefer it for new work; the shim exists for the specs that already rely on Jasmine.
Bootstrap ships its own TypeScript declarations, so consumers need no @types/bootstrap. Two tests protect that public API. Both assert the same surface, but each checks a different artifact.
types/api.ts checks the source (js/src/*.ts). It imports the source with relative paths. Run it with npm run js-typecheck. It runs before the build, as part of npm run lint.types/consumer.ts checks the shipped declarations (js/dist/*.d.ts). It imports through the package name (bootstrap and bootstrap/js/src/*.js), exactly as a downstream project does. Node resolves both through the exports map. Run it with npm run js-typecheck-dist. It needs the build first, so it runs after npm run dist.A source-only test cannot catch a declaration-emit bug. For example, a leaked private type or a broken import inside a .d.ts passes api.ts but fails consumer.ts. Keep the two files in sync when you change the public API.
js/tests/unit/<plugin-name>.spec.js).npm run js-test-unit to see the results of your newly-added test(s).To run a single file, pass its path: npm run js-test-unit -- js/tests/unit/tab.spec.js.
Note: Your new unit tests should fail before your changes are applied to the plugin, and should pass after your changes are applied to the plugin.
describe.expect to ensure something is expected.Currently we’re aiming for at least 90% test coverage for our code. To ensure your changes meet or exceed this limit, run npm run js-test-unit and open the file in js/coverage/lcov-report/index.html to see the code coverage for each plugin. See more details when you select a plugin and ensure your change is fully covered by unit tests.
The run fails if coverage drops below the thresholds in vitest.config.mts: 90% of statements, functions and lines, and 88% of branches. Coverage counts every file in js/src, including the ones no spec imports, so an untested plugin shows as 0% instead of disappearing from the report.
// Synchronous test describe('getInstance', () => { it('should return null if there is no instance', () => { // Make assertion expect(Tab.getInstance(fixtureEl)).toBeNull() }) it('should return this instance', () => { fixtureEl.innerHTML = '<div></div>' const divEl = fixtureEl.querySelector('div') const tab = new Tab(divEl) // Make assertion expect(Tab.getInstance(divEl)).toEqual(tab) }) }) // Asynchronous test it('should show a tooltip without the animation', () => { return new Promise(resolve => { fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { animation: false }) tooltipEl.addEventListener('shown.bs.tooltip', () => { const tip = document.querySelector('.tooltip') expect(tip).not.toBeNull() expect(tip.classList.contains('fade')).toEqual(false) resolve() }) tooltip.show() }) })