Skip to content
Open
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
319 changes: 167 additions & 152 deletions superset-frontend/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"d3-color": "^3.1.0",
"d3-scale": "^2.1.2",
"dayjs": "^1.11.18",
"dayjs-jalali": "^0.0.2",
"dom-to-image-more": "^3.6.0",
"dom-to-pdf": "^0.3.2",
"echarts": "^5.6.0",
Expand All @@ -159,6 +160,7 @@
"googleapis": "^154.1.0",
"immer": "^10.1.1",
"interweave": "^13.1.1",
"jalaali-js": "^1.2.8",
"jquery": "^3.7.1",
"js-levenshtein": "^1.1.6",
"js-yaml-loader": "^1.2.2",
Expand Down Expand Up @@ -189,6 +191,7 @@
"react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.16.1",
"react-loadable": "^5.5.0",
"react-multi-date-picker": "^4.5.2",
"react-redux": "^7.2.9",
"react-resize-detector": "^7.1.2",
"react-reverse-portal": "^2.3.0",
Expand Down Expand Up @@ -262,6 +265,7 @@
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
"@types/jalaali-js": "^1.2.0",
"@types/jest": "^29.5.14",
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* 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 { render, screen, userEvent } from 'spec/helpers/testing-library';
import type { Dayjs } from '@superset-ui/core/utils/dates';
import { JalaliDatePicker } from 'src/explore/components/controls/DateFilterControl/components/JalaliDatePicker';

jest.mock('react-resize-detector', () => ({
useResizeDetector: () => ({ ref: jest.fn(), width: 0 }),
}));

jest.mock('react-multi-date-picker', () => ({
__esModule: true,
default: ({ onChange }: { onChange: (value: unknown) => void }) => (
<button
type="button"
onClick={() =>
onChange([{ year: Number.NaN, month: undefined, day: undefined }])
}
>
Trigger invalid date
</button>
),
}));

