Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eb0a24a74 | |||
| f7c3617e63 | |||
| 926f423f70 |
@@ -0,0 +1,43 @@
|
||||
# Date-picker calendar-day preservation
|
||||
|
||||
11 November 2026. Baseline `ec57a24d77854bb416c9ea02b8b371be3e5554c0`, isolated branch `codex/i18n-deadline-picker-date`.
|
||||
|
||||
The Add and Edit job dialogs passed date-only strings through `new Date(value)`, which interprets a literal `YYYY-MM-DD` at UTC midnight. In Los Angeles, opening 23 November displayed the 22nd. Their picker change handlers then used `toISOString().slice(0, 10)`. Selecting local midnight on 23 November in Oslo or Kiritimati submitted the 22nd.
|
||||
|
||||
The existing wrapper pairs now use installed date-fns `parseISO` for literal dates and `format(value, "yyyy-MM-dd")` for the picker's local Date output. Non-literal parsing retains `new Date(value)`. Empty and invalid picker values retain their existing clearing behaviour. Edit's separate legacy `toDateInputValue` loading policy is preserved.
|
||||
|
||||
The affected callers are Add's applied date and deadline, and Edit's applied date, status-change date, reply date, follow-up date and deadline. All seven controls are date pickers backed by date-only state and request values. Revision and submit handlers are retained. There is no backend, timestamp serialization, dependency or migration change.
|
||||
|
||||
## Verification
|
||||
|
||||
The new component regression drives the actual Add/Edit state and conversion handlers with a picker test double implementing MUI's local-Date callback contract. Each run starts a new Jest process with the recorded timezone. It checks EN/NB values, a full create/save payload, source revision, five Edit/two Add controls, clearing, invalid picker values and an offset timestamp using the existing legacy loading policy.
|
||||
|
||||
| Process timezone | Before fix | After fix |
|
||||
| --- | --- | --- |
|
||||
| America/Los_Angeles | 5 failed | 5 passed |
|
||||
| Europe/Oslo | 4 failed, 1 passed | 5 passed |
|
||||
| Pacific/Kiritimati | 4 failed, 1 passed | 5 passed |
|
||||
|
||||
These are the same five cases executed in three environments. The nearby run passed 21 cases across date-picker, edit-revision, quick-capture and native file-input suites, including those same five cases. TypeScript without emit, ESLint on the three changed files and tracked source whitespace checks passed. The cached evidence check flagged the original TypeScript receipt's blank line at EOF. That raw receipt is retained unchanged in `evidence/date-picker-calendar-day/`.
|
||||
|
||||
Actual MUI browser interaction, screenshots, native devices and provider/API server verification were not run in this isolated increment. The root integration owner will qualify the rendered picker separately. Final I18N, A3, security and release acceptance remain open.
|
||||
|
||||
## Other reviewed date contracts
|
||||
|
||||
`CalendarAgenda` validates its date-only response, parses it at UTC noon and explicitly formats in UTC. The reviewed 23 November value stayed on the 23rd in all three zones. Its backend groups by the stored calendar date and explicitly retains the legacy timezone/precision limitation.
|
||||
|
||||
`CompaniesTable` formats next-contact and relationship dates using the viewer's local timezone. Its edit form takes the raw first ten characters. Literal `2026-11-23` therefore displays as the 22nd in Los Angeles, while `2026-11-23T23:30:00Z` displays as the 24th in Oslo and opens as the 23rd in the editor. The backend fields remain legacy `DateTime?` with no precision discriminator. The current locale test covers a noon UTC timestamp. These display/loading cases require a qualified date-versus-timestamp contract before a broader change.
|
||||
|
||||
The Jobs table similarly formats a literal deadline through local `Date`, and its current test expects that expression. Edit's retained `toDateInputValue` can also move an unspecified midnight timestamp back a day in positive zones. These are separate legacy precision/loading findings. This fix does not claim their closure or reconstruct missing timezone metadata.
|
||||
|
||||
Rollback reverts the two wrapper changes and their regression package. Existing persisted dates are not rewritten.
|
||||
|
||||
## Follow-up local-today default
|
||||
|
||||
From isolated checkpoint `f7c3617e`, Add's `getTodayIso` still selected the UTC date for initialization and a successful form reset. It now uses the already imported `format` with the browser's local Date. No persisted record is rewritten.
|
||||
|
||||
Two new component cases fix the UTC clock at `2026-11-23T00:30:00Z` and `2026-11-23T23:30:00Z`. Each timezone's red run had one failure and one pass, with the earlier five cases skipped. The final full suite passed seven cases in each of Los Angeles, Oslo and Kiritimati. That is seven distinct cases repeated in three environments, including the five earlier picker cases. TypeScript and two-file lint passed. The earlier 21-case nearby run belongs to the preceding checkpoint and was not rerun for this one-line follow-up.
|
||||
|
||||
The new cases cover initial creation, Create and add another resetting the default, and closing/reopening the resulting draft. Closing and reopening the same mounted dialog preserves its existing step and draft. It does not reset every open. The initial test draft assumed otherwise and was corrected to exercise the actual successful reset path. The only write is the explicit mocked creation request, whose selected date is asserted. API server, actual browser, export build and native checks remain unexecuted.
|
||||
|
||||
Follow-up receipts are in `evidence/date-picker-local-today/`. The legacy loading and Companies contract findings above remain open.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 5 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 6.688 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 5 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 6.561 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 5 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 7.132 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx edit-job-revision.test.tsx quick-capture.test.tsx file-picker-activation.test.tsx
|
||||
|
||||
|
||||
Test Suites: 4 passed, 4 total
|
||||
Tests: 21 passed, 21 total
|
||||
Snapshots: 0 total
|
||||
Time: 15.02 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx|edit-job-revision.test.tsx|quick-capture.test.tsx|file-picker-activation.test.tsx.
|
||||
@@ -0,0 +1,105 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
FAIL src/job-date-picker-calendar.test.tsx
|
||||
● en Add date-only picker round trip retains the selected day and existing creation payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
39 | render(wrap(<AddJobModal open initialUrl="https://example.test/job" onClose={jest.fn()} onCreated={jest.fn()} />));
|
||||
40 | await screen.findByDisplayValue("Teacher");
|
||||
> 41 | expect(screen.getByLabelText(copy.addJobModalDeadline)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
42 | fireEvent.click(screen.getByRole("button", { name: `${copy.addJobModalDeadline} clear` }));
|
||||
43 | expect(screen.getByLabelText(copy.addJobModalDeadline)).toBeEmptyDOMElement();
|
||||
44 | fireEvent.click(screen.getByRole("button", { name: `${copy.addJobModalDeadline} invalid` }));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:41:59)
|
||||
|
||||
● nb Add date-only picker round trip retains the selected day and existing creation payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
39 | render(wrap(<AddJobModal open initialUrl="https://example.test/job" onClose={jest.fn()} onCreated={jest.fn()} />));
|
||||
40 | await screen.findByDisplayValue("Teacher");
|
||||
> 41 | expect(screen.getByLabelText(copy.addJobModalDeadline)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
42 | fireEvent.click(screen.getByRole("button", { name: `${copy.addJobModalDeadline} clear` }));
|
||||
43 | expect(screen.getByLabelText(copy.addJobModalDeadline)).toBeEmptyDOMElement();
|
||||
44 | fireEvent.click(screen.getByRole("button", { name: `${copy.addJobModalDeadline} invalid` }));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:41:59)
|
||||
|
||||
● en Edit date-only picker round trip preserves all five date controls and revision payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
59 | render(wrap(<EditJobDialog open jobId={1} onClose={jest.fn()} onSaved={jest.fn()} />));
|
||||
60 | await screen.findByDisplayValue("Teacher");
|
||||
> 61 | expect(screen.getByLabelText(copy.editJobDeadline)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
62 | fireEvent.click(screen.getByRole("button", { name: `${copy.editJobDeadline} clear` }));
|
||||
63 | fireEvent.click(screen.getByRole("button", { name: `${copy.editJobDeadline} invalid` }));
|
||||
64 | expect(screen.getByLabelText(copy.editJobDeadline)).toBeEmptyDOMElement();
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:61:55)
|
||||
|
||||
● nb Edit date-only picker round trip preserves all five date controls and revision payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
59 | render(wrap(<EditJobDialog open jobId={1} onClose={jest.fn()} onSaved={jest.fn()} />));
|
||||
60 | await screen.findByDisplayValue("Teacher");
|
||||
> 61 | expect(screen.getByLabelText(copy.editJobDeadline)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
62 | fireEvent.click(screen.getByRole("button", { name: `${copy.editJobDeadline} clear` }));
|
||||
63 | fireEvent.click(screen.getByRole("button", { name: `${copy.editJobDeadline} invalid` }));
|
||||
64 | expect(screen.getByLabelText(copy.editJobDeadline)).toBeEmptyDOMElement();
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:61:55)
|
||||
|
||||
● Edit retains its existing UTC calendar-day loading policy for a legacy offset timestamp
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-24
|
||||
Received:
|
||||
2026-11-23
|
||||
|
||||
75 | render(wrap(<EditJobDialog open jobId={1} onClose={jest.fn()} onSaved={jest.fn()} />));
|
||||
76 | await screen.findByDisplayValue("Teacher");
|
||||
> 77 | expect(screen.getByLabelText(translations.en.editJobDeadline)).toHaveTextContent("2026-11-24");
|
||||
| ^
|
||||
78 | expect(client.put).not.toHaveBeenCalled();
|
||||
79 | });
|
||||
80 |
|
||||
|
||||
at Object.toHaveTextContent (src/job-date-picker-calendar.test.tsx:77:66)
|
||||
|
||||
Test Suites: 1 failed, 1 total
|
||||
Tests: 5 failed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 4.077 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
FAIL src/job-date-picker-calendar.test.tsx
|
||||
● en Add date-only picker round trip retains the selected day and existing creation payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
46 | for (const label of [copy.addJobModalDateApplied, copy.addJobModalDeadline]) {
|
||||
47 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 48 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
49 | }
|
||||
50 | fireEvent.click(screen.getByRole("button", { name: copy.continue }));
|
||||
51 | for (let step = 0; step < 3; step++) fireEvent.click(screen.getByRole("button", { name: copy.skipAndContinue }));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:48:42)
|
||||
|
||||
● nb Add date-only picker round trip retains the selected day and existing creation payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
46 | for (const label of [copy.addJobModalDateApplied, copy.addJobModalDeadline]) {
|
||||
47 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 48 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
49 | }
|
||||
50 | fireEvent.click(screen.getByRole("button", { name: copy.continue }));
|
||||
51 | for (let step = 0; step < 3; step++) fireEvent.click(screen.getByRole("button", { name: copy.skipAndContinue }));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:48:42)
|
||||
|
||||
● en Edit date-only picker round trip preserves all five date controls and revision payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
65 | for (const label of [copy.editJobAppliedOn, copy.editJobStatusChangedOn, copy.editJobReplyReceivedOn, copy.editJobFollowUpOn, copy.editJobDeadline]) {
|
||||
66 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 67 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
68 | }
|
||||
69 | fireEvent.click(screen.getByRole("button", { name: copy.save }));
|
||||
70 | await waitFor(() => expect(client.put).toHaveBeenCalledWith("/jobapplications/1", expect.objectContaining({ expectedVersion: 7, dateApplied: "2026-11-23", deadline: "2026-11-23", followUpAt: "2026-11-23", responseDate: "2026-11-23" })));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:67:42)
|
||||
|
||||
● nb Edit date-only picker round trip preserves all five date controls and revision payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
65 | for (const label of [copy.editJobAppliedOn, copy.editJobStatusChangedOn, copy.editJobReplyReceivedOn, copy.editJobFollowUpOn, copy.editJobDeadline]) {
|
||||
66 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 67 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
68 | }
|
||||
69 | fireEvent.click(screen.getByRole("button", { name: copy.save }));
|
||||
70 | await waitFor(() => expect(client.put).toHaveBeenCalledWith("/jobapplications/1", expect.objectContaining({ expectedVersion: 7, dateApplied: "2026-11-23", deadline: "2026-11-23", followUpAt: "2026-11-23", responseDate: "2026-11-23" })));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:67:42)
|
||||
|
||||
Test Suites: 1 failed, 1 total
|
||||
Tests: 4 failed, 1 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 4.882 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
FAIL src/job-date-picker-calendar.test.tsx
|
||||
● en Add date-only picker round trip retains the selected day and existing creation payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
46 | for (const label of [copy.addJobModalDateApplied, copy.addJobModalDeadline]) {
|
||||
47 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 48 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
49 | }
|
||||
50 | fireEvent.click(screen.getByRole("button", { name: copy.continue }));
|
||||
51 | for (let step = 0; step < 3; step++) fireEvent.click(screen.getByRole("button", { name: copy.skipAndContinue }));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:48:42)
|
||||
|
||||
● nb Add date-only picker round trip retains the selected day and existing creation payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
46 | for (const label of [copy.addJobModalDateApplied, copy.addJobModalDeadline]) {
|
||||
47 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 48 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
49 | }
|
||||
50 | fireEvent.click(screen.getByRole("button", { name: copy.continue }));
|
||||
51 | for (let step = 0; step < 3; step++) fireEvent.click(screen.getByRole("button", { name: copy.skipAndContinue }));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:48:42)
|
||||
|
||||
● en Edit date-only picker round trip preserves all five date controls and revision payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
65 | for (const label of [copy.editJobAppliedOn, copy.editJobStatusChangedOn, copy.editJobReplyReceivedOn, copy.editJobFollowUpOn, copy.editJobDeadline]) {
|
||||
66 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 67 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
68 | }
|
||||
69 | fireEvent.click(screen.getByRole("button", { name: copy.save }));
|
||||
70 | await waitFor(() => expect(client.put).toHaveBeenCalledWith("/jobapplications/1", expect.objectContaining({ expectedVersion: 7, dateApplied: "2026-11-23", deadline: "2026-11-23", followUpAt: "2026-11-23", responseDate: "2026-11-23" })));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:67:42)
|
||||
|
||||
● nb Edit date-only picker round trip preserves all five date controls and revision payload
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-23
|
||||
Received:
|
||||
2026-11-22
|
||||
|
||||
65 | for (const label of [copy.editJobAppliedOn, copy.editJobStatusChangedOn, copy.editJobReplyReceivedOn, copy.editJobFollowUpOn, copy.editJobDeadline]) {
|
||||
66 | fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
> 67 | expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
| ^
|
||||
68 | }
|
||||
69 | fireEvent.click(screen.getByRole("button", { name: copy.save }));
|
||||
70 | await waitFor(() => expect(client.put).toHaveBeenCalledWith("/jobapplications/1", expect.objectContaining({ expectedVersion: 7, dateApplied: "2026-11-23", deadline: "2026-11-23", followUpAt: "2026-11-23", responseDate: "2026-11-23" })));
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:67:42)
|
||||
|
||||
Test Suites: 1 failed, 1 total
|
||||
Tests: 4 failed, 1 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 4.669 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 typecheck
|
||||
> tsc --noEmit --incremental false
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 7 passed, 7 total
|
||||
Snapshots: 0 total
|
||||
Time: 8.432 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 7 passed, 7 total
|
||||
Snapshots: 0 total
|
||||
Time: 8.287 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx
|
||||
|
||||
Test Suites: 1 passed, 1 total
|
||||
Tests: 7 passed, 7 total
|
||||
Snapshots: 0 total
|
||||
Time: 8.955 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx.
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx -t Add defaults and successful reset
|
||||
|
||||
FAIL src/job-date-picker-calendar.test.tsx
|
||||
● Add defaults and successful reset use local today at 2026-11-23T00:30:00Z
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-22
|
||||
Received:
|
||||
2026-11-23
|
||||
|
||||
43 | const view = render(form(true));
|
||||
44 | fireEvent.click(screen.getByRole("button", { name: translations.en.addJobModalEnterManually }));
|
||||
> 45 | expect(screen.getByLabelText(translations.en.addJobModalDateApplied)).toHaveTextContent(expected);
|
||||
| ^
|
||||
46 | fireEvent.click(screen.getByRole("button", { name: `${translations.en.addJobModalDateApplied} choose` }));
|
||||
47 | fireEvent.change(screen.getByRole("combobox", { name: translations.en.company }), { target: { value: "Synthetic school" } });
|
||||
48 | fireEvent.change(screen.getByLabelText(translations.en.addJobModalJobTitle), { target: { value: "Teacher" } });
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:45:73)
|
||||
|
||||
Test Suites: 1 failed, 1 total
|
||||
Tests: 1 failed, 5 skipped, 1 passed, 7 total
|
||||
Snapshots: 0 total
|
||||
Time: 4.694 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx with tests matching "Add defaults and successful reset".
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx -t Add defaults and successful reset
|
||||
|
||||
FAIL src/job-date-picker-calendar.test.tsx
|
||||
● Add defaults and successful reset use local today at 2026-11-23T23:30:00Z
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-24
|
||||
Received:
|
||||
2026-11-23
|
||||
|
||||
43 | const view = render(form(true));
|
||||
44 | fireEvent.click(screen.getByRole("button", { name: translations.en.addJobModalEnterManually }));
|
||||
> 45 | expect(screen.getByLabelText(translations.en.addJobModalDateApplied)).toHaveTextContent(expected);
|
||||
| ^
|
||||
46 | fireEvent.click(screen.getByRole("button", { name: `${translations.en.addJobModalDateApplied} choose` }));
|
||||
47 | fireEvent.change(screen.getByRole("combobox", { name: translations.en.company }), { target: { value: "Synthetic school" } });
|
||||
48 | fireEvent.change(screen.getByLabelText(translations.en.addJobModalJobTitle), { target: { value: "Teacher" } });
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:45:73)
|
||||
|
||||
Test Suites: 1 failed, 1 total
|
||||
Tests: 1 failed, 5 skipped, 1 passed, 7 total
|
||||
Snapshots: 0 total
|
||||
Time: 4.977 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx with tests matching "Add defaults and successful reset".
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 test
|
||||
> jest --runInBand job-date-picker-calendar.test.tsx -t Add defaults and successful reset
|
||||
|
||||
FAIL src/job-date-picker-calendar.test.tsx
|
||||
● Add defaults and successful reset use local today at 2026-11-23T23:30:00Z
|
||||
|
||||
expect(element).toHaveTextContent()
|
||||
|
||||
Expected element to have text content:
|
||||
2026-11-24
|
||||
Received:
|
||||
2026-11-23
|
||||
|
||||
43 | const view = render(form(true));
|
||||
44 | fireEvent.click(screen.getByRole("button", { name: translations.en.addJobModalEnterManually }));
|
||||
> 45 | expect(screen.getByLabelText(translations.en.addJobModalDateApplied)).toHaveTextContent(expected);
|
||||
| ^
|
||||
46 | fireEvent.click(screen.getByRole("button", { name: `${translations.en.addJobModalDateApplied} choose` }));
|
||||
47 | fireEvent.change(screen.getByRole("combobox", { name: translations.en.company }), { target: { value: "Synthetic school" } });
|
||||
48 | fireEvent.change(screen.getByLabelText(translations.en.addJobModalJobTitle), { target: { value: "Teacher" } });
|
||||
|
||||
at toHaveTextContent (src/job-date-picker-calendar.test.tsx:45:73)
|
||||
|
||||
Test Suites: 1 failed, 1 total
|
||||
Tests: 1 failed, 5 skipped, 1 passed, 7 total
|
||||
Snapshots: 0 total
|
||||
Time: 4.542 s
|
||||
Ran all test suites matching job-date-picker-calendar.test.tsx with tests matching "Add defaults and successful reset".
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> job-tracker-ui@0.1.0 typecheck
|
||||
> tsc --noEmit --incremental false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { format, parseISO } from "date-fns";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
@@ -73,18 +74,18 @@ const FIELD_SX = { "& .MuiInputBase-root": { minHeight: 56 } };
|
||||
const PICKER_TEXT_FIELD_PROPS = { fullWidth: true, sx: FIELD_SX };
|
||||
|
||||
function getTodayIso() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
return format(new Date(), "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
function parsePickerDate(value?: string | null): Date | null {
|
||||
if (!value) return null;
|
||||
const parsed = new Date(value);
|
||||
const parsed = /^\d{4}-\d{2}-\d{2}$/.test(value) ? parseISO(value) : new Date(value);
|
||||
return Number.isNaN(+parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function toPickerIso(value: Date | null) {
|
||||
if (!value || Number.isNaN(+value)) return "";
|
||||
return value.toISOString().slice(0, 10);
|
||||
return format(value, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
function emptyAttachmentBuckets(): AttachmentBuckets {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { format, parseISO } from "date-fns";
|
||||
|
||||
import { api } from "../api";
|
||||
import { Company, JobApplication } from "../types";
|
||||
@@ -46,13 +47,13 @@ function toDateInputValue(isoLike?: string): string {
|
||||
|
||||
function parsePickerDate(value?: string | null): Date | null {
|
||||
if (!value) return null;
|
||||
const parsed = new Date(value);
|
||||
const parsed = /^\d{4}-\d{2}-\d{2}$/.test(value) ? parseISO(value) : new Date(value);
|
||||
return Number.isNaN(+parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function toPickerIso(value: Date | null): string {
|
||||
if (!value || Number.isNaN(+value)) return "";
|
||||
return value.toISOString().slice(0, 10);
|
||||
return format(value, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
function parseTags(raw: any): string[] {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { api } from "./api";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
import { translations } from "./i18n/translations";
|
||||
import { ToastProvider } from "./toast";
|
||||
import AddJobModal from "./components/AddJobModal";
|
||||
import EditJobDialog from "./components/EditJobDialog";
|
||||
|
||||
// Exercise the component's real Date conversion around the picker's local-Date contract.
|
||||
jest.mock("@mui/x-date-pickers/DatePicker", () => ({ DatePicker: ({ label, value, onChange, disabled }: any) => {
|
||||
const day = value ? `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}` : "";
|
||||
return <div><output aria-label={label}>{day}</output>
|
||||
<button disabled={disabled} onClick={() => onChange(new Date(2026, 10, 23))}>{label} choose</button>
|
||||
<button disabled={disabled} onClick={() => onChange(null)}>{label} clear</button>
|
||||
<button disabled={disabled} onClick={() => onChange(new Date(NaN))}>{label} invalid</button>
|
||||
</div>;
|
||||
} }));
|
||||
jest.mock("./api", () => ({ api: { get: jest.fn(), post: jest.fn(), put: jest.fn() }, getApiErrorMessage: (_: unknown, fallback: string) => fallback }));
|
||||
const mockCompanies = [{ id: 2, name: "Synthetic school" }];
|
||||
jest.mock("./hooks/useCompanies", () => ({
|
||||
useCompanies: () => ({ companies: mockCompanies }), invalidateCompaniesCache: jest.fn(),
|
||||
}));
|
||||
const client = api as jest.Mocked<typeof api>;
|
||||
const job = { id: 1, version: 7, jobTitle: "Teacher", company: { id: 2, name: "Synthetic school" }, status: "Applied", responseReceived: true, dateApplied: "2026-11-23", deadline: "2026-11-23" };
|
||||
const wrap = (content: React.ReactNode) => <ToastProvider><I18nProvider>{content}</I18nProvider></ToastProvider>;
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks(); localStorage.clear();
|
||||
client.get.mockResolvedValue({ data: job } as any);
|
||||
client.put.mockResolvedValue({ data: {} } as any);
|
||||
client.post.mockImplementation((url) => Promise.resolve({ data: String(url) === "/jobimport/preview"
|
||||
? { success: true, title: "Teacher", company: "Synthetic school", deadline: "2026-11-23" } : { id: 1 } } as any));
|
||||
});
|
||||
afterEach(() => { jest.useRealTimers(); });
|
||||
|
||||
test.each(["2026-11-23T00:30:00Z", "2026-11-23T23:30:00Z"])("Add defaults and successful reset use local today at %s", async clock => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date(clock));
|
||||
const localNow = new Date(clock);
|
||||
const expected = `${localNow.getFullYear()}-${String(localNow.getMonth() + 1).padStart(2, "0")}-${String(localNow.getDate()).padStart(2, "0")}`;
|
||||
const form = (open: boolean) => wrap(<AddJobModal open={open} onClose={jest.fn()} onCreated={jest.fn()} />);
|
||||
const view = render(form(true));
|
||||
fireEvent.click(screen.getByRole("button", { name: translations.en.addJobModalEnterManually }));
|
||||
expect(screen.getByLabelText(translations.en.addJobModalDateApplied)).toHaveTextContent(expected);
|
||||
fireEvent.click(screen.getByRole("button", { name: `${translations.en.addJobModalDateApplied} choose` }));
|
||||
fireEvent.change(screen.getByRole("combobox", { name: translations.en.company }), { target: { value: "Synthetic school" } });
|
||||
fireEvent.change(screen.getByLabelText(translations.en.addJobModalJobTitle), { target: { value: "Teacher" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: translations.en.continue }));
|
||||
for (let step = 0; step < 3; step++) fireEvent.click(screen.getByRole("button", { name: translations.en.skipAndContinue }));
|
||||
fireEvent.click(screen.getByRole("button", { name: translations.en.createAndAddAnother }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: translations.en.addJobModalEnterManually }));
|
||||
expect(screen.getByLabelText(translations.en.addJobModalDateApplied)).toHaveTextContent(expected);
|
||||
view.rerender(form(false));
|
||||
view.rerender(form(true));
|
||||
expect(screen.getByLabelText(translations.en.addJobModalDateApplied)).toHaveTextContent(expected);
|
||||
expect(client.post).toHaveBeenCalledTimes(1);
|
||||
expect(client.post).toHaveBeenCalledWith("/jobapplications", expect.objectContaining({ dateApplied: "2026-11-23" }));
|
||||
expect(client.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each(["en", "nb"] as const)("%s Add date-only picker round trip retains the selected day and existing creation payload", async language => {
|
||||
localStorage.setItem("uiLanguage", language);
|
||||
const copy = translations[language];
|
||||
render(wrap(<AddJobModal open initialUrl="https://example.test/job" onClose={jest.fn()} onCreated={jest.fn()} />));
|
||||
await screen.findByDisplayValue("Teacher");
|
||||
expect(screen.getByLabelText(copy.addJobModalDeadline)).toHaveTextContent("2026-11-23");
|
||||
fireEvent.click(screen.getByRole("button", { name: `${copy.addJobModalDeadline} clear` }));
|
||||
expect(screen.getByLabelText(copy.addJobModalDeadline)).toBeEmptyDOMElement();
|
||||
fireEvent.click(screen.getByRole("button", { name: `${copy.addJobModalDeadline} invalid` }));
|
||||
expect(screen.getByLabelText(copy.addJobModalDeadline)).toBeEmptyDOMElement();
|
||||
for (const label of [copy.addJobModalDateApplied, copy.addJobModalDeadline]) {
|
||||
fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
}
|
||||
fireEvent.click(screen.getByRole("button", { name: copy.continue }));
|
||||
for (let step = 0; step < 3; step++) fireEvent.click(screen.getByRole("button", { name: copy.skipAndContinue }));
|
||||
fireEvent.click(screen.getByRole("button", { name: copy.createJob }));
|
||||
await waitFor(() => expect(client.post).toHaveBeenCalledWith("/jobapplications", expect.objectContaining({ dateApplied: "2026-11-23", deadline: "2026-11-23" })));
|
||||
});
|
||||
|
||||
test.each(["en", "nb"] as const)("%s Edit date-only picker round trip preserves all five date controls and revision payload", async language => {
|
||||
localStorage.setItem("uiLanguage", language);
|
||||
const copy = translations[language];
|
||||
render(wrap(<EditJobDialog open jobId={1} onClose={jest.fn()} onSaved={jest.fn()} />));
|
||||
await screen.findByDisplayValue("Teacher");
|
||||
expect(screen.getByLabelText(copy.editJobDeadline)).toHaveTextContent("2026-11-23");
|
||||
fireEvent.click(screen.getByRole("button", { name: `${copy.editJobDeadline} clear` }));
|
||||
fireEvent.click(screen.getByRole("button", { name: `${copy.editJobDeadline} invalid` }));
|
||||
expect(screen.getByLabelText(copy.editJobDeadline)).toBeEmptyDOMElement();
|
||||
for (const label of [copy.editJobAppliedOn, copy.editJobStatusChangedOn, copy.editJobReplyReceivedOn, copy.editJobFollowUpOn, copy.editJobDeadline]) {
|
||||
fireEvent.click(screen.getByRole("button", { name: `${label} choose` }));
|
||||
expect(screen.getByLabelText(label)).toHaveTextContent("2026-11-23");
|
||||
}
|
||||
fireEvent.click(screen.getByRole("button", { name: copy.save }));
|
||||
await waitFor(() => expect(client.put).toHaveBeenCalledWith("/jobapplications/1", expect.objectContaining({ expectedVersion: 7, dateApplied: "2026-11-23", deadline: "2026-11-23", followUpAt: "2026-11-23", responseDate: "2026-11-23" })));
|
||||
});
|
||||
|
||||
test("Edit retains its existing UTC calendar-day loading policy for a legacy offset timestamp", async () => {
|
||||
client.get.mockResolvedValue({ data: { ...job, deadline: "2026-11-23T23:30:00-08:00" } } as any);
|
||||
render(wrap(<EditJobDialog open jobId={1} onClose={jest.fn()} onSaved={jest.fn()} />));
|
||||
await screen.findByDisplayValue("Teacher");
|
||||
expect(screen.getByLabelText(translations.en.editJobDeadline)).toHaveTextContent("2026-11-24");
|
||||
expect(client.put).not.toHaveBeenCalled();
|
||||
});
|
||||
Reference in New Issue
Block a user