Back to Repositories

Validating Duration Plugin Implementation in dayjs

This test suite validates the Duration plugin functionality in Day.js, covering duration creation, manipulation, formatting, and localization features. The tests ensure accurate time duration calculations and conversions across different time units.

Test Coverage Overview

The test suite provides comprehensive coverage of the Duration plugin’s core functionality:
  • Duration creation with various input formats (milliseconds, ISO strings, objects)
  • Time unit conversions and calculations
  • Duration manipulation (add/subtract)
  • Formatting and humanization features
  • Localization support
  • Edge cases including negative durations and floating-point precision

Implementation Analysis

The testing approach employs Jest’s describe/it pattern for organized test grouping. Mock dates are used to ensure consistent testing conditions.

Key testing patterns include:
  • Input validation across multiple formats
  • Precision testing for calculations
  • Locale-specific behavior verification
  • Integration with core dayjs functionality

Technical Details

Testing infrastructure includes:
  • Jest as the primary testing framework
  • MockDate for date manipulation
  • Multiple locale imports (fr, es)
  • Plugin extensions: duration and relativeTime
  • ISO string parsing and formatting validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Systematic test organization with clear describe blocks
  • Comprehensive edge case coverage
  • Consistent setup/teardown with beforeEach/afterEach
  • Precise floating-point comparisons
  • Isolated test cases with specific assertions

iamkun/dayjs

test/plugin/duration.test.js

            
import MockDate from 'mockdate'
import dayjs from '../../src'
import duration from '../../src/plugin/duration'
import relativeTime from '../../src/plugin/relativeTime'
import '../../src/locale/fr'
import '../../src/locale/es'

dayjs.extend(relativeTime)
dayjs.extend(duration)

beforeEach(() => {
  MockDate.set(new Date())
})

afterEach(() => {
  MockDate.reset()
})

describe('Creating', () => {
  it('no argument', () => {
    expect(dayjs.duration().toISOString()).toBe('P0D')
    expect(dayjs.duration().asMilliseconds()).toBe(0)
  })
  it('milliseconds', () => {
    expect(dayjs.duration(1, 'ms').toISOString()).toBe('PT0.001S')
    expect(dayjs.duration(100).toISOString()).toBe('PT0.1S')
    expect(dayjs.duration(1000).toISOString()).toBe('PT1S')
  })
  it('two argument will bubble up to the next', () => {
    expect(dayjs.duration(59, 'seconds').toISOString()).toBe('PT59S')
    expect(dayjs.duration(60, 'seconds').toISOString()).toBe('PT1M')
    expect(dayjs.duration(13213, 'seconds').toISOString()).toBe('PT3H40M13S')
  })
  it('two argument will bubble up to the next (negative number)', () => {
    expect(dayjs.duration(-59, 'seconds').toISOString()).toBe('-PT59S')
    expect(dayjs.duration(-60, 'seconds').toISOString()).toBe('-PT1M')
    expect(dayjs.duration(-13213, 'seconds').toISOString()).toBe('-PT3H40M13S')
  })
  it('object with float', () => {
    expect(dayjs.duration({
      seconds: 1,
      minutes: 2,
      hours: 3,
      days: 4,
      months: 6,
      years: 7
    }).toISOString()).toBe('P7Y6M4DT3H2M1S')
  })
  it('object with weeks and float', () => {
    expect(dayjs.duration({
      seconds: 1.1,
      minutes: 2,
      hours: 3,
      days: 4,
      weeks: 5,
      months: 6,
      years: 7
    }).toISOString()).toBe('P7Y6M39DT3H2M1.1S')
  })
  it('object with millisecond', () => {
    expect(dayjs.duration({
      ms: 1
    }).toISOString()).toBe('PT0.001S')
  })
  it('object with negative millisecond', () => {
    expect(dayjs.duration({
      ms: -1
    }).toISOString()).toBe('-PT0.001S')
  })
  it('convert to milliseconds', () => {
    expect(+dayjs.duration(100)).toBe(100)
  })
  it('handles rounding to millisecond precision', () => {
    expect(dayjs.duration(2 / 3).toISOString()).toBe('PT0.001S')
  })
  it('should handle round with millisecond precision when negative', () => {
    expect(dayjs.duration(1000.5).toISOString()).toBe('PT1.001S')
    expect(dayjs.duration(-1000.5).toISOString()).toBe('-PT1S')
  })
  it('should handle floating point rounding errors', () => {
    // An example of this is when adding 2 to 0.812 seconds, which is how
    // the seconds component is calculated in .toISOString().
    // > 2 + 0.812
    // 2.8120000000000003
    expect(dayjs.duration(-2812).toISOString()).toBe('-PT2.812S') // was -PT2.8120000000000003S
    expect(dayjs.duration(3121632.27382247).toISOString()).toBe('PT52M1.632S') // was PT52M1.6320000000000001S
    expect(dayjs.duration(7647826.525774224).toISOString()).toBe('PT2H7M27.827S') // was PT2H7M27.826999999999998S
  })
})

