Skip to content

Commit

Permalink
fix(date-assertion): Solve .toMatchDateParts inconsistencies (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseLion authored Jun 30, 2023
1 parent d9d5b92 commit f1002ed
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 116 deletions.
27 changes: 13 additions & 14 deletions package/src/lib/DateAssertion.ts
Original file line number Diff line number Diff line change
@@ -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<keyof DateOptions, DateMethod> = {
day: "getDay",
hours: "getHours",
miliseconds: "getMilliseconds",
milliseconds: "getMilliseconds",
minutes: "getMinutes",
month: "getMonth",
seconds: "getSeconds",
Expand Down Expand Up @@ -64,7 +64,7 @@ export class DateAssertion extends Assertion<Date> {
* 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.
*
Expand All @@ -73,7 +73,7 @@ export class DateAssertion extends Assertion<Date> {
* const septemberTenth2022 = new Date(2022, 8, 10);
*
* expect(octoberTenth2022).toMatchDateParts({
* month: 8,
* month: "august", // or just `8`
* year:2022,
* });
* ```
Expand All @@ -82,23 +82,22 @@ export class DateAssertion extends Assertion<Date> {
* @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,
});
Expand Down
2 changes: 1 addition & 1 deletion package/src/lib/DateAssertion.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
146 changes: 102 additions & 44 deletions package/src/lib/helpers/dates.ts
Original file line number Diff line number Diff line change
@@ -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;
}
21 changes: 8 additions & 13 deletions package/test/lib/DateAssertion.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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,
Expand All @@ -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,
});
});
Expand All @@ -81,21 +79,18 @@ 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,
year: 2021,
};
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);
Expand Down
Loading

0 comments on commit f1002ed

Please sign in to comment.