001 /** 002 * Copyright 2004-2013 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.hr.time.test; 017 018 import java.math.BigDecimal; 019 import java.sql.Date; 020 import java.sql.Timestamp; 021 import java.util.ArrayList; 022 import java.util.Arrays; 023 import java.util.Calendar; 024 import java.util.GregorianCalendar; 025 import java.util.HashMap; 026 import java.util.Iterator; 027 import java.util.LinkedList; 028 import java.util.List; 029 import java.util.Map; 030 import java.util.Set; 031 032 import org.apache.commons.lang3.StringUtils; 033 import org.apache.log4j.Logger; 034 import org.joda.time.DateTime; 035 import org.joda.time.DateTimeZone; 036 import org.joda.time.Days; 037 import org.joda.time.Duration; 038 import org.junit.Assert; 039 import org.kuali.hr.job.Job; 040 import org.kuali.hr.lm.accrual.AccrualCategory; 041 import org.kuali.hr.lm.accrual.service.AccrualService; 042 import org.kuali.hr.lm.leaveblock.LeaveBlock; 043 import org.kuali.hr.lm.leaveblock.service.LeaveBlockService; 044 import org.kuali.hr.lm.leavecalendar.LeaveCalendarDocument; 045 import org.kuali.hr.time.assignment.Assignment; 046 import org.kuali.hr.time.flsa.FlsaDay; 047 import org.kuali.hr.time.flsa.FlsaWeek; 048 import org.kuali.hr.time.service.base.TkServiceLocator; 049 import org.kuali.hr.time.timeblock.TimeBlock; 050 import org.kuali.hr.time.timeblock.TimeHourDetail; 051 import org.kuali.hr.time.timeblock.service.TimeBlockService; 052 import org.kuali.hr.time.timesheet.TimesheetDocument; 053 import org.kuali.hr.time.util.*; 054 import org.kuali.rice.kew.api.exception.WorkflowException; 055 import org.w3c.dom.NamedNodeMap; 056 import org.w3c.dom.Node; 057 058 import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput; 059 import com.gargoylesoftware.htmlunit.html.HtmlElement; 060 import com.gargoylesoftware.htmlunit.html.HtmlForm; 061 import com.gargoylesoftware.htmlunit.html.HtmlInput; 062 import com.gargoylesoftware.htmlunit.html.HtmlPage; 063 import com.gargoylesoftware.htmlunit.html.HtmlSelect; 064 import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput; 065 import com.gargoylesoftware.htmlunit.html.HtmlTextArea; 066 067 public class TkTestUtils { 068 069 private static final Logger LOG = Logger.getLogger(TkTestUtils.class); 070 071 public static TimesheetDocument populateBlankTimesheetDocument(Date calDate) { 072 try { 073 TimesheetDocument timesheet = TkServiceLocator.getTimesheetService().openTimesheetDocument(TKUser.getCurrentTargetPersonId(), 074 TkServiceLocator.getCalendarService().getCurrentCalendarDates(TKUser.getCurrentTargetPersonId(), 075 calDate)); 076 for(TimeBlock timeBlock : timesheet.getTimeBlocks()){ 077 TkServiceLocator.getTimeBlockService().deleteTimeBlock(timeBlock); 078 } 079 080 return timesheet; 081 } catch (WorkflowException e) { 082 throw new RuntimeException("Problem fetching document"); 083 } 084 } 085 086 public static TimesheetDocument populateTimesheetDocument(Date calDate) { 087 try { 088 TimesheetDocument timesheet = TkServiceLocator.getTimesheetService().openTimesheetDocument(TKUser.getCurrentTargetPersonId(), 089 TkServiceLocator.getCalendarService().getCurrentCalendarDates(TKUser.getCurrentTargetPersonId(), 090 calDate)); 091 for(TimeBlock timeBlock : timesheet.getTimeBlocks()){ 092 TkServiceLocator.getTimeBlockService().deleteTimeBlock(timeBlock); 093 } 094 095 //refetch clean document 096 timesheet = TkServiceLocator.getTimesheetService().openTimesheetDocument(TKUser.getCurrentTargetPersonId(), 097 TkServiceLocator.getCalendarService().getCurrentCalendarDates(TKUser.getCurrentTargetPersonId(), 098 calDate)); 099 List<TimeBlock> timeBlocks = new LinkedList<TimeBlock>(); 100 for(int i = 0;i<5;i++){ 101 TimeBlock timeBlock = createTimeBlock(timesheet, i+1, 10); 102 timeBlocks.add(timeBlock); 103 timesheet.setTimeBlocks(timeBlocks); 104 } 105 106 //Add a TEX accrual earn code 107 TimeBlock timeBlock = createTimeBlock(timesheet, 1, 4,"TEX"); 108 timeBlocks.add(timeBlock); 109 timesheet.setTimeBlocks(timeBlocks); 110 return timesheet; 111 112 } catch (WorkflowException e) { 113 throw new RuntimeException("Problem fetching document"); 114 } 115 } 116 117 //TODO populate this 118 public static void createEarnGroup(String earnGroup, List<String> earnCodes, Date asOfDate){ 119 120 } 121 122 /** 123 * Helper method to create regular time blocks for use in testing. 124 * 125 * @param start 126 * @param days 127 * @param hours 128 * @param earnCode 129 * @param jobNumber 130 * @param workArea 131 * @return 132 */ 133 public static List<TimeBlock> createUniformTimeBlocks(DateTime start, int days, BigDecimal hours, String earnCode, Long jobNumber, Long workArea) { 134 return TkTestUtils.createUniformTimeBlocks(start, days, hours, earnCode, jobNumber, workArea, null); 135 } 136 137 public static List<TimeBlock> createUniformTimeBlocks(DateTime start, int days, BigDecimal hours, String earnCode, Long jobNumber, Long workArea, Long task) { 138 List<TimeBlock> blocks = new ArrayList<TimeBlock>(); 139 140 for (int i=0; i<days; i++) { 141 DateTime ci = start.plusDays(i); 142 DateTime co = ci.plusHours(hours.intValue()); 143 TimeBlock block = TkTestUtils.createDummyTimeBlock(ci, co, hours, earnCode, jobNumber, workArea); 144 block.setTask(task); 145 blocks.add(block); 146 } 147 148 return blocks; 149 } 150 151 public static TimeBlock createDummyTimeBlock(DateTime clockIn, DateTime clockOut, BigDecimal hours, String earnCode) { 152 return TkTestUtils.createDummyTimeBlock(clockIn, clockOut, hours, earnCode, -1L, -1L); 153 } 154 155 public static TimeBlock createDummyTimeBlock(DateTime clockIn, DateTime clockOut, BigDecimal hours, String earnCode, Long jobNumber, Long workArea) { 156 TimeBlock block = new TimeBlock(); 157 Timestamp ci = new Timestamp(clockIn.getMillis()); 158 Timestamp co = new Timestamp(clockOut.getMillis()); 159 block.setBeginTimestamp(ci); 160 block.setEndTimestamp(co); 161 block.setHours(hours); 162 block.setBeginTimeDisplay(new DateTime(ci.getTime())); 163 block.setEndTimeDisplay(new DateTime(co.getTime())); 164 165 block.setEarnCode(earnCode); 166 block.setJobNumber(jobNumber); 167 block.setWorkArea(workArea); 168 169 TimeHourDetail thd = new TimeHourDetail(); 170 thd.setHours(hours); 171 thd.setEarnCode(earnCode); 172 List<TimeHourDetail> timeHourDetails = new ArrayList<TimeHourDetail>(); 173 timeHourDetails.add(thd); 174 block.setTimeHourDetails(timeHourDetails); 175 176 return block; 177 } 178 179 public static TimeBlock createTimeBlock(TimesheetDocument timesheetDocument, int dayInPeriod, int numHours){ 180 return createTimeBlock(timesheetDocument, dayInPeriod, numHours,"RGN"); 181 } 182 public static TimeBlock createTimeBlock(TimesheetDocument timesheetDocument, int dayInPeriod, int numHours, String earnCode){ 183 TimeBlock timeBlock = new TimeBlock(); 184 Calendar cal = GregorianCalendar.getInstance(); 185 cal.setTimeInMillis(timesheetDocument.getCalendarEntry().getBeginPeriodDateTime().getTime()); 186 for(int i = 1; i< dayInPeriod;i++){ 187 cal.add(Calendar.DAY_OF_MONTH, 1); 188 } 189 cal.set(Calendar.HOUR, 8); 190 cal.set(Calendar.MINUTE, 0); 191 192 timeBlock.setDocumentId(timesheetDocument.getDocumentId()); 193 timeBlock.setBeginTimeDisplay(new DateTime(cal.getTimeInMillis())); 194 195 timeBlock.setBeginTimestamp(new Timestamp(cal.getTimeInMillis())); 196 timeBlock.setBeginTimestampTimezone("EST"); 197 timeBlock.setEarnCode(earnCode); 198 timeBlock.setJobNumber(1L); 199 timeBlock.setWorkArea(1234L); 200 timeBlock.setTask(1L); 201 timeBlock.setHours((new BigDecimal(numHours)).setScale(TkConstants.BIG_DECIMAL_SCALE, TkConstants.BIG_DECIMAL_SCALE_ROUNDING)); 202 cal.add(Calendar.HOUR, numHours); 203 timeBlock.setEndTimestamp(new Timestamp(cal.getTimeInMillis())); 204 timeBlock.setEndTimeDisplay(new DateTime(cal.getTimeInMillis())); 205 206 return timeBlock; 207 } 208 209 public static List<Job> getJobs(Date calDate){ 210 return TkServiceLocator.getJobService().getJobs(TKContext.getPrincipalId(), calDate); 211 } 212 /** 213 * 214 * @param page: current html page 215 * @param criteria: The key is the field name and the value is a string array which contains the field value and the field type which can be chosen from TkTestConstants 216 * @return HtmlPage resultPage 217 * @throws Exception 218 */ 219 public static HtmlPage fillOutForm(HtmlPage page, Map<String, Object> criteria) throws Exception { 220 HtmlForm lookupForm = HtmlUnitUtil.getDefaultForm(page); 221 String formFieldPrefix = ""; 222 HtmlPage resultPage = null; 223 HtmlSelect select = null; 224 HtmlInput input = null; 225 HtmlCheckBoxInput checkBox = null; 226 HtmlTextArea textArea = null; 227 228 229 // Common class of both HtmlInput and HtmlTextArea -- since either of these can appear 230 // on a web-form. 231 HtmlElement htmlBasicInput = null; 232 233 Set<Map.Entry<String, Object>> entries = criteria.entrySet(); 234 Iterator<Map.Entry<String, Object>> it = entries.iterator(); 235 236 while (it.hasNext()) { 237 Map.Entry<String,Object> entry = it.next(); 238 239 // if the field type is not <input> 240 if(entry.getValue() instanceof String[]) { 241 String key = Arrays.asList((String[])entry.getValue()).get(0).toString(); 242 String value = Arrays.asList((String[])entry.getValue()).get(1).toString(); 243 244 // drop-down 245 if(key.equals(TkTestConstants.FormElementTypes.DROPDOWN)) { 246 247 try { 248 select = (HtmlSelect) lookupForm.getSelectByName(formFieldPrefix + entry.getKey()); 249 } catch (Exception e) { 250 select = (HtmlSelect) lookupForm.getElementById(formFieldPrefix + entry.getKey()); 251 } 252 // try to get a useful option, other than the typical blank default, by getting the last option 253 // if the size of the options list is zero, then there is a problem. might as well error here with an array out of bounds. 254 resultPage = (HtmlPage) select.getOption(select.getOptionSize()-1).setSelected(true); 255 HtmlUnitUtil.createTempFile(resultPage); 256 } 257 // check box 258 else if(key.equals(TkTestConstants.FormElementTypes.CHECKBOX)) { 259 try { 260 checkBox = page.getHtmlElementById(formFieldPrefix + entry.getKey()); 261 } 262 catch(Exception e) { 263 checkBox = page.getElementByName(formFieldPrefix + entry.getKey()); 264 } 265 resultPage = (HtmlPage) checkBox.setChecked(Boolean.parseBoolean(value)); 266 } 267 // text area 268 else if(key.equals(TkTestConstants.FormElementTypes.TEXTAREA)) { 269 try { 270 textArea = page.getHtmlElementById(formFieldPrefix + entry.getKey()); 271 } catch (Exception e){ 272 textArea = page.getElementByName(formFieldPrefix + entry.getKey()); 273 } 274 textArea.setText(Arrays.asList((String[])entry.getValue()).get(1).toString()); 275 } 276 } else { 277 try { 278 htmlBasicInput = page.getHtmlElementById(formFieldPrefix + entry.getKey()); 279 if (htmlBasicInput instanceof HtmlTextArea) { 280 textArea = (HtmlTextArea)htmlBasicInput; 281 textArea.setText(entry.getValue().toString()); 282 resultPage = (HtmlPage) textArea.getPage(); 283 } else if (htmlBasicInput instanceof HtmlInput) { 284 input = (HtmlInput)htmlBasicInput; 285 resultPage = (HtmlPage) input.setValueAttribute(entry.getValue().toString()); 286 } else { 287 LOG.error("Request to populate a non-input html form field."); 288 } 289 } catch (Exception e) { 290 htmlBasicInput = page.getElementByName(formFieldPrefix + entry.getKey()); 291 292 if (htmlBasicInput instanceof HtmlTextArea) { 293 textArea = (HtmlTextArea)htmlBasicInput; 294 textArea.setText(entry.getValue().toString()); 295 resultPage = (HtmlPage) textArea.getPage(); 296 } else if (htmlBasicInput instanceof HtmlInput) { 297 input = (HtmlInput)htmlBasicInput; 298 resultPage = (HtmlPage) input.setValueAttribute(entry.getValue().toString()); 299 } else { 300 LOG.error("Request to populate a non-input html form field."); 301 } 302 } 303 } 304 } 305 HtmlUnitUtil.createTempFile(resultPage); 306 return resultPage; 307 } 308 309 /** 310 * 311 * @param page: current html page //NOTE doesnt seem to work currently for js setting of form variables 312 * @param name: the button name 313 * @return 314 * @throws Exception 315 */ 316 public static HtmlPage clickButton(HtmlPage page, String name) throws Exception { 317 HtmlForm form = HtmlUnitUtil.getDefaultForm(page); 318 HtmlSubmitInput input = form.getInputByName(name); 319 return (HtmlPage) input.click(); 320 } 321 322 public static HtmlPage clickClockInOrOutButton(HtmlPage page) throws Exception { 323 HtmlForm form = HtmlUnitUtil.getDefaultForm(page); 324 325 HtmlSubmitInput input = form.getInputByName("clockAction"); 326 form.getInputByName("methodToCall").setValueAttribute("clockAction"); 327 return (HtmlPage) input.click(); 328 } 329 330 public static HtmlPage clickLunchInOrOutButton(HtmlPage page, String lunchAction) throws Exception { 331 HtmlForm form = HtmlUnitUtil.getDefaultForm(page); 332 333 HtmlSubmitInput input = form.getInputByName("clockAction"); 334 form.getInputByName("methodToCall").setValueAttribute("clockAction"); 335 form.getInputByName("currentClockAction").setValueAttribute(lunchAction); 336 return (HtmlPage) input.click(); 337 } 338 339 @SuppressWarnings("serial") 340 public static void verifyAggregateHourSumsFlatList(String msg, final Map<String,BigDecimal> ecToHoursMap, TkTimeBlockAggregate aggregate) { 341 // Initializes sum map to zeros, since we only care about the entires 342 // that were passed in. 343 Map<String,BigDecimal> ecToSumMap = new HashMap<String,BigDecimal>() {{ for (String ec : ecToHoursMap.keySet()) { put(ec, BigDecimal.ZERO); }}}; 344 345 for (TimeBlock bl : aggregate.getFlattenedTimeBlockList()) { 346 for (TimeHourDetail thd : bl.getTimeHourDetails()) { 347 if (ecToSumMap.containsKey(thd.getEarnCode())) { 348 ecToSumMap.put(thd.getEarnCode(), ecToSumMap.get(thd.getEarnCode()).add(thd.getHours())); 349 } 350 } 351 } 352 353 // Assert that our values are correct. 354 for (String key : ecToHoursMap.keySet()) { 355 Assert.assertEquals( 356 msg + " >> ("+key+") Wrong number of hours expected: " + ecToHoursMap.get(key) + " found: " + ecToSumMap.get(key) + " :: ", 357 0, 358 ecToHoursMap.get(key).compareTo(ecToSumMap.get(key))); 359 } 360 } 361 362 /** 363 * Helper method to verify that the aggregate contains the correct sums as 364 * indicated in the ecToHoursMapping, on a SINGLE given flsaWeek. 365 * 366 * Warning! Contains Assertions, use only with Test Cases. 367 * 368 * @param ecToHoursMap ex: { 'REG' => 40, 'OVT' => 10 } 369 * @param aggregate An aggregate object containing the time blocks 370 * @param flsaWeek 0 indexed start week (pulling from aggregate) 371 */ 372 @SuppressWarnings("serial") 373 public static void verifyAggregateHourSums(String msg, final Map<String,BigDecimal> ecToHoursMap, TkTimeBlockAggregate aggregate, int flsaWeek) { 374 // Initializes sum map to zeros, since we only care about the entires 375 // that were passed in. 376 Map<String,BigDecimal> ecToSumMap = new HashMap<String,BigDecimal>() {{ for (String ec : ecToHoursMap.keySet()) { put(ec, BigDecimal.ZERO); }}}; 377 378 List<FlsaWeek> flsaWeeks = aggregate.getFlsaWeeks(DateTimeZone.forID(TKUtils.getSystemTimeZone())); 379 Assert.assertTrue(msg + " >> Not enough FLSA weeks to verify aggregate hours, max: " + (flsaWeeks.size() - 1), flsaWeeks.size() > flsaWeek); 380 381 // Build our Sum Map. 382 FlsaWeek week = flsaWeeks.get(flsaWeek); 383 List<FlsaDay> flsaDays = week.getFlsaDays(); 384 for (FlsaDay day : flsaDays) { 385 for (TimeBlock bl : day.getAppliedTimeBlocks()) { 386 for (TimeHourDetail thd : bl.getTimeHourDetails()) { 387 if (ecToSumMap.containsKey(thd.getEarnCode())) { 388 ecToSumMap.put(thd.getEarnCode(), ecToSumMap.get(thd.getEarnCode()).add(thd.getHours())); 389 } 390 } 391 } 392 } 393 394 395 // Assert that our values are correct. 396 for (String key : ecToHoursMap.keySet()) { 397 Assert.assertEquals( 398 msg + " >> ("+key+") Wrong number of hours expected: " + ecToHoursMap.get(key) + " found: " + ecToSumMap.get(key) + " :: ", 399 0, 400 ecToHoursMap.get(key).compareTo(ecToSumMap.get(key))); 401 } 402 } 403 404 405 public static void verifyAggregateHourSums(final Map<String,BigDecimal> ecToHoursMap, TkTimeBlockAggregate aggregate, int flsaWeek) { 406 TkTestUtils.verifyAggregateHourSums("", ecToHoursMap, aggregate, flsaWeek); 407 } 408 409 410 /** 411 * Helper method to generate time blocks suitable for db persistence in 412 * unit tests. 413 */ 414 public static List<TimeBlock> createUniformActualTimeBlocks(TimesheetDocument timesheetDocument, Assignment assignment, String earnCode, DateTime start, int days, BigDecimal hours, BigDecimal amount) { 415 TimeBlockService service = TkServiceLocator.getTimeBlockService(); 416 List<TimeBlock> blocks = new ArrayList<TimeBlock>(); 417 418 for (int i=0; i<days; i++) { 419 DateTime ci = start.plusDays(i); 420 DateTime co = ci.plusHours(hours.intValue()); 421 Timestamp tsin = new Timestamp(ci.getMillis()); 422 Timestamp tsout = new Timestamp(co.getMillis()); 423 424 blocks.addAll(service.buildTimeBlocks(assignment, earnCode, timesheetDocument, tsin, tsout, hours, amount, false, false, TKContext.getPrincipalId())); 425 } 426 427 return blocks; 428 } 429 430 public static Map<Timestamp, BigDecimal> getDateToHoursMap(TimeBlock timeBlock, TimeHourDetail timeHourDetail) { 431 Map<Timestamp, BigDecimal> dateToHoursMap = new HashMap<Timestamp, BigDecimal>(); 432 DateTime beginTime = new DateTime(timeBlock.getBeginTimestamp()); 433 DateTime endTime = new DateTime(timeBlock.getEndTimestamp()); 434 435 Days d = Days.daysBetween(beginTime, endTime); 436 int numberOfDays = d.getDays(); 437 if (numberOfDays < 1) { 438 dateToHoursMap.put(timeBlock.getBeginTimestamp(), timeHourDetail.getHours()); 439 return dateToHoursMap; 440 } 441 DateTime currentTime = beginTime; 442 for (int i = 0; i < numberOfDays; i++) { 443 DateTime nextDayAtMidnight = new DateTime(currentTime.plusDays(1).getMillis()); 444 nextDayAtMidnight = nextDayAtMidnight.hourOfDay().setCopy(12); 445 nextDayAtMidnight = nextDayAtMidnight.minuteOfDay().setCopy(0); 446 nextDayAtMidnight = nextDayAtMidnight.secondOfDay().setCopy(0); 447 nextDayAtMidnight = nextDayAtMidnight.millisOfSecond().setCopy(0); 448 Duration dur = new Duration(currentTime, nextDayAtMidnight); 449 long duration = dur.getStandardSeconds(); 450 BigDecimal hrs = new BigDecimal(duration / 3600, TkConstants.MATH_CONTEXT); 451 dateToHoursMap.put(new Timestamp(currentTime.getMillis()), hrs); 452 currentTime = nextDayAtMidnight; 453 } 454 Duration dur = new Duration(currentTime, endTime); 455 long duration = dur.getStandardSeconds(); 456 BigDecimal hrs = new BigDecimal(duration / 3600, TkConstants.MATH_CONTEXT); 457 dateToHoursMap.put(new Timestamp(currentTime.getMillis()), hrs); 458 459 return dateToHoursMap; 460 } 461 462 public static Date createDate(int month, int day, int year, int hours, int minutes, int seconds){ 463 DateTime dt = new DateTime(year, month, day, hours, minutes, seconds, 0); 464 return new Date(dt.getMillis()); 465 } 466 467 468 /** 469 * Method to obtain the HREF onclick='' value from the button when 470 * the client side typically processes the request. 471 * @param button 472 */ 473 public static String getOnClickHref(HtmlElement button) { 474 NamedNodeMap attributes = button.getAttributes(); 475 Node node = attributes.getNamedItem("onclick"); 476 477 //location.href='TimesheetSubmit.do?action=R&documentId=2000&methodToCall=approveTimesheet' 478 String hrefRaw = node.getNodeValue(); 479 int sidx = hrefRaw.indexOf("='"); 480 481 return hrefRaw.substring(sidx+2, hrefRaw.length() - 1); 482 } 483 484 }