From a4272b3055573fb9cc5ae9c70693a327228f2181 Mon Sep 17 00:00:00 2001 From: Jose Luis Leon Date: Thu, 29 Jun 2023 17:08:28 -0500 Subject: [PATCH] fix(date-assertion): Solve .toMatchDateParts inconsistencies --- package/src/lib/DateAssertion.ts | 27 +++-- package/src/lib/DateAssertion.types.ts | 2 +- package/src/lib/helpers/dates.ts | 146 +++++++++++++++++-------- package/test/lib/DateAssertion.test.ts | 21 ++-- package/test/lib/helpers/dates.test.ts | 133 ++++++++++++++-------- 5 files changed, 213 insertions(+), 116 deletions(-) diff --git a/package/src/lib/DateAssertion.ts b/package/src/lib/DateAssertion.ts index fe06b97e..9ffd4e72 100644 --- a/package/src/lib/DateAssertion.ts +++ b/package/src/lib/DateAssertion.ts @@ -1,13 +1,13 @@ import { Assertion } from "./Assertion"; import { DateMethod, DateOptions, DayOfWeek } from "./DateAssertion.types"; -import { dateOptionsToDate, dayOfWeekAsNumber } from "./helpers/dates"; +import { optionsToDate, dayOfWeekAsNumber, dateToOptions } from "./helpers/dates"; import { AssertionError } from "assert"; const DATE_METHOD_MAP: Record = { day: "getDay", hours: "getHours", - miliseconds: "getMilliseconds", + milliseconds: "getMilliseconds", minutes: "getMinutes", month: "getMonth", seconds: "getSeconds", @@ -64,7 +64,7 @@ export class DateAssertion extends Assertion { * Check if two dates are equal or partially equal * by using a configuration object that can contain * optional specifications for: year, month, day, hour, - * minutes, seconds and miliseconds, equals the actual date. + * minutes, seconds and milliseconds, equals the actual date. * The test fails when the value of one of the specifications * doesn't match the actual date. * @@ -73,7 +73,7 @@ export class DateAssertion extends Assertion { * const septemberTenth2022 = new Date(2022, 8, 10); * * expect(octoberTenth2022).toMatchDateParts({ - * month: 8, + * month: "august", // or just `8` * year:2022, * }); * ``` @@ -82,23 +82,22 @@ export class DateAssertion extends Assertion { * @returns the assertion instance */ public toMatchDateParts(options: DateOptions): this { - const optionsAsDate = dateOptionsToDate(options); - const assertWhen = Object.keys(options).every(key => { - const dateMethod = DATE_METHOD_MAP[key]; - return optionsAsDate[dateMethod]() === this.actual[dateMethod](); - }); + const optionsAsDate = optionsToDate(options); const error = new AssertionError({ - actual: this.actual, + actual: dateToOptions(this.actual, options), expected: options, - message: `Expected <${this.actual.toISOString()}> to be equal to <${optionsAsDate.toISOString()}>`, + message: `Expected <${this.actual.toISOString()}> to have parts <${JSON.stringify(options)}>`, }); const invertedError = new AssertionError({ - actual: this.actual, - message: `Expected <${this.actual.toISOString()}> NOT to be equal to <${optionsAsDate.toISOString()}>`, + actual: dateToOptions(this.actual, options), + message: `Expected <${this.actual.toISOString()}> NOT to have parts <${JSON.stringify(options)}>`, }); return this.execute({ - assertWhen, + assertWhen: Object.keys(options).every(key => { + const dateMethod = DATE_METHOD_MAP[key]; + return optionsAsDate[dateMethod]() === this.actual[dateMethod](); + }), error, invertedError, }); diff --git a/package/src/lib/DateAssertion.types.ts b/package/src/lib/DateAssertion.types.ts index 8236020a..f0612a3d 100644 --- a/package/src/lib/DateAssertion.types.ts +++ b/package/src/lib/DateAssertion.types.ts @@ -30,7 +30,7 @@ export type DateMethod = { export interface DateOptions { day?: DayOfWeek | number; hours?: number; - miliseconds?: number; + milliseconds?: number; minutes?: number; month?: Month | number; seconds?: number; diff --git a/package/src/lib/helpers/dates.ts b/package/src/lib/helpers/dates.ts index 491df103..9449c9d4 100644 --- a/package/src/lib/helpers/dates.ts +++ b/package/src/lib/helpers/dates.ts @@ -1,50 +1,108 @@ import { DateOptions, DayOfWeek, Month } from "../DateAssertion.types"; +const DAYS_OF_WEEK: DayOfWeek[] = [ + "sunday", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", +]; + +const MONTHS: Month[] = [ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december", +]; + +/** + * Provides a numeric representation of a day of the week string. The number is + * consistent with JavaScript's {@link Date}, so it's zero-based and starts + * from Sunday = `0` to Saturday = `6`. + * + * @param day a day of the week string + * @returns a number representing the day of the week + */ export function dayOfWeekAsNumber (day: DayOfWeek): number { - switch (day) { - case "sunday": return 0; - case "monday": return 1; - case "tuesday": return 2; - case "wednesday": return 3; - case "thursday": return 4; - case "friday": return 5; - case "saturday": return 6; - } - } + return DAYS_OF_WEEK.indexOf(day); +} - export function monthOfYear (month: Month): number { - switch (month) { - case "january": return 0; - case "february": return 1; - case "march": return 2; - case "april": return 3; - case "may": return 4; - case "june": return 5; - case "july": return 6; - case "august": return 7; - case "september": return 8; - case "october": return 9; - case "november": return 10; - case "december": return 11; - } - } +/** + * Provides a numeric representation of a month string. The number is consistent + * with JavaScript's {@link Date}, so it's zero-based and starts from + * January = `0` to December = `11`. + * + * @param month a month string + * @returns a number representing the month + */ +export function monthOfYear (month: Month): number { + return MONTHS.indexOf(month); +} + +export function optionsToDate(options: DateOptions): Date { + const { + year = 0, + month = 0, + day = 0, + hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0, + } = options; + const monthAsNum = typeof month === "string" + ? monthOfYear(month) + 1 + : month; + const dayAsNum = typeof day === "string" + ? dayOfWeekAsNumber(day) + : day; - export function dateOptionsToDate(options: DateOptions): Date { - const { year, month, day, hours, minutes, seconds, miliseconds } = options; - const monthAsNum = typeof month === "string" - ? monthOfYear(month) - : month; - const dayAsNum = typeof day === "string" - ? dayOfWeekAsNumber(day) - : day; - const today = new Date(); - return new Date( - year ?? today.getFullYear(), - monthAsNum ?? today.getMonth(), - dayAsNum ?? today.getDate(), - hours ?? today.getHours(), - minutes ?? today.getMinutes(), - seconds ?? today.getSeconds(), - miliseconds ?? today.getMilliseconds(), - ); + return new Date( + year, + monthAsNum, + dayAsNum, + hours, + minutes, + seconds, + milliseconds, + ); +} + +export function dateToOptions(date: Date, sample?: DateOptions): DateOptions { + const options = { + day: date.getDate(), + hours: date.getHours(), + milliseconds: date.getMilliseconds(), + minutes: date.getMinutes(), + month: date.getMonth(), + seconds: date.getSeconds(), + year: date.getFullYear(), + }; + + if (sample !== undefined) { + return Object.keys(sample).reduce((acc, key) => { + const dayOrMonth = key === "day" + ? DAYS_OF_WEEK[date.getDay()] + : MONTHS[date.getMonth()]; + const value = typeof sample[key] === "string" + ? dayOrMonth + : options[key]; + + return { + ...acc, + [key]: value, + }; + }, { } as DateOptions); } + + return options; +} diff --git a/package/test/lib/DateAssertion.test.ts b/package/test/lib/DateAssertion.test.ts index 5b196842..08b80287 100644 --- a/package/test/lib/DateAssertion.test.ts +++ b/package/test/lib/DateAssertion.test.ts @@ -1,7 +1,8 @@ import dedent from "@cometlib/dedent"; import { DateAssertion } from "../../src/lib/DateAssertion"; -import { dateOptionsToDate, dayOfWeekAsNumber } from "../../src/lib/helpers/dates"; +import { DateOptions } from "../../src/lib/DateAssertion.types"; +import { dayOfWeekAsNumber } from "../../src/lib/helpers/dates"; import assert, { AssertionError } from "assert"; @@ -47,10 +48,10 @@ describe("[Unit] DateAssertion.test.ts", () => { context("when the actual date matches the passed date", () => { it("returns the assertion instance", () => { const actualDate = new Date(2021, 1, 1, 12, 10, 15, 25); - const options = { + const options: DateOptions = { day: 1, hours: 12, - miliseconds: 25, + milliseconds: 25, minutes: 10, month: 1, seconds: 15, @@ -59,10 +60,7 @@ describe("[Unit] DateAssertion.test.ts", () => { const test = new DateAssertion(actualDate); assert.deepStrictEqual(test.toMatchDateParts(options), test); assert.throws(() => test.not.toMatchDateParts(options), { - message: dedent` - Expected <${actualDate.toISOString()}> NOT to be equal to \ - <${dateOptionsToDate(options).toISOString()}> - `, + message: `Expected <${actualDate.toISOString()}> NOT to have parts <${JSON.stringify(options)}>`, name: AssertionError.name, }); }); @@ -81,10 +79,10 @@ describe("[Unit] DateAssertion.test.ts", () => { context("when the actual date is NOT equal to the passed date", () => { it("throws an assertion error", () => { const actualDate = new Date(2021, 1, 1, 12, 10, 15, 25); - const options = { + const options: DateOptions = { day: 1, hours: 12, - miliseconds: 24, + milliseconds: 24, minutes: 10, month: 1, seconds: 15, @@ -92,10 +90,7 @@ describe("[Unit] DateAssertion.test.ts", () => { }; const test = new DateAssertion(actualDate); assert.throws(() => test.toMatchDateParts(options), { - message: dedent` - Expected <${actualDate.toISOString()}> to be equal to \ - <${dateOptionsToDate(options).toISOString()}> - `, + message: `Expected <${actualDate.toISOString()}> to have parts <${JSON.stringify(options)}>`, name: AssertionError.name, }); assert.deepStrictEqual(test.not.toMatchDateParts(options), test); diff --git a/package/test/lib/helpers/dates.test.ts b/package/test/lib/helpers/dates.test.ts index d7bf3107..7e05239d 100644 --- a/package/test/lib/helpers/dates.test.ts +++ b/package/test/lib/helpers/dates.test.ts @@ -1,59 +1,104 @@ -import Sinon from "sinon"; - -import { dateOptionsToDate } from "../../../src/lib/helpers/dates"; +import { DateOptions } from "../../../src/lib/DateAssertion.types"; +import { dateToOptions, dayOfWeekAsNumber, monthOfYear, optionsToDate } from "../../../src/lib/helpers/dates"; import assert from "assert"; describe("[Unit] dates.test.ts", () => { - context("when the object has all options", () => { - it("returns a new date with the given options", () => { - const options = { - day: 1, - hours: 12, - miliseconds: 25, - minutes: 10, - month: 1, - seconds: 15, - year: 2021, - }; - - assert.deepStrictEqual( - dateOptionsToDate(options), - new Date(2021, 1, 1, 12, 10, 15, 25), - ); + describe(".dayOfWeekAsNumber", () => { + it("returns the numeric representation of a day of the week string", () => { + const day = dayOfWeekAsNumber("wednesday"); + + assert.deepStrictEqual(day, 3); + }); + }); + + describe(".monthOfYear", () => { + it("returns the numeric representation of a month string", () => { + const month = monthOfYear("march"); + + assert.deepStrictEqual(month, 2); }); }); - context("when the object has some options", () => { - it("returns today's date for the missing options", () => { - const today = new Date(); - const options = { - day: 2, - month: 2, - seconds: 30, - year: 2021, - }; - const expected = new Date( - 2021, - 2, - 2, - today.getHours(), - today.getMinutes(), - 30, - today.getMilliseconds(), - ); - Sinon.useFakeTimers(today); - - assert.deepStrictEqual(dateOptionsToDate(options), expected); + describe(".optionsToDate", () => { + context("when the object has all options", () => { + it("returns a new date with the given options", () => { + const options: DateOptions = { + day: 1, + hours: 12, + milliseconds: 25, + minutes: 10, + month: 1, + seconds: 15, + year: 2021, + }; + const expected = new Date(2021, 1, 1, 12, 10, 15, 25); + + assert.deepStrictEqual(optionsToDate(options), expected); + }); + }); + + context("when the object has some options", () => { + it("returns zero on the missing options", () => { + const options = { + day: 2, + month: 2, + seconds: 30, + year: 2021, + }; + const expected = new Date(2021, 2, 2, 0, 0, 30, 0); + + assert.deepStrictEqual(optionsToDate(options), expected); + }); }); }); context("when the object has no options", () => { - it("returns today's date", () => { - const today = new Date(); - Sinon.useFakeTimers(today); + it("returns an all zeros date", () => { + const expected = new Date(0, 0, 0, 0, 0, 0, 0); + + assert.deepStrictEqual(optionsToDate({ }), expected); + }); + }); + + describe(".dateToOptions", () => { + context("when a sample is not provided", () => { + it("transforms all the values of the date all as numbers", () => { + const date = new Date("2023-06-29T14:30:15.125Z"); + const expected: DateOptions = { + day: 29, + hours: 14 - (date.getTimezoneOffset() / 60), + milliseconds: 125, + minutes: 30, + month: 5, + seconds: 15, + year: 2023, + }; + + assert.deepStrictEqual(dateToOptions(date), expected); + }); + }); + + context("when a sample is provided", () => { + context("and not all values are present in the sample", () => { + it("transforms only the values present in the sample", () => { + const date = new Date("2023-06-29T14:30:15.125Z"); + const sample: DateOptions = { day: 5, minutes: 0 }; + const expected: DateOptions = { day: 29, minutes: 30 }; + + assert.deepStrictEqual(dateToOptions(date, sample), expected); + }); + }); + + context("and the month and the day of the week in the sample are strings", () => { + it("transforms the month and the day of the week as strings as well", () => { + const date = new Date("2023-06-29T14:30:15.125Z"); + const sample: DateOptions = { day: "saturday", month: "august", year: 2020 }; + const expected: DateOptions = { day: "thursday", month: "june", year: 2023 }; - assert.deepStrictEqual(dateOptionsToDate({ }), today); + assert.deepStrictEqual(dateToOptions(date, sample), expected); + }); + }); }); }); });