test('returns empty range when conversion fails', async () => {
const handleChange =
jest.fn<(range: [Dayjs | null, Dayjs | null]) => void>();

render(
<JalaliDatePicker
mode="range"
value={[null, null] as [Dayjs | null, Dayjs | null]}
onChange={handleChange}
/>,
);

await userEvent.click(screen.getByRole('button', { name: /invalid date/i }));

expect(handleChange).toHaveBeenCalledWith([null, null]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { NO_TIME_RANGE } from '@superset-ui/core';
import {
screen,
render,
fireEvent,
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import {
extendedDayjs as dayjs,
type Dayjs,
} from '@superset-ui/core/utils/dates';
import type { JalaliDatePickerProps } from 'src/explore/components/controls/DateFilterControl/components/JalaliDatePicker';
import { PersianCalendarFrame } from 'src/explore/components/controls/DateFilterControl/components/PersianCalendarFrame';

jest.mock(
'src/explore/components/controls/DateFilterControl/components/JalaliDatePicker',
() => {
const { extendedDayjs } = jest.requireActual(
'@superset-ui/core/utils/dates',
);
const parseDate = (input: string) =>
input ? extendedDayjs(input, 'YYYY-MM-DD') : null;
return {
JalaliDatePicker: ({
value,
onChange,
placeholder,
mode,
}: JalaliDatePickerProps) => {
if (mode === 'range') {
const [start, end] = (value as [Dayjs | null, Dayjs | null]) ?? [
null,
null,
];
return (
<div>
<input
aria-label={`${placeholder}-start`}
value={start ? start.format('YYYY-MM-DD') : ''}
onChange={event =>
onChange([parseDate(event.target.value), end ?? null])
}
/>
<input
aria-label={`${placeholder}-end`}
value={end ? end.format('YYYY-MM-DD') : ''}
onChange={event =>
onChange([start ?? null, parseDate(event.target.value)])
}
/>
</div>
);
}
const singleValue = value as Dayjs | null;
return (
<input
aria-label={placeholder}
value={singleValue ? singleValue.format('YYYY-MM-DD') : ''}
onChange={event => onChange(parseDate(event.target.value))}
/>
);
},
};
},
);

test('renders preset selection and applies relative range on change', async () => {
const onChange = jest.fn();
render(<PersianCalendarFrame value="Last 7 days" onChange={onChange} />);

await userEvent.click(screen.getByRole('radio', { name: /Last 30 days/i }));

expect(onChange).toHaveBeenCalledWith('Last 30 days');
});

test('emits custom range when both Jalali pickers are filled', async () => {
const onChange = jest.fn();
render(<PersianCalendarFrame value={NO_TIME_RANGE} onChange={onChange} />);

await userEvent.click(screen.getByRole('radio', { name: /Custom range/i }));

const startInput = screen.getByLabelText('Select date range-start');
const endInput = screen.getByLabelText('Select date range-end');

fireEvent.change(startInput, { target: { value: '2024-01-10' } });
fireEvent.change(endInput, { target: { value: '2024-01-12' } });

expect(onChange).toHaveBeenLastCalledWith('2024-01-10 : 2024-01-12');
});

test('parses persisted custom range values', () => {
render(
<PersianCalendarFrame
value="2024-01-01 : 2024-01-05"
onChange={jest.fn()}
/>,
);

expect(
screen.getByText(/Selected Jalali range/i),
).toBeInTheDocument();
});

test('normalizes Jalali range strings to Gregorian inputs', () => {
render(
<PersianCalendarFrame
value="1403-01-02 : 1403-01-04"
onChange={jest.fn()}
/>,
);

expect(
screen.getByLabelText('Select date range-start'),
).toHaveValue('2024-03-21');
expect(screen.getByLabelText('Select date range-end')).toHaveValue(
'2024-03-23',
);
});

test('selects persisted relative range radio button', () => {
render(<PersianCalendarFrame value="Last year" onChange={jest.fn()} />);

expect(
screen.getByRole('radio', { name: /Last year/i }),
).toBeChecked();
});

test('custom range defaults both inputs to today when enabled', async () => {
render(<PersianCalendarFrame value={NO_TIME_RANGE} onChange={jest.fn()} />);

await userEvent.click(screen.getByRole('radio', { name: /Custom range/i }));

const isoDate = dayjs().format('YYYY-MM-DD');
await waitFor(() =>
expect(
screen.getByLabelText('Select date range-start'),
).toHaveValue(isoDate),
);
expect(
screen.getByLabelText('Select date range-end'),
).toHaveValue(isoDate);
});
68 changes: 68 additions & 0 deletions superset-frontend/spec/utils/persianCalendar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* 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 {
gregorianToPersian,
isPersianLocale,
isRTLLayout,
} from 'src/utils/persianCalendar';

const defaultDir = document.documentElement.dir;
const defaultLang = document.documentElement.lang;
const defaultNavigatorLanguage = navigator.language;
const defaultNavigatorLanguages = navigator.languages;

const setNavigatorLanguage = (language?: string, languages?: string[]) => {
Object.defineProperty(window.navigator, 'language', {
value: language,
configurable: true,
});
Object.defineProperty(window.navigator, 'languages', {
value: languages,
configurable: true,
});
};

afterEach(() => {
document.documentElement.dir = defaultDir;
document.documentElement.lang = defaultLang;
setNavigatorLanguage(defaultNavigatorLanguage, defaultNavigatorLanguages);
});

test('gregorian dates map to correct Persian weekday names', () => {
expect(gregorianToPersian(2024, 3, 31).weekday).toBe('یکشنبه');
expect(gregorianToPersian(2024, 4, 1).weekday).toBe('دوشنبه');
});

test('detects RTL when document direction is rtl', () => {
document.documentElement.dir = 'rtl';
document.documentElement.lang = 'en-US';
setNavigatorLanguage('en-US', ['en-US']);

expect(isRTLLayout()).toBe(true);
});

test('detects Persian locale from navigator languages', () => {
document.documentElement.dir = '';
document.documentElement.lang = '';
setNavigatorLanguage('fa-IR', ['fa-IR']);

expect(isPersianLocale()).toBe(true);
expect(isRTLLayout()).toBe(true);
});
Loading
Loading