Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions airflow-core/src/airflow/ui/src/components/DateTimeInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";

import { TimezoneContext } from "src/context/timezone";
import { Wrapper } from "src/utils/Wrapper";

import { DateTimeInput } from "./DateTimeInput";

describe("DateTimeInput paste", () => {
it("Pastes a valid datetime and fires onChange with UTC value", () => {
const onChange = vi.fn();

render(
<TimezoneContext.Provider value={{ selectedTimezone: "UTC", setSelectedTimezone: vi.fn() }}>
<DateTimeInput onChange={onChange} value="" />
</TimezoneContext.Provider>,
{ wrapper: Wrapper },
);

const input = screen.getByTestId("datetime-input");
const pasteEvent = new ClipboardEvent("paste", {
clipboardData: new DataTransfer(),
});
Object.defineProperty(pasteEvent, "clipboardData", {
value: {
getData: vi.fn().mockReturnValue("2026-05-13 10:00:00"),
},
});

fireEvent(input, pasteEvent);

expect(onChange).toHaveBeenCalledTimes(1);

Check failure on line 50 in airflow-core/src/airflow/ui/src/components/DateTimeInput.test.tsx

View workflow job for this annotation

GitHub Actions / Basic tests / React UI tests

src/components/DateTimeInput.test.tsx > DateTimeInput paste > Pastes a valid datetime and fires onChange with UTC value

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/components/DateTimeInput.test.tsx:50:22
const callArg = onChange.mock.calls[0][0];
expect(callArg.target.value).toBe("2026-05-13T10:00:00.000Z");
});

it("Interprets timezone-less pasted values in the selected timezone", () => {
const onChange = vi.fn();

render(
<TimezoneContext.Provider
value={{ selectedTimezone: "America/New_York", setSelectedTimezone: vi.fn() }}
>
<DateTimeInput onChange={onChange} value="" />
</TimezoneContext.Provider>,
{ wrapper: Wrapper },
);

const input = screen.getByTestId("datetime-input");
const pasteEvent = new ClipboardEvent("paste", {
clipboardData: new DataTransfer(),
});
Object.defineProperty(pasteEvent, "clipboardData", {
value: {
getData: vi.fn().mockReturnValue("2026-05-13 10:00:00"),
},
});

fireEvent(input, pasteEvent);

expect(onChange).toHaveBeenCalledTimes(1);

Check failure on line 79 in airflow-core/src/airflow/ui/src/components/DateTimeInput.test.tsx

View workflow job for this annotation

GitHub Actions / Basic tests / React UI tests

src/components/DateTimeInput.test.tsx > DateTimeInput paste > Interprets timezone-less pasted values in the selected timezone

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/components/DateTimeInput.test.tsx:79:22
const callArg = onChange.mock.calls[0][0];
// 10:00 in New York (EDT, UTC-4) → 14:00 UTC
expect(callArg.target.value).toBe("2026-05-13T14:00:00.000Z");
});

it("Does not fire onChange for an invalid pasted string", () => {
const onChange = vi.fn();

render(
<TimezoneContext.Provider value={{ selectedTimezone: "UTC", setSelectedTimezone: vi.fn() }}>
<DateTimeInput onChange={onChange} value="" />
</TimezoneContext.Provider>,
{ wrapper: Wrapper },
);

const input = screen.getByTestId("datetime-input");
const pasteEvent = new ClipboardEvent("paste", {
clipboardData: new DataTransfer(),
});
Object.defineProperty(pasteEvent, "clipboardData", {
value: {
getData: vi.fn().mockReturnValue("not a date"),
},
});

fireEvent(input, pasteEvent);

expect(onChange).not.toHaveBeenCalled();
});

it("Does not fire onChange for an empty pasted string", () => {
const onChange = vi.fn();

render(
<TimezoneContext.Provider value={{ selectedTimezone: "UTC", setSelectedTimezone: vi.fn() }}>
<DateTimeInput onChange={onChange} value="" />
</TimezoneContext.Provider>,
{ wrapper: Wrapper },
);

const input = screen.getByTestId("datetime-input");
const pasteEvent = new ClipboardEvent("paste", {
clipboardData: new DataTransfer(),
});
Object.defineProperty(pasteEvent, "clipboardData", {
value: {
getData: vi.fn().mockReturnValue(""),
},
});

fireEvent(input, pasteEvent);

expect(onChange).not.toHaveBeenCalled();
});
});
15 changes: 14 additions & 1 deletion airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { Input, type InputProps } from "@chakra-ui/react";
import dayjs from "dayjs";
import tz from "dayjs/plugin/timezone";
import { forwardRef, type ChangeEvent, useState } from "react";
import { forwardRef, type ChangeEvent, type ClipboardEvent, useState } from "react";
import { useDebouncedCallback } from "use-debounce";

import { useTimezone } from "src/context/timezone";
Expand Down Expand Up @@ -54,9 +54,22 @@ export const DateTimeInput = forwardRef<HTMLInputElement, Props>(({ onChange, va
debounceDelay,
);

const onPaste = (event: ClipboardEvent<HTMLInputElement>) => {
const pasted = event.clipboardData.getData("text");
if (!pasted) return;
const parsed = dayjs.tz(pasted, selectedTimezone);
if (!parsed.isValid()) return;
event.preventDefault();
const local = parsed.tz(selectedTimezone).format(DEFAULT_DATETIME_FORMAT);
const utc = parsed.toISOString();
setDisplayDate(local);
onChange?.({ ...event, target: { ...event.target, value: utc } } as unknown as ChangeEvent<HTMLInputElement>);
};

return (
<Input
data-testid="datetime-input"
onPaste={onPaste}
onChange={(event) => {
const local = dayjs(event.target.value).isValid() ? event.target.value : "";

Expand Down
Loading