@meetelise/chat 1.55.1 → 1.55.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
+ import isBefore from "date-fns/isBefore";
2
+ import startOfToday from "date-fns/startOfToday";
1
3
  import { LitElement, html, TemplateResult } from "lit";
2
4
  import { customElement, property, state } from "lit/decorators.js";
3
5
  import { LayoutOption } from "../../../../fetchBuildingWebchatView";
4
6
  import { ALL_LAYOUTS_OPTION, DEFAULT_LEASE_TERM } from "../../constants";
5
7
  import { Unit } from "../../../../services/fees/fetchBuildingUnits";
6
8
  import floorPlanSelectorStyles from "./floor-plan-selector-styles";
7
- import { TODAY } from "../../../../globals";
9
+ import { formatLocalDate, getTodayString } from "../../utils/date-utils";
8
10
 
9
11
  import "../floorplan-image-card/floorplan-image-card";
10
12
 
@@ -46,7 +48,7 @@ export class FloorPlanSelector extends LitElement {
46
48
  onLeaseTermChange: ((leaseTerm: number) => void) | null = null;
47
49
 
48
50
  @state()
49
- moveInDate: Date | null = TODAY;
51
+ moveInDate: Date | null = startOfToday();
50
52
 
51
53
  @state()
52
54
  leaseTerm = DEFAULT_LEASE_TERM;
@@ -126,19 +128,28 @@ export class FloorPlanSelector extends LitElement {
126
128
  handleMoveInDateChange = (e: Event): void => {
127
129
  const inputElement = e.target as HTMLInputElement;
128
130
  const inputValue = inputElement.value;
131
+ const today = startOfToday();
129
132
 
130
133
  if (!inputValue) {
131
- this.moveInDate = TODAY;
134
+ this.moveInDate = today;
132
135
  } else {
133
136
  const [year, month, day] = inputValue.split("-").map(Number);
134
- this.moveInDate = new Date(year, month - 1, day);
137
+ const moveInDate = new Date(year, month - 1, day);
138
+ // Typing skips `min`, and Chrome reports each digit of a typed year (0002, 0020...) as a date.
139
+ // Ignore past dates instead of resetting the input mid-typing.
140
+ if (isBefore(moveInDate, today)) return;
141
+ this.moveInDate = moveInDate;
135
142
  }
136
143
 
137
- this.onMoveInDateChange?.(inputValue || TODAY.toISOString().split("T")[0]);
144
+ this.onMoveInDateChange?.(inputValue || formatLocalDate(today));
138
145
  };
139
146
 
140
147
  handleLeaseTermChange = (e: Event): void => {
141
- this.leaseTerm = parseInt((e.target as HTMLInputElement).value);
148
+ const leaseTerm = parseInt((e.target as HTMLInputElement).value);
149
+ // Clearing the box to retype it reports an empty value (NaN).
150
+ // Keep the last valid lease term instead of reloading units mid-edit.
151
+ if (!Number.isInteger(leaseTerm) || leaseTerm <= 0) return;
152
+ this.leaseTerm = leaseTerm;
142
153
  this.onLeaseTermChange?.(this.leaseTerm);
143
154
  };
144
155
 
@@ -225,7 +236,10 @@ export class FloorPlanSelector extends LitElement {
225
236
  <input
226
237
  type="date"
227
238
  placeholder="Select move in date"
228
- .value=${this.moveInDate?.toISOString().split("T")[0] ?? ""}
239
+ min=${getTodayString()}
240
+ .value=${this.moveInDate
241
+ ? formatLocalDate(this.moveInDate)
242
+ : ""}
229
243
  @input=${this.handleMoveInDateChange}
230
244
  />
231
245
  </div>
@@ -18,7 +18,7 @@ import { XOutlineIcon } from "../icons/XOutlineIcon";
18
18
  import { IncentiveV2 } from "../../types/incentive-v2";
19
19
  import { Unit } from "../../services/fees/fetchBuildingUnits";
20
20
  import { RentableItemSummary } from "./model/rentable-item-summary";
21
- import { TODAY } from "../../globals";
21
+ import { getTodayString } from "./utils/date-utils";
22
22
  import { MarketableFee, MarketableFeeFrequency } from "./model/marketable-fee";
23
23
  import { ChargeInputs, QuoteError } from "../../services/fees/calculateQuote";
24
24
  import { DesiredRentableItem } from "./model/desired-rentable-item";
@@ -96,7 +96,7 @@ export class FeeCalculator extends LitElement {
96
96
  private desiredRentableItems: DesiredRentableItem[] = [];
97
97
 
98
98
  @state()
99
- private moveInDateString: string = TODAY.toISOString().split("T")[0];
99
+ private moveInDateString: string = getTodayString();
100
100
 
101
101
  @state()
102
102
  private leaseTerm = DEFAULT_LEASE_TERM;
@@ -0,0 +1,17 @@
1
+ import format from "date-fns/format";
2
+ import startOfToday from "date-fns/startOfToday";
3
+
4
+ /**
5
+ * Formats a date as its local `yyyy-MM-dd` calendar date, the value format of a date input.
6
+ * `toISOString()` gives the UTC date instead, which can be a day off.
7
+ */
8
+ export function formatLocalDate(date: Date): string {
9
+ return format(date, "yyyy-MM-dd");
10
+ }
11
+
12
+ /**
13
+ * Gets the prospect's local date today as `yyyy-MM-dd`.
14
+ */
15
+ export function getTodayString(): string {
16
+ return formatLocalDate(startOfToday());
17
+ }
@@ -0,0 +1,71 @@
1
+ import { expect } from "@esm-bundle/chai";
2
+ import "../public/dist/index";
3
+ import type { FloorPlanSelector } from "./WebComponent/FeeCalculator/components/floor-plan-selector/floor-plan-selector";
4
+
5
+ describe("floor-plan-selector lease term", () => {
6
+ let selector: FloorPlanSelector;
7
+ let sentLeaseTerms: number[];
8
+
9
+ const getLeaseTermInput = (): HTMLInputElement => {
10
+ const input = selector.shadowRoot?.querySelector<HTMLInputElement>(
11
+ 'input[type="number"]'
12
+ );
13
+ if (!input) throw new Error("Expected lease term input");
14
+ return input;
15
+ };
16
+
17
+ const enterLeaseTerm = async (value: string): Promise<void> => {
18
+ const input = getLeaseTermInput();
19
+ input.value = value;
20
+ input.dispatchEvent(new Event("input"));
21
+ await selector.updateComplete;
22
+ };
23
+
24
+ beforeEach(async () => {
25
+ sentLeaseTerms = [];
26
+ selector = document.createElement(
27
+ "floor-plan-selector"
28
+ ) as FloorPlanSelector;
29
+ selector.onLeaseTermChange = (leaseTerm) => sentLeaseTerms.push(leaseTerm);
30
+ document.body.appendChild(selector);
31
+ await selector.updateComplete;
32
+ });
33
+
34
+ afterEach(() => {
35
+ selector.remove();
36
+ });
37
+
38
+ it("starts on the default lease term", () => {
39
+ expect(getLeaseTermInput().value).to.equal("12");
40
+ });
41
+
42
+ it("sends each lease term the prospect types", async () => {
43
+ await enterLeaseTerm("1");
44
+ await enterLeaseTerm("18");
45
+
46
+ expect(sentLeaseTerms).to.deep.equal([1, 18]);
47
+ });
48
+
49
+ it("keeps the box empty without sending anything while it is retyped", async () => {
50
+ await enterLeaseTerm("");
51
+ // A rerender, for example from new units arriving, must not refill the box.
52
+ selector.requestUpdate();
53
+ await selector.updateComplete;
54
+
55
+ expect(getLeaseTermInput().value).to.equal("");
56
+ expect(sentLeaseTerms).to.deep.equal([]);
57
+
58
+ await enterLeaseTerm("1");
59
+ await enterLeaseTerm("18");
60
+
61
+ expect(getLeaseTermInput().value).to.equal("18");
62
+ expect(sentLeaseTerms).to.deep.equal([1, 18]);
63
+ });
64
+
65
+ it("ignores lease terms below one month", async () => {
66
+ await enterLeaseTerm("0");
67
+ await enterLeaseTerm("-6");
68
+
69
+ expect(sentLeaseTerms).to.deep.equal([]);
70
+ });
71
+ });
@@ -0,0 +1,113 @@
1
+ import { expect } from "@esm-bundle/chai";
2
+ import {
3
+ restore,
4
+ useFakeTimers,
5
+ useFakeXMLHttpRequest,
6
+ } from "sinon/pkg/sinon-esm";
7
+ import "../public/dist/index";
8
+ import type { FloorPlanSelector } from "./WebComponent/FeeCalculator/components/floor-plan-selector/floor-plan-selector";
9
+ import type { FeeCalculator } from "./WebComponent/FeeCalculator/fee-calculator";
10
+ import { formatLocalDate } from "./WebComponent/FeeCalculator/utils/date-utils";
11
+
12
+ type FakeRequest = XMLHttpRequest & { url: string };
13
+
14
+ // Late evening, and far from the real date, so a date read at module load cannot match.
15
+ const NOW = new Date(2030, 4, 20, 23, 30);
16
+ const TODAY = "2030-05-20";
17
+
18
+ const waitFor = async <T>(
19
+ find: () => T | undefined,
20
+ description: string
21
+ ): Promise<T> => {
22
+ for (let attempt = 0; attempt < 100; attempt++) {
23
+ const found = find();
24
+ if (found) return found;
25
+ await new Promise((resolve) => setTimeout(resolve, 10));
26
+ }
27
+ throw new Error(`Timed out waiting for ${description}`);
28
+ };
29
+
30
+ beforeEach(() => {
31
+ useFakeTimers({ now: NOW, toFake: ["Date"] });
32
+ });
33
+
34
+ afterEach(() => {
35
+ restore();
36
+ });
37
+
38
+ it("formats the local calendar date at both ends of the day", () => {
39
+ expect(formatLocalDate(new Date(2030, 4, 20, 0, 0))).to.equal(TODAY);
40
+ expect(formatLocalDate(new Date(2030, 4, 20, 23, 59))).to.equal(TODAY);
41
+ });
42
+
43
+ describe("floor-plan-selector move-in date", () => {
44
+ let selector: FloorPlanSelector;
45
+ let sentDates: string[];
46
+
47
+ const getDateInput = (): HTMLInputElement => {
48
+ const input =
49
+ selector.shadowRoot?.querySelector<HTMLInputElement>(
50
+ 'input[type="date"]'
51
+ );
52
+ if (!input) throw new Error("Expected move-in date input");
53
+ return input;
54
+ };
55
+
56
+ const enterDate = async (value: string): Promise<void> => {
57
+ const input = getDateInput();
58
+ input.value = value;
59
+ input.dispatchEvent(new Event("input"));
60
+ await selector.updateComplete;
61
+ };
62
+
63
+ beforeEach(async () => {
64
+ sentDates = [];
65
+ selector = document.createElement(
66
+ "floor-plan-selector"
67
+ ) as FloorPlanSelector;
68
+ selector.onMoveInDateChange = (date) => sentDates.push(date);
69
+ document.body.appendChild(selector);
70
+ await selector.updateComplete;
71
+ });
72
+
73
+ afterEach(() => {
74
+ selector.remove();
75
+ });
76
+
77
+ it("starts on today and does not allow earlier dates", () => {
78
+ expect(getDateInput().value).to.equal(TODAY);
79
+ expect(getDateInput().min).to.equal(TODAY);
80
+ });
81
+
82
+ it("shows and sends the date the prospect picks", async () => {
83
+ await enterDate("2030-06-01");
84
+
85
+ expect(getDateInput().value).to.equal("2030-06-01");
86
+ expect(sentDates).to.deep.equal(["2030-06-01"]);
87
+ });
88
+
89
+ it("ignores typed dates before today", async () => {
90
+ // Chrome reports each digit of a typed year as a date, for example 0002-06-01.
91
+ await enterDate("2030-05-19");
92
+ await enterDate("0002-06-01");
93
+
94
+ expect(sentDates).to.deep.equal([]);
95
+ });
96
+ });
97
+
98
+ it("requests units for today's local date by default", async () => {
99
+ const fakeXhr = useFakeXMLHttpRequest();
100
+ const requests: FakeRequest[] = [];
101
+ fakeXhr.onCreate = (request: FakeRequest) => requests.push(request);
102
+ const calculator = document.createElement("fee-calculator") as FeeCalculator;
103
+ calculator.buildingSlug = "test-building";
104
+ document.body.appendChild(calculator);
105
+
106
+ const unitsRequest = await waitFor(
107
+ () => requests.find((request) => request.url.includes("/units")),
108
+ "the units request"
109
+ );
110
+
111
+ expect(unitsRequest.url).to.include(`move_in_date_earliest=${TODAY}`);
112
+ calculator.remove();
113
+ });
package/src/globals.ts CHANGED
@@ -1,5 +1 @@
1
- import startOfDay from "date-fns/startOfDay";
2
-
3
1
  export const BASE_DOMAIN = "https://app.meetelise.com";
4
-
5
- export const TODAY = startOfDay(new Date());
@@ -19,7 +19,10 @@ export const buildBuildingUnitsQuery = ({
19
19
  )
20
20
  .addSingle("move_in_date_earliest", moveInDateString)
21
21
  .addSingle("move_in_date_latest", moveInDateString)
22
- .addSingle("lease_term_min", leaseTerm?.toString());
22
+ .addSingle(
23
+ "lease_term_min",
24
+ Number.isFinite(leaseTerm) ? leaseTerm?.toString() : undefined
25
+ );
23
26
 
24
27
  return queryParamBuilder.build();
25
28
  };
@@ -77,7 +77,7 @@ export const calculateQuote = async (
77
77
  unitId,
78
78
  chargeInputs,
79
79
  rentableItems,
80
- leaseTerm,
80
+ ...(Number.isFinite(leaseTerm) ? { leaseTerm } : {}),
81
81
  moveInDate,
82
82
  }),
83
83
  { signal }
@@ -16,6 +16,26 @@ it("requests units for the selected move-in date", () => {
16
16
  expect(queryString).to.include("lease_term_min=12");
17
17
  });
18
18
 
19
+ it("leaves out a lease term that is not a number", () => {
20
+ const queryString = buildBuildingUnitsQuery({
21
+ moveInDateString: "2026-08-01",
22
+ leaseTerm: NaN,
23
+ });
24
+
25
+ expect(queryString).to.include("move_in_date_earliest=2026-08-01");
26
+ expect(queryString).not.to.include("lease_term_min");
27
+ expect(queryString).not.to.include("NaN");
28
+ });
29
+
30
+ it("leaves out the lease term when none is given", () => {
31
+ const queryString = buildBuildingUnitsQuery({
32
+ moveInDateString: "2026-08-01",
33
+ });
34
+
35
+ expect(queryString).to.include("move_in_date_earliest=2026-08-01");
36
+ expect(queryString).not.to.include("lease_term_min");
37
+ });
38
+
19
39
  it("sorts the cheapest units first without mutating the API response", () => {
20
40
  const units = [
21
41
  { id: 3, startingPrice: 2699 },