Skip to content

Commit

Permalink
[UI] Also cloning recurring run schedule, fixes #3761 (#3840)
Browse files Browse the repository at this point in the history
* [UI] Also cloning recurring run schedule

* Fix unit test for trigger and utils

* Add and fix unit tests for Trigger

* Add NewRun page unit tests

* Fix unit tests

* Fix jest test timezone
  • Loading branch information
Bobgy authored May 25, 2020
1 parent e52481a commit 508f31a
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 57 deletions.
6 changes: 6 additions & 0 deletions frontend/global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default () => {
// This let unit tests run in UTC timezone consistently, despite developers'
// dev machine's timezone.
// Reference: https://stackoverflow.com/a/56482581
process.env.TZ = 'UTC';
};
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"!src/index.tsx",
"!src/CSSReset.tsx"
],
"globalSetup": "./global-setup.js",
"snapshotSerializers": [
"./src/__serializers__/mock-function",
"snapshot-diff/serializer.js",
Expand Down
95 changes: 86 additions & 9 deletions frontend/src/components/Trigger.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,31 @@ const PERIODIC_DEFAULT = {
};
const CRON_DEFAULT = { cron: '0 * * * * ?', end_time: undefined, start_time: undefined };

beforeAll(() => {
process.env.TZ = 'UTC';
});

describe('Trigger', () => {
// tslint:disable-next-line:variable-name
const RealDate = Date;

function mockDate(isoDate: any): void {
(global as any).Date = class extends RealDate {
constructor() {
constructor(...args: any[]) {
super();
return new RealDate(isoDate);
if (args.length === 0) {
// Use mocked date when calling new Date()
return new RealDate(isoDate);
} else {
// Otherwise, use real Date constructor
return new (RealDate as any)(...args);
}
}
};
}
const testDate = new Date(2018, 11, 21, 7, 53);
mockDate(testDate);
const now = new Date(2018, 11, 21, 7, 53);
mockDate(now);
const oneWeekLater = new Date(2018, 11, 28, 7, 53);

it('renders periodic schedule controls for initial render', () => {
const tree = shallow(<Trigger />);
Expand Down Expand Up @@ -113,7 +124,7 @@ describe('Trigger', () => {
expect(spy).toHaveBeenLastCalledWith({
...PARAMS_DEFAULT,
trigger: {
periodic_schedule: { ...PERIODIC_DEFAULT, start_time: testDate },
periodic_schedule: { ...PERIODIC_DEFAULT, start_time: now },
},
});
});
Expand All @@ -128,7 +139,7 @@ describe('Trigger', () => {
target: { type: 'checkbox', checked: true },
});
(tree.instance() as Trigger).handleChange('startDate')({ target: { value: '2018-11-23' } });
(tree.instance() as Trigger).handleChange('endTime')({ target: { value: '08:35' } });
(tree.instance() as Trigger).handleChange('startTime')({ target: { value: '08:35' } });
expect(spy).toHaveBeenLastCalledWith({
...PARAMS_DEFAULT,
trigger: {
Expand Down Expand Up @@ -193,7 +204,7 @@ describe('Trigger', () => {
expect(spy).toHaveBeenLastCalledWith({
...PARAMS_DEFAULT,
trigger: {
periodic_schedule: { ...PERIODIC_DEFAULT, end_time: testDate, start_time: testDate },
periodic_schedule: { ...PERIODIC_DEFAULT, end_time: oneWeekLater, start_time: now },
},
});
});
Expand Down Expand Up @@ -292,6 +303,38 @@ describe('Trigger', () => {
},
});
});

it('inits with cloned initial props', () => {
const spy = jest.fn();
const startTime = new Date('2020-01-01T23:53:00.000Z');
shallow(
<Trigger
onChange={spy}
initialProps={{
maxConcurrentRuns: '3',
catchup: false,
trigger: {
periodic_schedule: {
interval_second: '' + 60 * 60 * 3, // 3 hours
start_time: startTime.toISOString() as any,
},
},
}}
/>,
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith({
catchup: false,
maxConcurrentRuns: '3',
trigger: {
periodic_schedule: {
end_time: undefined,
interval_second: '10800',
start_time: startTime,
},
},
});
});
});

describe('cron', () => {
Expand All @@ -318,7 +361,7 @@ describe('Trigger', () => {
expect(spy).toHaveBeenLastCalledWith({
...PARAMS_DEFAULT,
trigger: {
cron_schedule: { ...CRON_DEFAULT, start_time: testDate },
cron_schedule: { ...CRON_DEFAULT, start_time: new Date('2018-03-23T07:53:00.000Z') },
},
});
});
Expand All @@ -336,7 +379,7 @@ describe('Trigger', () => {
expect(spy).toHaveBeenLastCalledWith({
...PARAMS_DEFAULT,
trigger: {
cron_schedule: { ...CRON_DEFAULT, end_time: testDate, cron: '0 0 0 * * ?' },
cron_schedule: { ...CRON_DEFAULT, end_time: oneWeekLater, cron: '0 0 0 * * ?' },
},
});
});
Expand Down Expand Up @@ -384,5 +427,39 @@ describe('Trigger', () => {
},
});
});

it('inits with cloned initial props', () => {
const spy = jest.fn();
const startTime = new Date('2020-01-01T00:00:00.000Z');
const endTime = new Date('2020-01-02T01:02:00.000Z');
shallow(
<Trigger
onChange={spy}
initialProps={{
maxConcurrentRuns: '4',
catchup: true,
trigger: {
cron_schedule: {
cron: '0 0 0 ? * 1,5,6',
start_time: startTime.toISOString() as any,
end_time: endTime.toISOString() as any,
},
},
}}
/>,
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith({
catchup: true,
maxConcurrentRuns: '4',
trigger: {
cron_schedule: {
cron: '0 0 0 ? * 1,5,6',
start_time: startTime,
end_time: endTime,
},
},
});
});
});
});
67 changes: 46 additions & 21 deletions frontend/src/components/Trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,19 @@ import {
pickersToDate,
triggers,
TriggerType,
parseTrigger,
ParsedTrigger,
} from '../lib/TriggerUtils';
import { logger } from 'src/lib/Utils';

type TriggerInitialProps = {
maxConcurrentRuns?: string;
catchup?: boolean;
trigger?: ApiTrigger;
};

interface TriggerProps {
initialProps?: TriggerInitialProps;
onChange?: (config: {
trigger?: ApiTrigger;
maxConcurrentRuns?: string;
Expand Down Expand Up @@ -67,33 +77,48 @@ const css = stylesheet({
});

export default class Trigger extends React.Component<TriggerProps, TriggerState> {
public state = (() => {
const now = new Date();
const inAWeek = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() + 7,
now.getHours(),
now.getMinutes(),
);
const [startDate, startTime] = dateToPickerFormat(now);
const [endDate, endTime] = dateToPickerFormat(inAWeek);
public state: TriggerState = (() => {
const { maxConcurrentRuns, catchup, trigger } =
this.props.initialProps || ({} as TriggerInitialProps);
let parsedTrigger: Partial<ParsedTrigger> = {};
try {
if (trigger) {
parsedTrigger = parseTrigger(trigger);
}
} catch (err) {
logger.warn('Failed to parse original trigger: ', trigger);
logger.warn(err);
}
const startDateTime = parsedTrigger.startDateTime ?? new Date();
const endDateTime =
parsedTrigger.endDateTime ??
new Date(
startDateTime.getFullYear(),
startDateTime.getMonth(),
startDateTime.getDate() + 7,
startDateTime.getHours(),
startDateTime.getMinutes(),
);
const [startDate, startTime] = dateToPickerFormat(startDateTime);
const [endDate, endTime] = dateToPickerFormat(endDateTime);

return {
catchup: true,
cron: '',
editCron: false,
catchup: catchup ?? true,
maxConcurrentRuns: maxConcurrentRuns || '10',
hasEndDate: !!parsedTrigger?.endDateTime,
endDate,
endTime,
hasEndDate: false,
hasStartDate: false,
intervalCategory: PeriodicInterval.MINUTE,
intervalValue: 1,
maxConcurrentRuns: '10',
selectedDays: new Array(7).fill(true),
hasStartDate: !!parsedTrigger?.startDateTime,
startDate,
startTime,
type: TriggerType.INTERVALED,
selectedDays: new Array(7).fill(true),
type: parsedTrigger.type ?? TriggerType.INTERVALED,
// cron state
editCron: parsedTrigger.type === TriggerType.CRON,
cron: parsedTrigger.cron || '',
// interval state
intervalCategory: parsedTrigger.intervalCategory ?? PeriodicInterval.MINUTE,
intervalValue: parsedTrigger.intervalValue ?? 1,
};
})();

Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/__snapshots__/Trigger.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ exports[`Trigger enables a single day on click 1`] = `
}
}
type="date"
value="2018-12-21"
value="2018-12-28"
variant="outlined"
width={160}
/>
Expand Down Expand Up @@ -459,7 +459,7 @@ exports[`Trigger renders all week days enabled 1`] = `
}
}
type="date"
value="2018-12-21"
value="2018-12-28"
variant="outlined"
width={160}
/>
Expand Down Expand Up @@ -800,7 +800,7 @@ exports[`Trigger renders periodic schedule controls for initial render 1`] = `
}
}
type="date"
value="2018-12-21"
value="2018-12-28"
variant="outlined"
width={160}
/>
Expand Down Expand Up @@ -1039,7 +1039,7 @@ exports[`Trigger renders periodic schedule controls if the trigger type is CRON
}
}
type="date"
value="2018-12-21"
value="2018-12-28"
variant="outlined"
width={160}
/>
Expand Down Expand Up @@ -1301,7 +1301,7 @@ exports[`Trigger renders week days if the trigger type is CRON and interval is w
}
}
type="date"
value="2018-12-21"
value="2018-12-28"
variant="outlined"
width={160}
/>
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/lib/TriggerUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
TriggerType,
dateToPickerFormat,
triggerDisplayString,
parseTrigger,
} from './TriggerUtils';
import { ApiTrigger } from '../apis/job';

Expand Down Expand Up @@ -236,6 +237,45 @@ describe('TriggerUtils', () => {
});
});

describe('parseTrigger', () => {
it('throws on invalid trigger', () => {
expect(() => parseTrigger({})).toThrow('Invalid trigger: {}');
});

it('parses periodic schedule', () => {
const startTime = new Date(1234);
const parsedTrigger = parseTrigger({
periodic_schedule: {
start_time: startTime,
interval_second: '120',
},
});
expect(parsedTrigger).toEqual({
type: TriggerType.INTERVALED,
startDateTime: startTime,
endDateTime: undefined,
intervalCategory: PeriodicInterval.MINUTE,
intervalValue: 2,
});
});

it('parses cron schedule', () => {
const endTime = new Date(12345);
const parsedTrigger = parseTrigger({
cron_schedule: {
end_time: endTime,
cron: '0 0 0 ? * 0,6',
},
});
expect(parsedTrigger).toEqual({
type: TriggerType.CRON,
cron: '0 0 0 ? * 0,6',
startDateTime: undefined,
endDateTime: endTime,
});
});
});

describe('dateToPickerFormat', () => {
it('converts date to picker format date and time', () => {
const testDate = new Date(2018, 11, 13, 11, 33);
Expand Down
Loading

0 comments on commit 508f31a

Please sign in to comment.