diff --git a/web/html/src/branding/css/susemanager/components/buttons.less b/web/html/src/branding/css/susemanager/components/buttons.less index 1d3012473143..b5ab981f1fdd 100644 --- a/web/html/src/branding/css/susemanager/components/buttons.less +++ b/web/html/src/branding/css/susemanager/components/buttons.less @@ -196,6 +196,7 @@ button.is-plain { -webkit-appearance: none; -moz-appearance: none; appearance: none; + background: none; border: none; border-radius: 0; box-sizing: border-box; diff --git a/web/html/src/branding/css/uyuni/buttons.less b/web/html/src/branding/css/uyuni/buttons.less index 797e8b67fba0..3a6919a041e1 100644 --- a/web/html/src/branding/css/uyuni/buttons.less +++ b/web/html/src/branding/css/uyuni/buttons.less @@ -2,6 +2,7 @@ button.is-plain { -webkit-appearance: none; -moz-appearance: none; appearance: none; + background: none; border: none; border-radius: 0; box-sizing: border-box; diff --git a/web/html/src/components/datetime/DateTimePicker.test.tsx b/web/html/src/components/datetime/DateTimePicker.test.tsx new file mode 100644 index 000000000000..8313781819ab --- /dev/null +++ b/web/html/src/components/datetime/DateTimePicker.test.tsx @@ -0,0 +1,92 @@ +import * as React from "react"; +import { useState } from "react"; + +import { localizedMoment } from "utils"; +import { render, screen, type } from "utils/test-utils"; + +import { DateTimePicker } from "./DateTimePicker"; + +describe("DateTimePicker", () => { + const getInputs = () => { + const datePicker = screen.getByTestId("date-picker"); + const timePicker = screen.getByTestId("time-picker"); + return { datePicker, timePicker }; + }; + + test("prop value is rendered in the user's timezone", () => { + const validISOString = "2020-02-01T04:00:00.000Z"; + const Setup = () => { + const [value, setValue] = useState(localizedMoment(validISOString)); + return ; + }; + render(); + + const { datePicker, timePicker } = getInputs(); + // These values need to be offset by the user's timezone + expect(datePicker.value).toEqual("2020-01-31"); + expect(timePicker.value).toEqual("20:00"); + }); + + test("picking a date and time uses the user's timezone", (done) => { + const validISOString = "2020-02-01T04:00:00.000Z"; + let changeEventCount = 0; + + const Setup = () => { + const [value, setValue] = useState(localizedMoment(validISOString)); + return ( + { + changeEventCount += 1; + setValue(newValue); + + if (changeEventCount === 2) { + // Note that this value should be in a different day due to the timezone offset + expect(newValue.toISOString()).toEqual("2020-01-17T07:30:00.000Z"); + done(); + } + }} + /> + ); + }; + render(); + + const { datePicker, timePicker } = getInputs(); + datePicker.click(); + screen.getByText("16").click(); + timePicker.click(); + screen.getByText("23:30").click(); + + expect(datePicker.value).toEqual("2020-01-16"); + expect(timePicker.value).toEqual("23:30"); + }); + + test("clearing the time input doesn't change the date (bsc#1210253)", (done) => { + const validISOString = "2020-01-30T15:00:00.000Z"; + let changeEventCount = 0; + + const Setup = () => { + const [value, setValue] = useState(localizedMoment(validISOString)); + return ( + { + changeEventCount += 1; + setValue(newValue); + + if (changeEventCount === 2) { + expect(newValue.toUserDateTimeString()).toEqual("2020-01-16 00:00"); + done(); + } + }} + /> + ); + }; + render(); + + const { datePicker, timePicker } = getInputs(); + datePicker.click(); + screen.getByText("16").click(); + type(timePicker, "0", false); + }); +}); diff --git a/web/html/src/components/datetime/DateTimePicker.tsx b/web/html/src/components/datetime/DateTimePicker.tsx index df0550f15762..ba721245e62b 100644 --- a/web/html/src/components/datetime/DateTimePicker.tsx +++ b/web/html/src/components/datetime/DateTimePicker.tsx @@ -64,9 +64,9 @@ export const DateTimePicker = (props: Props) => { timePickerRef.current?.setOpen(true); }; - const onChange = (newDateValue: Date | null) => { + const onChange = (date: Date | null) => { // Currently we don't support propagating null values, we might want to do this in the future - if (newDateValue === null) { + if (date === null) { return; } // The date we get here is now in the browsers local timezone but with values that should be reinterpreted @@ -76,7 +76,7 @@ export const DateTimePicker = (props: Props) => { localizedMoment( // We first clone the date again to not modify the original. This has the unintended side effect of converting to // UTC and adjusting the values. - localizedMoment(newDateValue) + localizedMoment(date) // To get back to the values we want we just convert back to the browsers local timezone as it was before. .local() // Then we set the timezone of the date to the users configured timezone without adjusting the values. @@ -144,6 +144,7 @@ export const DateTimePicker = (props: Props) => { className="form-control no-right-border" // This is used by Cucumber to interact with the component data-testid="date-picker" + maxLength={10} /> } previousMonthAriaLabel={previousMonth} @@ -172,7 +173,18 @@ export const DateTimePicker = (props: Props) => { portalId="time-picker-portal" ref={timePickerRef} selected={browserTimezoneValue.toDate()} - onChange={onChange} + onChange={(date) => { + if (date === null) { + return; + } + /** + * NB! Only take the hours and minutes from this change event since react-datepicker updates the date + * value when it should only update the time value (bsc#1202991, bsc#1215820) + */ + const mergedDate = browserTimezoneValue.toDate(); + mergedDate.setHours(date.getHours(), date.getMinutes()); + onChange(mergedDate); + }} showTimeSelect showTimeSelectOnly // We want the regular primary display to only show the time here, so using TIME_FORMAT is intentional @@ -188,22 +200,21 @@ export const DateTimePicker = (props: Props) => { className="form-control" // This is used by Cucumber to interact with the component data-testid="time-picker" + maxLength={5} /> } /> )} - {props.serverTimeZone ? localizedMoment.serverTimeZoneAbbr : localizedMoment.userTimeZoneAbbr} + {timeZone} {process.env.NODE_ENV !== "production" && SHOW_DEBUG_VALUES ? (
           user:{"   "}
-          {props.value.toUserString()}
-          
- server: {props.value.toServerString()} -
+ {props.value.toUserDateTimeString()} ({localizedMoment.userTimeZone})
+ server: {props.value.toServerDateTimeString()} ({localizedMoment.serverTimeZone})
iso:{" "} {props.value.toISOString()}
diff --git a/web/html/src/utils/test-utils/mock.tsx b/web/html/src/utils/test-utils/mock.tsx index d6cda3157ed0..e11af47f8baf 100644 --- a/web/html/src/utils/test-utils/mock.tsx +++ b/web/html/src/utils/test-utils/mock.tsx @@ -1,13 +1,3 @@ -// Mock the datetime picker to avoid it causing issues due to missing jQuery/Bootstrap parts -jest.mock("components/datetime/DateTimePicker", () => { - return { - __esModule: true, - DateTimePicker: () => { - return
DateTimePicker mockup
; - }, - }; -}); - // Mock the ACE Editor to avoid it causing issues due to the missing proper library import jest.mock("components/ace-editor", () => { return { diff --git a/web/html/src/utils/test-utils/screen.ts b/web/html/src/utils/test-utils/screen.ts index d250245c5735..872bf81cb31b 100644 --- a/web/html/src/utils/test-utils/screen.ts +++ b/web/html/src/utils/test-utils/screen.ts @@ -1,4 +1,4 @@ -import { getDefaultNormalizer, Screen, screen as rawScreen } from "@testing-library/react"; +import { getDefaultNormalizer, queryHelpers, Screen, screen as rawScreen } from "@testing-library/react"; // Utility type, if a function TargetFunction returns a Promise, return an intersection with Promise, otherwise with T type ReturnFromWith< @@ -34,14 +34,42 @@ const labelNormalizer = (input: string) => { // Override `screen.getByLabelText` const getByLabelText = rawScreen.getByLabelText; type GetByLabelTextArgs = Parameters; + +// Helpers for querying by testid so we use similar selectors to Cucumber, see https://testing-library.com/docs/dom-testing-library/api-custom-queries/ +const queryByTestId = queryHelpers.queryByAttribute.bind(null, "data-testid"); +const queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, "data-testid"); + +function getByTestId(...[container, id, ...rest]: Parameters) { + const result = getAllByTestId(container, id, ...rest); + if (result.length > 1) { + throw queryHelpers.getElementError(`Found multiple elements with the [data-testid="${id}"]`, container); + } + return result[0]; +} +function getAllByTestId(...[container, id, ...rest]: Parameters) { + const els = queryAllByTestId(container, id, ...rest); + if (!els.length) { + throw queryHelpers.getElementError(`Unable to find an element by: [data-testid="${id}"]`, container); + } + return els; +} + +const additionalQueries = { + queryByTestId, + queryAllByTestId, + getByTestId, + getAllByTestId, +}; + Object.assign(rawScreen, { getByLabelText: (...[text, options, waitForElementOptions]: GetByLabelTextArgs) => { options ??= {}; options.normalizer ??= labelNormalizer; return getByLabelText(text, options, waitForElementOptions); }, + additionalQueries, } as Partial); -const screen = rawScreen as GenericScreen; +const screen = rawScreen as GenericScreen & typeof additionalQueries; export { screen }; diff --git a/web/spacewalk-web.changes.eth.Manager-4.3-MU-4.3.8-bsc-1215820 b/web/spacewalk-web.changes.eth.Manager-4.3-MU-4.3.8-bsc-1215820 new file mode 100644 index 000000000000..8cb3b75b8843 --- /dev/null +++ b/web/spacewalk-web.changes.eth.Manager-4.3-MU-4.3.8-bsc-1215820 @@ -0,0 +1 @@ +- Fix datetimepicker erroneously updating the date field (bsc#1210253, bsc#1215820)