Skip to content

Commit 763ccbb

Browse files
Fix/441 cron scheduler improve (#442)
* feat: add CPS Scheduler cypress test * feat: add CronValidationService for validating AWS EventBridge Scheduler cron expressions - Implemented CronValidationService with methods to validate 6-field cron expressions. - Added support for wildcards, ranges, steps, lists, and special characters (L, W, #). - Created comprehensive unit tests for various cron expression patterns and edge cases. * refactor: update CronValidationService to generalize validation for extended cron expressions * refactor: inject dependencies in CpsSchedulerComponent and remove constructor * feat: enhance CronValidationService to restrict multiple hash expressions in day-of-week field
1 parent e9089dd commit 763ccbb

File tree

7 files changed

+2075
-124
lines changed

7 files changed

+2075
-124
lines changed

cypress/e2e/cps-scheduler.cy.ts

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
describe('CPS Scheduler Component', () => {
2+
beforeEach(() => {
3+
cy.visit('/scheduler/examples');
4+
cy.get('cps-scheduler').should('be.visible');
5+
});
6+
7+
/**
8+
* Dropdown selection helper
9+
*/
10+
const selectDropdownOption = (selector: string, optionText: string): void => {
11+
// Click the dropdown to open it
12+
cy.get(selector).click();
13+
14+
// Try to find and click the option with the specified text
15+
// Use a more generic approach that waits for any clickable element containing the text
16+
cy.get('body').contains(optionText).should('be.visible').click();
17+
};
18+
19+
describe('Core Functionality', () => {
20+
it('should display scheduler with proper initialization', () => {
21+
cy.get('[data-cy="schedule-type-toggle"]').should('be.visible');
22+
cy.get('[data-cy="schedule-type-toggle"]').should(
23+
'contain.text',
24+
'Not set'
25+
);
26+
});
27+
});
28+
29+
describe('Minutes Schedule - Cron Generation', () => {
30+
beforeEach(() => {
31+
cy.get('[data-cy="schedule-type-toggle"]').contains('Minutes').click();
32+
cy.get('[data-cy="minutes-config"]').should('be.visible');
33+
});
34+
35+
it('should generate cron expression for minute intervals', () => {
36+
selectDropdownOption('[data-cy="minutes-input"]', '5');
37+
38+
// Switch to Advanced to see generated cron
39+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
40+
cy.get('[data-cy="advanced-cron-input"]')
41+
.find('input')
42+
.should('have.value', '0/5 * 1/1 * ? *');
43+
});
44+
45+
it('should generate cron expression for 15-minute intervals', () => {
46+
selectDropdownOption('[data-cy="minutes-input"]', '15');
47+
48+
// Switch to Advanced to see generated cron
49+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
50+
cy.get('[data-cy="advanced-cron-input"]')
51+
.find('input')
52+
.should('have.value', '0/15 * 1/1 * ? *');
53+
});
54+
});
55+
56+
describe('Weekly Schedule - Cron Generation', () => {
57+
beforeEach(() => {
58+
cy.get('[data-cy="schedule-type-toggle"]').contains('Weekly').click();
59+
cy.get('[data-cy="weekly-config"]').should('be.visible');
60+
});
61+
62+
it('should generate correct cron for Monday and Wednesday', () => {
63+
cy.get('[data-cy="weekly-MON"]').click();
64+
cy.get('[data-cy="weekly-WED"]').click();
65+
66+
// Verify timezone selector appears
67+
cy.get('[data-cy="timezone-selector"]').should('be.visible');
68+
69+
// Verify specific cron expression is generated
70+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
71+
cy.get('[data-cy="advanced-cron-input"]')
72+
.find('input')
73+
.should('have.value', '0 0 ? * MON,WED *');
74+
});
75+
76+
it('should generate correct cron for Friday only', () => {
77+
// First ensure Monday is unchecked
78+
cy.get('[data-cy="weekly-MON"]').within(() => {
79+
cy.get('input[type="checkbox"]').uncheck({ force: true });
80+
});
81+
82+
// Check Friday
83+
cy.get('[data-cy="weekly-FRI"]').within(() => {
84+
cy.get('input[type="checkbox"]').check({ force: true });
85+
});
86+
87+
// Verify timezone selector appears
88+
cy.get('[data-cy="timezone-selector"]').should('be.visible');
89+
90+
// Verify specific cron expression is generated
91+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
92+
cy.get('[data-cy="advanced-cron-input"]')
93+
.find('input')
94+
.should('have.value', '0 0 ? * FRI *');
95+
});
96+
});
97+
98+
describe('Monthly Schedule - Cron Generation', () => {
99+
beforeEach(() => {
100+
cy.get('[data-cy="schedule-type-toggle"]').contains('Monthly').click();
101+
cy.get('[data-cy="monthly-config"]').should('be.visible');
102+
});
103+
104+
it('should generate correct cron for specific weekday (Second Tuesday of every month)', () => {
105+
// Select Second week
106+
selectDropdownOption('[data-cy="monthly-week-select"]', 'Second');
107+
108+
// Select Tuesday
109+
selectDropdownOption('[data-cy="monthly-weekday-select"]', 'Tuesday');
110+
111+
// Select starting month as April
112+
selectDropdownOption(
113+
'[data-cy="monthly-weekday-start-month-select"]',
114+
'April'
115+
);
116+
117+
// Verify timezone selector appears
118+
cy.get('[data-cy="timezone-selector"]').should('be.visible');
119+
120+
// Switch to Advanced to see generated cron
121+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
122+
cy.get('[data-cy="advanced-cron-input"]')
123+
.find('input')
124+
.should('contain.value', '30 9 ? 4/4 TUE#2 *');
125+
});
126+
127+
it('should generate correct cron for specific weekday (Fourth Sunday starting in October)', () => {
128+
// Select Fourth week
129+
selectDropdownOption('[data-cy="monthly-week-select"]', 'Fourth');
130+
131+
// Select Sunday
132+
selectDropdownOption('[data-cy="monthly-weekday-select"]', 'Sunday');
133+
134+
// Select starting month as October
135+
selectDropdownOption(
136+
'[data-cy="monthly-weekday-start-month-select"]',
137+
'October'
138+
);
139+
140+
// Set custom time to 14:45 (2:45 PM)
141+
cy.get('[data-cy="monthly-weekday-timepicker"]').within(() => {
142+
cy.get('input').eq(1).clear();
143+
cy.get('input').eq(1).type('45');
144+
cy.get('input').first().clear();
145+
cy.get('input').first().type('14');
146+
});
147+
148+
// Switch to Advanced to see generated cron
149+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
150+
cy.get('[data-cy="advanced-cron-input"]')
151+
.find('input')
152+
.should('contain.value', '45 14 ? 10/4 SUN#4 *');
153+
});
154+
});
155+
156+
describe('Advanced Schedule - Direct Input', () => {
157+
beforeEach(() => {
158+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
159+
cy.get('[data-cy="advanced-config"]').should('be.visible');
160+
});
161+
162+
it('should accept valid cron expressions', () => {
163+
const testCron = '0 30 14 ? * MON-FRI';
164+
165+
cy.get('[data-cy="advanced-cron-input"]').find('input, textarea').clear();
166+
167+
cy.get('[data-cy="advanced-cron-input"]')
168+
.find('input, textarea')
169+
.type(testCron);
170+
171+
// Verify input contains the cron
172+
cy.get('[data-cy="advanced-cron-input"]')
173+
.find('input, textarea')
174+
.should('have.value', testCron);
175+
});
176+
177+
it('should handle invalid cron expressions', () => {
178+
cy.get('[data-cy="advanced-cron-input"]').find('input, textarea').clear();
179+
180+
cy.get('[data-cy="advanced-cron-input"]')
181+
.find('input, textarea')
182+
.type('invalid cron');
183+
184+
// Component should not crash
185+
cy.get('[data-cy="advanced-cron-input"]').should('be.visible');
186+
187+
// Check validation state
188+
cy.get('.ng-invalid, .error').should('exist');
189+
});
190+
});
191+
192+
describe('State Management', () => {
193+
it('should maintain state when switching between schedule types', () => {
194+
// Set up Weekly schedule
195+
cy.get('[data-cy="schedule-type-toggle"]').contains('Weekly').click();
196+
cy.get('[data-cy="weekly-MON"]').click();
197+
198+
// Switch to Advanced to verify cron was generated
199+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
200+
cy.get('[data-cy="advanced-cron-input"]')
201+
.find('input, textarea')
202+
.should('not.have.value', '');
203+
204+
// Switch back to Weekly
205+
cy.get('[data-cy="schedule-type-toggle"]').contains('Weekly').click();
206+
cy.get('[data-cy="weekly-config"]').should('be.visible');
207+
});
208+
209+
it('should reset when selecting Not set', () => {
210+
// First set a schedule
211+
cy.get('[data-cy="schedule-type-toggle"]').contains('Weekly').click();
212+
cy.get('[data-cy="weekly-MON"]').click();
213+
214+
// Reset to Not set
215+
cy.get('[data-cy="schedule-type-toggle"]').contains('Not set').click();
216+
217+
// Component should be in reset state
218+
cy.get('cps-scheduler').should('be.visible');
219+
cy.get('[data-cy="schedule-type-toggle"]').should(
220+
'contain.text',
221+
'Not set'
222+
);
223+
});
224+
});
225+
226+
describe('Timezone Functionality', () => {
227+
it('should allow timezone filtering and show autocomplete options', () => {
228+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
229+
// Type in the autocomplete to filter timezone options
230+
cy.get('[data-cy="timezone-select"]').find('input').clear();
231+
232+
cy.get('[data-cy="timezone-select"]')
233+
.find('input')
234+
.type('UTC', { force: true });
235+
236+
// Verify that autocomplete dropdown appears with options
237+
cy.get('.cps-autocomplete-options, .cps-autocomplete-option').should(
238+
'exist'
239+
);
240+
241+
// Verify the input field contains what we typed
242+
cy.get('[data-cy="timezone-select"]')
243+
.find('input')
244+
.should('have.value', 'UTC');
245+
});
246+
247+
it('should maintain typed text in timezone input', () => {
248+
cy.get('[data-cy="schedule-type-toggle"]').contains('Advanced').click();
249+
250+
// Type a specific timezone
251+
cy.get('[data-cy="timezone-select"]').find('input').clear();
252+
253+
cy.get('[data-cy="timezone-select"]')
254+
.find('input')
255+
.type('Europe/London', { force: true });
256+
257+
// Verify the text remains in the input
258+
cy.get('[data-cy="timezone-select"]')
259+
.find('input')
260+
.should('have.value', 'Europe/London');
261+
262+
// Verify timezone selector is still functional
263+
cy.get('[data-cy="timezone-select"]').should('be.visible');
264+
});
265+
});
266+
267+
describe('Error Handling', () => {
268+
it('should handle rapid type switching', () => {
269+
const types = ['Minutes', 'Hourly', 'Weekly', 'Advanced'] as const;
270+
271+
types.forEach((type) => {
272+
cy.get('[data-cy="schedule-type-toggle"]').contains(type).click();
273+
});
274+
275+
// Should end in valid state
276+
cy.get('[data-cy="advanced-config"]').should('be.visible');
277+
});
278+
});
279+
});

projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
12
import {
23
ComponentFixture,
34
TestBed,
5+
discardPeriodicTasks,
46
fakeAsync,
5-
tick,
6-
discardPeriodicTasks
7+
tick
78
} from '@angular/core/testing';
8-
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
9-
import { CpsAutocompleteComponent } from './cps-autocomplete.component';
109
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
11-
import { LabelByValuePipe } from '../../pipes/internal/label-by-value.pipe';
12-
import { CheckOptionSelectedPipe } from '../../pipes/internal/check-option-selected.pipe';
1310
import { By } from '@angular/platform-browser';
14-
import { NO_ERRORS_SCHEMA } from '@angular/core';
11+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
12+
import { CheckOptionSelectedPipe } from '../../pipes/internal/check-option-selected.pipe';
13+
import { LabelByValuePipe } from '../../pipes/internal/label-by-value.pipe';
1514
import { CpsMenuHideReason } from '../cps-menu/cps-menu.component';
15+
import { CpsAutocompleteComponent } from './cps-autocomplete.component';
1616

1717
describe('CpsAutocompleteComponent', () => {
1818
let component: CpsAutocompleteComponent;

0 commit comments

Comments
 (0)