describe('Parse ISO string', () => {
  it('Full ISO string', () => {
    expect(dayjs.duration('P7Y6M4DT3H2M1S').toISOString()).toBe('P7Y6M4DT3H2M1S')
  })
  it('Part ISO string', () => {
    expect(dayjs.duration('PT2777H46M40S').toISOString()).toBe('PT2777H46M40S')
  })
  it('Formatting missing components', () => {
    expect(dayjs.duration('PT1H').format('YYYY-MM-DDTHH:mm:ss')).toBe('0000-00-00T01:00:00')
  })
  it('ISO string with week', () => {
    const d = dayjs.duration('P2M3W4D')
    expect(d.toISOString()).toBe('P2M25D')
    expect(d.asDays()).toBe(85.83333333333333) // moment 86, count 2M as 61 days
    expect(d.asWeeks()).toBe(12.261904761904763) // moment 12.285714285714286
    expect(d.asMonths()).toBe(2.8219178082191783) // moment 2.8213721020965523
  })
  it('Invalid ISO string', () => {
    expect(dayjs.duration('Invalid').toISOString()).toBe('P0D')
  })
})

it('Is duration', () => {
  expect(dayjs.isDuration(dayjs.duration())).toBe(true)
  expect(dayjs.isDuration(dayjs.duration(1))).toBe(true)
  expect(dayjs.isDuration(dayjs())).toBe(false)
  expect(dayjs.isDuration({})).toBe(false)
  expect(dayjs.isDuration()).toBe(false)
})

it('toJSON', () => {
  expect(JSON.stringify({
    postDuration: dayjs.duration(5, 'minutes')
  })).toBe('{"postDuration":"PT5M"}')
})

describe('Humanize', () => {
  it('Humaniz', () => {
    expect(dayjs.duration(1, 'minutes').humanize()).toBe('a minute')
    expect(dayjs.duration(2, 'minutes').humanize()).toBe('2 minutes')
    expect(dayjs.duration(24, 'hours').humanize()).toBe('a day')
    expect(dayjs.duration(1, 'minutes').humanize(true)).toBe('in a minute')
    expect(dayjs.duration(-1, 'minutes').humanize(true)).toBe('a minute ago')
  })

  it('Locale', () => {
    expect(dayjs.duration(1, 'minutes').humanize(true)).toBe('in a minute')
    expect(dayjs.duration(1, 'minutes').locale('fr').humanize(true)).toBe('dans une minute')
    expect(dayjs.duration(1, 'minutes').locale('es').humanize(true)).toBe('en un minuto')
  })
  it('Global Locale', () => {
    dayjs.locale('en')
    expect(dayjs.duration(1, 'minutes').humanize(true)).toBe('in a minute')
    dayjs.locale('fr')
    expect(dayjs.duration(1, 'minutes').humanize(true)).toBe('dans une minute')
    dayjs.locale('es')
    expect(dayjs.duration(1, 'minutes').humanize(true)).toBe('en un minuto')
    dayjs.locale('en')
  })
})

describe('Clone', () => {
  it('Locale clone', () => {
    const d = dayjs.duration(1, 'minutes').locale('fr')
    const r = 'dans une minute'
    expect(d.humanize(true)).toBe(r)
    expect(d.clone().humanize(true)).toBe(r)
  })
})

describe('Milliseconds', () => {
  expect(dayjs.duration(500).milliseconds()).toBe(500)
  expect(dayjs.duration(1500).milliseconds()).toBe(500)
  expect(dayjs.duration(15000).milliseconds()).toBe(0)
  expect(dayjs.duration(500).asMilliseconds()).toBe(500)
  expect(dayjs.duration(1500).asMilliseconds()).toBe(1500)
  expect(dayjs.duration(15000).asMilliseconds()).toBe(15000)
})

describe('Milliseconds', () => {
  describe('Positive number', () => {
    expect(dayjs.duration(500).milliseconds()).toBe(500)
    expect(dayjs.duration(1500).milliseconds()).toBe(500)
    expect(dayjs.duration(15000).milliseconds()).toBe(0)
    expect(dayjs.duration(500).asMilliseconds()).toBe(500)
    expect(dayjs.duration(1500).asMilliseconds()).toBe(1500)
    expect(dayjs.duration(15000).asMilliseconds()).toBe(15000)
  })

  describe('Negative number', () => {
    expect(dayjs.duration(-500).milliseconds()).toBe(-500)
    expect(dayjs.duration(-1500).milliseconds()).toBe(-500)
    expect(dayjs.duration(-15000).milliseconds()).toBe(0)
    expect(dayjs.duration(-500).asMilliseconds()).toBe(-500)
    expect(dayjs.duration(-1500).asMilliseconds()).toBe(-1500)
    expect(dayjs.duration(-15000).asMilliseconds()).toBe(-15000)
  })
})

