UNPKG

wavesurfer.js

Version:
374 lines (373 loc) 15.3 kB
import { MAX_CANVAS_WIDTH, MAX_NODES, calculateBarHeights, calculateBarRenderConfig, calculateBarSegments, calculateLinePaths, calculateScrollPercentages, calculateSingleCanvasWidth, calculateVerticalScale, calculateWaveformLayout, clampToUnit, clampWidthToBarGrid, getLazyRenderRange, getPixelRatio, getRelativePointerPosition, resolveBarYPosition, resolveChannelHeight, resolveColorValue, shouldClearCanvases, shouldRenderBars, sliceChannelData, } from '../renderer-utils.js'; describe('renderer-utils', () => { describe('clampToUnit', () => { it('clamps numbers to the [0, 1] range', () => { expect(clampToUnit(-0.5)).toBe(0); expect(clampToUnit(0.3)).toBe(0.3); expect(clampToUnit(1.8)).toBe(1); }); }); describe('calculateBarRenderConfig', () => { const options = { container: document.createElement('div'), barWidth: 2, barGap: 1, barRadius: 3, }; it('derives spacing values and scaling information', () => { const config = calculateBarRenderConfig({ width: 100, height: 50, length: 10, options, pixelRatio: 2, }); expect(config).toEqual({ halfHeight: 25, barWidth: 4, barGap: 2, barRadius: 3, barIndexScale: 100 / ((4 + 2) * 10), barSpacing: 6, barMinHeight: 0, }); }); }); describe('calculateBarHeights', () => { it('returns rounded heights and ensures total height is at least 1', () => { expect(calculateBarHeights({ maxTop: 0.5, maxBottom: 0.25, halfHeight: 20, vScale: 1, })).toEqual({ topHeight: 10, totalHeight: 15 }); expect(calculateBarHeights({ maxTop: 0, maxBottom: 0, halfHeight: 20, vScale: 1, })).toEqual({ topHeight: 0, totalHeight: 1 }); }); it('ensures total height is at least barMinHeight', () => { expect(calculateBarHeights({ maxTop: 0, maxBottom: 0, halfHeight: 20, vScale: 1, barMinHeight: 10, })).toEqual({ topHeight: 5, totalHeight: 10 }); expect(calculateBarHeights({ maxTop: 0, maxBottom: 0, halfHeight: 20, vScale: 1, barMinHeight: 10, barAlign: 'top', })).toEqual({ topHeight: 0, totalHeight: 10 }); }); }); describe('resolveBarYPosition', () => { const baseArgs = { halfHeight: 20, topHeight: 10, totalHeight: 20, canvasHeight: 40, }; it('positions bars relative to alignment', () => { expect(resolveBarYPosition(Object.assign({ barAlign: 'top' }, baseArgs))).toBe(0); expect(resolveBarYPosition(Object.assign({ barAlign: 'bottom' }, baseArgs))).toBe(20); expect(resolveBarYPosition(Object.assign({ barAlign: undefined }, baseArgs))).toBe(10); }); }); describe('calculateBarSegments', () => { const options = { container: document.createElement('div'), }; it('aggregates bar segments across the channel data', () => { const { barIndexScale, barSpacing, barWidth, halfHeight } = calculateBarRenderConfig({ width: 6, height: 20, length: 6, options, pixelRatio: 1, }); const segments = calculateBarSegments({ channelData: [ new Float32Array([0.2, -0.4, 0.6, -0.8, 1, -1]), new Float32Array([0.1, -0.2, 0.3, -0.4, 0.5, -0.6]), ], barIndexScale, barSpacing, barWidth, halfHeight, vScale: 1, canvasHeight: 40, barAlign: undefined, barMinHeight: 0, }); expect(segments).toEqual([ { x: 0, y: 8, width: 1, height: 3 }, { x: 1, y: 6, width: 1, height: 6 }, { x: 2, y: 4, width: 1, height: 9 }, { x: 3, y: 2, width: 1, height: 12 }, { x: 4, y: 0, width: 1, height: 15 }, { x: 5, y: 0, width: 1, height: 16 }, ]); }); it('ensures bars are at least barMinHeight tall', () => { const height = 40; const length = 10; const { barIndexScale, barSpacing, barWidth, halfHeight } = calculateBarRenderConfig({ width: 100, height, length, options, pixelRatio: 1, }); const segments = calculateBarSegments({ channelData: [ new Float32Array(length).fill(0.001), // Very small values ], barIndexScale, barSpacing, barWidth, halfHeight, vScale: 1, canvasHeight: height / 2, barAlign: undefined, barMinHeight: 10, }); expect(segments.length).toBeGreaterThan(0); expect(segments[0].height).toBe(10); expect(segments[0].y).toBe(15); // Centered: 20 - 10/2 }); }); describe('getRelativePointerPosition', () => { it('returns pointer coordinates as relative offsets', () => { const rect = { left: 10, top: 20, width: 200, height: 100, }; expect(getRelativePointerPosition(rect, 110, 70)).toEqual([0.5, 0.5]); }); }); describe('resolveChannelHeight', () => { it('returns numeric height when provided', () => { expect(resolveChannelHeight({ optionsHeight: 150, parentHeight: 0, numberOfChannels: 2, })).toBe(150); }); it('splits height across channels when auto with overlays disabled', () => { const splitChannels = [{ overlay: false }, { overlay: false }]; expect(resolveChannelHeight({ optionsHeight: 'auto', optionsSplitChannels: splitChannels, parentHeight: 200, numberOfChannels: 2, })).toBe(100); }); it('falls back to default height when invalid', () => { expect(resolveChannelHeight({ optionsHeight: 'invalid', parentHeight: 0, numberOfChannels: 2, defaultHeight: 75, })).toBe(75); }); }); describe('getPixelRatio', () => { it('never returns less than 1', () => { expect(getPixelRatio(undefined)).toBe(1); expect(getPixelRatio(0.5)).toBe(1); expect(getPixelRatio(2)).toBe(2); }); }); describe('shouldRenderBars', () => { const options = { container: document.createElement('div') }; it('returns true when any bar option is configured', () => { expect(shouldRenderBars(Object.assign(Object.assign({}, options), { barWidth: 1 }))).toBe(true); expect(shouldRenderBars(Object.assign(Object.assign({}, options), { barGap: 2 }))).toBe(true); expect(shouldRenderBars(Object.assign(Object.assign({}, options), { barAlign: 'top' }))).toBe(true); }); it('returns false when bars are not configured', () => { expect(shouldRenderBars(options)).toBe(false); }); }); describe('resolveColorValue', () => { const canvas = document.createElement('canvas'); let createLinearGradient; let addColorStop; beforeEach(() => { createLinearGradient = jest.fn(() => ({ addColorStop })); addColorStop = jest.fn(); jest.spyOn(document, 'createElement').mockImplementation(() => canvas); jest .spyOn(canvas, 'getContext') .mockImplementation(() => ({ createLinearGradient })); }); afterEach(() => { jest.restoreAllMocks(); }); it('returns string values unchanged', () => { expect(resolveColorValue('#000', 2)).toBe('#000'); }); it('falls back to default gray when gradient list is empty', () => { expect(resolveColorValue([], 2)).toBe('#999'); }); it('uses the single color when gradient list has one item', () => { expect(resolveColorValue(['#111'], 2)).toBe('#111'); }); it('creates a canvas gradient for multiple colors', () => { const gradient = resolveColorValue(['#000', '#fff'], 2); expect(createLinearGradient).toHaveBeenCalledWith(0, 0, 0, 300); expect(addColorStop).toHaveBeenCalledTimes(2); expect(addColorStop).toHaveBeenNthCalledWith(1, 0, '#000'); expect(addColorStop).toHaveBeenNthCalledWith(2, 1, '#fff'); expect(gradient.addColorStop).toBe(addColorStop); }); }); describe('calculateWaveformLayout', () => { const baseArgs = { duration: 2, parentWidth: 300, pixelRatio: 1, }; it('uses parent width when not scrollable and fillParent is true', () => { expect(calculateWaveformLayout(Object.assign(Object.assign({}, baseArgs), { minPxPerSec: 10, fillParent: true }))).toEqual({ scrollWidth: 20, isScrollable: false, useParentWidth: true, width: 300, }); }); it('uses scroll width when waveform exceeds parent width', () => { expect(calculateWaveformLayout(Object.assign(Object.assign({}, baseArgs), { minPxPerSec: 500, fillParent: true }))).toEqual({ scrollWidth: 1000, isScrollable: true, useParentWidth: false, width: 1000, }); }); }); describe('clampWidthToBarGrid', () => { const options = { container: document.createElement('div'), barWidth: 2, barGap: 1 }; it('returns original width when bars are disabled', () => { expect(clampWidthToBarGrid(123, { container: document.createElement('div') })).toBe(123); }); it('clamps width down to align with bar grid spacing', () => { expect(clampWidthToBarGrid(10, options)).toBe(9); }); }); describe('calculateSingleCanvasWidth', () => { const options = { container: document.createElement('div'), barWidth: 2, barGap: 1 }; it('limits width by canvas cap, client size, and total width', () => { expect(calculateSingleCanvasWidth({ clientWidth: 9000, totalWidth: 5000, options, })).toBe(clampWidthToBarGrid(Math.min(MAX_CANVAS_WIDTH, 5000), options)); }); }); describe('sliceChannelData', () => { it('returns proportional slices based on offset and width', () => { const channel = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8]); const slices = sliceChannelData({ channelData: [channel, channel], offset: 100, clampedWidth: 50, totalWidth: 200, }); expect(slices[0]).toEqual(new Float32Array([5, 6])); expect(slices[1]).toEqual(new Float32Array([5, 6])); }); }); describe('shouldClearCanvases', () => { it('clears when exceeding maximum nodes', () => { expect(shouldClearCanvases(MAX_NODES)).toBe(false); expect(shouldClearCanvases(MAX_NODES + 1)).toBe(true); }); }); describe('getLazyRenderRange', () => { it('returns surrounding canvas indices', () => { expect(getLazyRenderRange({ scrollLeft: 50, totalWidth: 200, numCanvases: 5, })).toEqual([0, 1, 2]); }); it('defaults to the first canvas when width is zero', () => { expect(getLazyRenderRange({ scrollLeft: 0, totalWidth: 0, numCanvases: 3 })).toEqual([0]); }); }); describe('calculateVerticalScale', () => { it('returns base scale when not normalizing', () => { expect(calculateVerticalScale({ channelData: [new Float32Array([0.5])], barHeight: 2, normalize: false, })).toBe(2); }); it('normalizes against the maximum magnitude when requested', () => { expect(calculateVerticalScale({ channelData: [new Float32Array([0.25, -0.5])], barHeight: 2, normalize: true, })).toBe(4); }); }); describe('calculateLinePaths', () => { it('produces symmetrical paths for mirrored channel data', () => { const [topPath, bottomPath] = calculateLinePaths({ channelData: [new Float32Array([0, 0.5, 1]), new Float32Array([0, 0.25, 0.75])], width: 6, height: 8, vScale: 1, }); expect(topPath[0]).toEqual({ x: 0, y: 4 }); expect(topPath[topPath.length - 1]).toEqual({ x: 6, y: 4 }); expect(bottomPath[0]).toEqual({ x: 0, y: 4 }); expect(bottomPath[bottomPath.length - 1]).toEqual({ x: 6, y: 4 }); expect(topPath).toEqual([ { x: 0, y: 4 }, { x: 0, y: 3 }, { x: 2, y: 2 }, { x: 4, y: 0 }, { x: 6, y: 4 }, ]); expect(bottomPath).toEqual([ { x: 0, y: 4 }, { x: 0, y: 5 }, { x: 2, y: 5 }, { x: 4, y: 7 }, { x: 6, y: 4 }, ]); }); }); describe('calculateScrollPercentages', () => { it('returns full range when scroll width is zero', () => { expect(calculateScrollPercentages({ scrollLeft: 0, clientWidth: 100, scrollWidth: 0, })).toEqual({ startX: 0, endX: 1 }); }); it('returns start and end ratios relative to scroll width', () => { expect(calculateScrollPercentages({ scrollLeft: 50, clientWidth: 100, scrollWidth: 400, })).toEqual({ startX: 0.125, endX: 0.375 }); }); it('clamps values to 0-1 range', () => { expect(calculateScrollPercentages({ scrollLeft: -10, clientWidth: 100, scrollWidth: 400, })).toEqual({ startX: 0, endX: 0.225 }); }); }); });