fix(i18n): preserve local calendar day in job date pickers

This commit is contained in:
cesnimda
2026-11-11 07:42:27 +01:00
parent df93bfb64e
commit 265794e010
13 changed files with 436 additions and 4 deletions
+33
View File
@@ -0,0 +1,33 @@
# 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 diff whitespace checks passed. Raw receipts are 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.
@@ -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
@@ -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,
@@ -78,13 +79,13 @@ function getTodayIso() {
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,79 @@
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));
});
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();
});