describe('Add', () => {
  const a = dayjs.duration(1, 'days')
  const b = dayjs.duration(2, 'days')
  expect(a.add(b).days()).toBe(3)
  expect(a.add(1, 'days').days()).toBe(2)
  expect(a.add({ days: 5 }).days()).toBe(6)
})

describe('Add to a dayjs()', () => {
  const a = dayjs()
  const b = dayjs.duration({ hours: 7, minutes: 10 })
  expect(a.add(b)).toEqual(a.add(7, 'hours').add(10, 'minutes'))
})

test('Add duration', () => {
  const a = dayjs('2020-10-01')
  const days = dayjs.duration(2, 'days')
  expect(a.add(days).format('YYYY-MM-DD')).toBe('2020-10-03')

  const b = dayjs('2023-02-01 00:00:00')
  const p = dayjs.duration('P1Y1M1DT1H1M1S')
  expect(b.add(p).format('YYYY-MM-DD HH:mm:ss')).toBe('2024-03-02 01:01:01')
})

describe('Subtract', () => {
  const a = dayjs.duration(3, 'days')
  const b = dayjs.duration(2, 'days')
  expect(a.subtract(b).days()).toBe(1)
})

test('Subtract duration', () => {
  const a = dayjs('2020-10-20')
  const days = dayjs.duration(2, 'days')
  expect(a.subtract(days).format('YYYY-MM-DD')).toBe('2020-10-18')

  const b = dayjs('2023-03-02 02:02:02')
  const p = dayjs.duration('P1Y1M1DT1H1M1S')
  expect(b.subtract(p).format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-01 01:01:01')
})

describe('Seconds', () => {
  expect(dayjs.duration(500).seconds()).toBe(0)
  expect(dayjs.duration(1500).seconds()).toBe(1)
  expect(dayjs.duration(15000).seconds()).toBe(15)
  expect(dayjs.duration(61000).seconds()).toBe(1) // 1 minute 1 second
  expect(dayjs.duration(500).asSeconds()).toBe(0.5)
  expect(dayjs.duration(1500).asSeconds()).toBe(1.5)
  expect(dayjs.duration(15000).asSeconds()).toBe(15)
})

describe('Minutes', () => {
  expect(dayjs.duration(100000).minutes()).toBe(1)
  expect(dayjs.duration(61000).minutes()).toBe(1) // 1 minute 1 second
  expect(dayjs.duration(100000).asMinutes().toFixed(2)).toBe('1.67')
})

describe('Hours', () => {
  expect(dayjs.duration(10000000).hours()).toBe(2)
  expect(dayjs.duration(10000000).asHours().toFixed(2)).toBe('2.78')
})

describe('Days', () => {
  it('positive number', () => {
    expect(dayjs.duration(100000000).days()).toBe(1)
    expect(dayjs.duration(100000000).asDays().toFixed(2)).toBe('1.16')
  })

  it('negative number', () => {
    expect(dayjs.duration(-1).days()).toBe(0)
    expect(dayjs.duration(-86399999).asDays()).toBeCloseTo(-0.999999, 4)
  })
})

describe('Weeks', () => {
  expect(dayjs.duration(1000000000).weeks()).toBe(1)
  expect(dayjs.duration(1000000000).asWeeks().toFixed(2)).toBe('1.65')
})

describe('Month', () => {
  expect(dayjs.duration(10000000000).months()).toBe(3)
  expect(dayjs.duration({ months: 3 }).asMonths()).toBe(3)
})

describe('Years', () => {
  expect(dayjs.duration(100000000000).years()).toBe(3)
  expect(dayjs.duration(100000000000).asYears().toFixed(2)).toBe('3.17')
})

describe('prettyUnit', () => {
  const d = dayjs.duration(2, 's')
  expect(d.toISOString()).toBe('PT2S')
  expect(d.as('seconds')).toBe(2)
  expect(d.get('s')).toBe(2)
  expect(dayjs.duration({
    M: 12,
    m: 12
  }).toISOString()).toBe('P12MT12M')
})

describe('Format', () => {
  test('no formatStr', () => {
    const d = dayjs.duration(15, 'seconds')
      .add(13, 'hours')
      .add(35, 'minutes')
      .add(16, 'days')
      .add(10, 'months')
      .add(22, 'years')
    expect(d.format()).toBe('0022-10-16T13:35:15')
  })

  test('with formatStr for all tokens', () => {
    const d = dayjs.duration(1, 'seconds')
      .add(8, 'hours')
      .add(5, 'minutes')
      .add(6, 'days')
      .add(9, 'months')
      .add(2, 'years')
      .add(10, 'milliseconds')
    expect(d.format('Y/YY.YYYYTESTM:MM:D:DD:H:HH:m:mm:s:ss:SSS'))
      .toBe('2/02.0002TEST9:09:6:06:8:08:5:05:1:01:010')
  })
})