date-range-input-harness-Bp1T4oUe.mjs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import { HarnessPredicate, ComponentHarness, parallel, TestKey } from '@angular/cdk/testing';
  2. import { MatFormFieldControlHarness } from './form-field/testing/control.mjs';
  3. /** Sets up the filter predicates for a datepicker input harness. */
  4. function getInputPredicate(type, options) {
  5. return new HarnessPredicate(type, options)
  6. .addOption('value', options.value, (harness, value) => {
  7. return HarnessPredicate.stringMatches(harness.getValue(), value);
  8. })
  9. .addOption('placeholder', options.placeholder, (harness, placeholder) => {
  10. return HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder);
  11. });
  12. }
  13. /** Base class for datepicker input harnesses. */
  14. class MatDatepickerInputHarnessBase extends MatFormFieldControlHarness {
  15. /** Whether the input is disabled. */
  16. async isDisabled() {
  17. return (await this.host()).getProperty('disabled');
  18. }
  19. /** Whether the input is required. */
  20. async isRequired() {
  21. return (await this.host()).getProperty('required');
  22. }
  23. /** Gets the value of the input. */
  24. async getValue() {
  25. // The "value" property of the native input is always defined.
  26. return await (await this.host()).getProperty('value');
  27. }
  28. /**
  29. * Sets the value of the input. The value will be set by simulating
  30. * keypresses that correspond to the given value.
  31. */
  32. async setValue(newValue) {
  33. const inputEl = await this.host();
  34. await inputEl.clear();
  35. // We don't want to send keys for the value if the value is an empty
  36. // string in order to clear the value. Sending keys with an empty string
  37. // still results in unnecessary focus events.
  38. if (newValue) {
  39. await inputEl.sendKeys(newValue);
  40. }
  41. await inputEl.dispatchEvent('change');
  42. }
  43. /** Gets the placeholder of the input. */
  44. async getPlaceholder() {
  45. return await (await this.host()).getProperty('placeholder');
  46. }
  47. /**
  48. * Focuses the input and returns a promise that indicates when the
  49. * action is complete.
  50. */
  51. async focus() {
  52. return (await this.host()).focus();
  53. }
  54. /**
  55. * Blurs the input and returns a promise that indicates when the
  56. * action is complete.
  57. */
  58. async blur() {
  59. return (await this.host()).blur();
  60. }
  61. /** Whether the input is focused. */
  62. async isFocused() {
  63. return (await this.host()).isFocused();
  64. }
  65. /** Gets the formatted minimum date for the input's value. */
  66. async getMin() {
  67. return (await this.host()).getAttribute('min');
  68. }
  69. /** Gets the formatted maximum date for the input's value. */
  70. async getMax() {
  71. return (await this.host()).getAttribute('max');
  72. }
  73. }
  74. /** Harness for interacting with a standard Material calendar cell in tests. */
  75. class MatCalendarCellHarness extends ComponentHarness {
  76. static hostSelector = '.mat-calendar-body-cell';
  77. /** Reference to the inner content element inside the cell. */
  78. _content = this.locatorFor('.mat-calendar-body-cell-content');
  79. /**
  80. * Gets a `HarnessPredicate` that can be used to search for a `MatCalendarCellHarness`
  81. * that meets certain criteria.
  82. * @param options Options for filtering which cell instances are considered a match.
  83. * @return a `HarnessPredicate` configured with the given options.
  84. */
  85. static with(options = {}) {
  86. return new HarnessPredicate(MatCalendarCellHarness, options)
  87. .addOption('text', options.text, (harness, text) => {
  88. return HarnessPredicate.stringMatches(harness.getText(), text);
  89. })
  90. .addOption('selected', options.selected, async (harness, selected) => {
  91. return (await harness.isSelected()) === selected;
  92. })
  93. .addOption('active', options.active, async (harness, active) => {
  94. return (await harness.isActive()) === active;
  95. })
  96. .addOption('disabled', options.disabled, async (harness, disabled) => {
  97. return (await harness.isDisabled()) === disabled;
  98. })
  99. .addOption('today', options.today, async (harness, today) => {
  100. return (await harness.isToday()) === today;
  101. })
  102. .addOption('inRange', options.inRange, async (harness, inRange) => {
  103. return (await harness.isInRange()) === inRange;
  104. })
  105. .addOption('inComparisonRange', options.inComparisonRange, async (harness, inComparisonRange) => {
  106. return (await harness.isInComparisonRange()) === inComparisonRange;
  107. })
  108. .addOption('inPreviewRange', options.inPreviewRange, async (harness, inPreviewRange) => {
  109. return (await harness.isInPreviewRange()) === inPreviewRange;
  110. });
  111. }
  112. /** Gets the text of the calendar cell. */
  113. async getText() {
  114. return (await this._content()).text();
  115. }
  116. /** Gets the aria-label of the calendar cell. */
  117. async getAriaLabel() {
  118. // We're guaranteed for the `aria-label` to be defined
  119. // since this is a private element that we control.
  120. return (await this.host()).getAttribute('aria-label');
  121. }
  122. /** Whether the cell is selected. */
  123. async isSelected() {
  124. const host = await this.host();
  125. return (await host.getAttribute('aria-pressed')) === 'true';
  126. }
  127. /** Whether the cell is disabled. */
  128. async isDisabled() {
  129. return this._hasState('disabled');
  130. }
  131. /** Whether the cell is currently activated using keyboard navigation. */
  132. async isActive() {
  133. return this._hasState('active');
  134. }
  135. /** Whether the cell represents today's date. */
  136. async isToday() {
  137. return (await this._content()).hasClass('mat-calendar-body-today');
  138. }
  139. /** Selects the calendar cell. Won't do anything if the cell is disabled. */
  140. async select() {
  141. return (await this.host()).click();
  142. }
  143. /** Hovers over the calendar cell. */
  144. async hover() {
  145. return (await this.host()).hover();
  146. }
  147. /** Moves the mouse away from the calendar cell. */
  148. async mouseAway() {
  149. return (await this.host()).mouseAway();
  150. }
  151. /** Focuses the calendar cell. */
  152. async focus() {
  153. return (await this.host()).focus();
  154. }
  155. /** Removes focus from the calendar cell. */
  156. async blur() {
  157. return (await this.host()).blur();
  158. }
  159. /** Whether the cell is the start of the main range. */
  160. async isRangeStart() {
  161. return this._hasState('range-start');
  162. }
  163. /** Whether the cell is the end of the main range. */
  164. async isRangeEnd() {
  165. return this._hasState('range-end');
  166. }
  167. /** Whether the cell is part of the main range. */
  168. async isInRange() {
  169. return this._hasState('in-range');
  170. }
  171. /** Whether the cell is the start of the comparison range. */
  172. async isComparisonRangeStart() {
  173. return this._hasState('comparison-start');
  174. }
  175. /** Whether the cell is the end of the comparison range. */
  176. async isComparisonRangeEnd() {
  177. return this._hasState('comparison-end');
  178. }
  179. /** Whether the cell is inside of the comparison range. */
  180. async isInComparisonRange() {
  181. return this._hasState('in-comparison-range');
  182. }
  183. /** Whether the cell is the start of the preview range. */
  184. async isPreviewRangeStart() {
  185. return this._hasState('preview-start');
  186. }
  187. /** Whether the cell is the end of the preview range. */
  188. async isPreviewRangeEnd() {
  189. return this._hasState('preview-end');
  190. }
  191. /** Whether the cell is inside of the preview range. */
  192. async isInPreviewRange() {
  193. return this._hasState('in-preview');
  194. }
  195. /** Returns whether the cell has a particular CSS class-based state. */
  196. async _hasState(name) {
  197. return (await this.host()).hasClass(`mat-calendar-body-${name}`);
  198. }
  199. }
  200. /** Possible views of a `MatCalendarHarness`. */
  201. var CalendarView;
  202. (function (CalendarView) {
  203. CalendarView[CalendarView["MONTH"] = 0] = "MONTH";
  204. CalendarView[CalendarView["YEAR"] = 1] = "YEAR";
  205. CalendarView[CalendarView["MULTI_YEAR"] = 2] = "MULTI_YEAR";
  206. })(CalendarView || (CalendarView = {}));
  207. /** Harness for interacting with a standard Material calendar in tests. */
  208. class MatCalendarHarness extends ComponentHarness {
  209. static hostSelector = '.mat-calendar';
  210. /** Queries for the calendar's period toggle button. */
  211. _periodButton = this.locatorFor('.mat-calendar-period-button');
  212. /**
  213. * Gets a `HarnessPredicate` that can be used to search for a `MatCalendarHarness`
  214. * that meets certain criteria.
  215. * @param options Options for filtering which calendar instances are considered a match.
  216. * @return a `HarnessPredicate` configured with the given options.
  217. */
  218. static with(options = {}) {
  219. return new HarnessPredicate(MatCalendarHarness, options);
  220. }
  221. /**
  222. * Gets a list of cells inside the calendar.
  223. * @param filter Optionally filters which cells are included.
  224. */
  225. async getCells(filter = {}) {
  226. return this.locatorForAll(MatCalendarCellHarness.with(filter))();
  227. }
  228. /** Gets the current view that is being shown inside the calendar. */
  229. async getCurrentView() {
  230. if (await this.locatorForOptional('mat-multi-year-view')()) {
  231. return CalendarView.MULTI_YEAR;
  232. }
  233. if (await this.locatorForOptional('mat-year-view')()) {
  234. return CalendarView.YEAR;
  235. }
  236. return CalendarView.MONTH;
  237. }
  238. /** Gets the label of the current calendar view. */
  239. async getCurrentViewLabel() {
  240. return (await this._periodButton()).text();
  241. }
  242. /** Changes the calendar view by clicking on the view toggle button. */
  243. async changeView() {
  244. return (await this._periodButton()).click();
  245. }
  246. /** Goes to the next page of the current view (e.g. next month when inside the month view). */
  247. async next() {
  248. return (await this.locatorFor('.mat-calendar-next-button')()).click();
  249. }
  250. /**
  251. * Goes to the previous page of the current view
  252. * (e.g. previous month when inside the month view).
  253. */
  254. async previous() {
  255. return (await this.locatorFor('.mat-calendar-previous-button')()).click();
  256. }
  257. /**
  258. * Selects a cell in the current calendar view.
  259. * @param filter An optional filter to apply to the cells. The first cell matching the filter
  260. * will be selected.
  261. */
  262. async selectCell(filter = {}) {
  263. const cells = await this.getCells(filter);
  264. if (!cells.length) {
  265. throw Error(`Cannot find calendar cell matching filter ${JSON.stringify(filter)}`);
  266. }
  267. await cells[0].select();
  268. }
  269. }
  270. /** Base class for harnesses that can trigger a calendar. */
  271. class DatepickerTriggerHarnessBase extends ComponentHarness {
  272. /** Opens the calendar if the trigger is enabled and it has a calendar. */
  273. async openCalendar() {
  274. const [isDisabled, hasCalendar] = await parallel(() => [this.isDisabled(), this.hasCalendar()]);
  275. if (!isDisabled && hasCalendar) {
  276. return this._openCalendar();
  277. }
  278. }
  279. /** Closes the calendar if it is open. */
  280. async closeCalendar() {
  281. if (await this.isCalendarOpen()) {
  282. await closeCalendar(getCalendarId(this.host()), this.documentRootLocatorFactory());
  283. // This is necessary so that we wait for the closing animation to finish in touch UI mode.
  284. await this.forceStabilize();
  285. }
  286. }
  287. /** Gets whether there is a calendar associated with the trigger. */
  288. async hasCalendar() {
  289. return (await getCalendarId(this.host())) != null;
  290. }
  291. /**
  292. * Gets the `MatCalendarHarness` that is associated with the trigger.
  293. * @param filter Optionally filters which calendar is included.
  294. */
  295. async getCalendar(filter = {}) {
  296. return getCalendar(filter, this.host(), this.documentRootLocatorFactory());
  297. }
  298. }
  299. /** Gets the ID of the calendar that a particular test element can trigger. */
  300. async function getCalendarId(host) {
  301. return (await host).getAttribute('data-mat-calendar');
  302. }
  303. /** Closes the calendar with a specific ID. */
  304. async function closeCalendar(calendarId, documentLocator) {
  305. // We close the calendar by clicking on the backdrop, even though all datepicker variants
  306. // have the ability to close by pressing escape. The backdrop is preferrable, because the
  307. // escape key has multiple functions inside a range picker (either cancel the current range
  308. // or close the calendar). Since we don't have access to set the ID on the backdrop in all
  309. // cases, we set a unique class instead which is the same as the calendar's ID and suffixed
  310. // with `-backdrop`.
  311. const backdropSelector = `.${await calendarId}-backdrop`;
  312. return (await documentLocator.locatorFor(backdropSelector)()).click();
  313. }
  314. /** Gets the test harness for a calendar associated with a particular host. */
  315. async function getCalendar(filter, host, documentLocator) {
  316. const calendarId = await getCalendarId(host);
  317. if (!calendarId) {
  318. throw Error(`Element is not associated with a calendar`);
  319. }
  320. return documentLocator.locatorFor(MatCalendarHarness.with({
  321. ...filter,
  322. selector: `#${calendarId}`,
  323. }))();
  324. }
  325. /** Harness for interacting with a standard Material datepicker inputs in tests. */
  326. class MatDatepickerInputHarness extends MatDatepickerInputHarnessBase {
  327. static hostSelector = '.mat-datepicker-input';
  328. /**
  329. * Gets a `HarnessPredicate` that can be used to search for a `MatDatepickerInputHarness`
  330. * that meets certain criteria.
  331. * @param options Options for filtering which input instances are considered a match.
  332. * @return a `HarnessPredicate` configured with the given options.
  333. */
  334. static with(options = {}) {
  335. return getInputPredicate(MatDatepickerInputHarness, options);
  336. }
  337. /** Gets whether the calendar associated with the input is open. */
  338. async isCalendarOpen() {
  339. // `aria-owns` is set only if there's an open datepicker so we can use it as an indicator.
  340. const host = await this.host();
  341. return (await host.getAttribute('aria-owns')) != null;
  342. }
  343. /** Opens the calendar associated with the input. */
  344. async openCalendar() {
  345. const [isDisabled, hasCalendar] = await parallel(() => [this.isDisabled(), this.hasCalendar()]);
  346. if (!isDisabled && hasCalendar) {
  347. // Alt + down arrow is the combination for opening the calendar with the keyboard.
  348. const host = await this.host();
  349. return host.sendKeys({ alt: true }, TestKey.DOWN_ARROW);
  350. }
  351. }
  352. /** Closes the calendar associated with the input. */
  353. async closeCalendar() {
  354. if (await this.isCalendarOpen()) {
  355. await closeCalendar(getCalendarId(this.host()), this.documentRootLocatorFactory());
  356. // This is necessary so that we wait for the closing animation to finish in touch UI mode.
  357. await this.forceStabilize();
  358. }
  359. }
  360. /** Whether a calendar is associated with the input. */
  361. async hasCalendar() {
  362. return (await getCalendarId(this.host())) != null;
  363. }
  364. /**
  365. * Gets the `MatCalendarHarness` that is associated with the trigger.
  366. * @param filter Optionally filters which calendar is included.
  367. */
  368. async getCalendar(filter = {}) {
  369. return getCalendar(filter, this.host(), this.documentRootLocatorFactory());
  370. }
  371. }
  372. /** Harness for interacting with a standard Material date range start input in tests. */
  373. class MatStartDateHarness extends MatDatepickerInputHarnessBase {
  374. static hostSelector = '.mat-start-date';
  375. /**
  376. * Gets a `HarnessPredicate` that can be used to search for a `MatStartDateHarness`
  377. * that meets certain criteria.
  378. * @param options Options for filtering which input instances are considered a match.
  379. * @return a `HarnessPredicate` configured with the given options.
  380. */
  381. static with(options = {}) {
  382. return getInputPredicate(MatStartDateHarness, options);
  383. }
  384. }
  385. /** Harness for interacting with a standard Material date range end input in tests. */
  386. class MatEndDateHarness extends MatDatepickerInputHarnessBase {
  387. static hostSelector = '.mat-end-date';
  388. /**
  389. * Gets a `HarnessPredicate` that can be used to search for a `MatEndDateHarness`
  390. * that meets certain criteria.
  391. * @param options Options for filtering which input instances are considered a match.
  392. * @return a `HarnessPredicate` configured with the given options.
  393. */
  394. static with(options = {}) {
  395. return getInputPredicate(MatEndDateHarness, options);
  396. }
  397. }
  398. /** Harness for interacting with a standard Material date range input in tests. */
  399. class MatDateRangeInputHarness extends DatepickerTriggerHarnessBase {
  400. static hostSelector = '.mat-date-range-input';
  401. /**
  402. * Gets a `HarnessPredicate` that can be used to search for a `MatDateRangeInputHarness`
  403. * that meets certain criteria.
  404. * @param options Options for filtering which input instances are considered a match.
  405. * @return a `HarnessPredicate` configured with the given options.
  406. */
  407. static with(options = {}) {
  408. return new HarnessPredicate(MatDateRangeInputHarness, options).addOption('value', options.value, (harness, value) => HarnessPredicate.stringMatches(harness.getValue(), value));
  409. }
  410. /** Gets the combined value of the start and end inputs, including the separator. */
  411. async getValue() {
  412. const [start, end, separator] = await parallel(() => [
  413. this.getStartInput().then(input => input.getValue()),
  414. this.getEndInput().then(input => input.getValue()),
  415. this.getSeparator(),
  416. ]);
  417. return start + `${end ? ` ${separator} ${end}` : ''}`;
  418. }
  419. /** Gets the inner start date input inside the range input. */
  420. async getStartInput() {
  421. // Don't pass in filters here since the start input is required and there can only be one.
  422. return this.locatorFor(MatStartDateHarness)();
  423. }
  424. /** Gets the inner start date input inside the range input. */
  425. async getEndInput() {
  426. // Don't pass in filters here since the end input is required and there can only be one.
  427. return this.locatorFor(MatEndDateHarness)();
  428. }
  429. /** Gets the separator text between the values of the two inputs. */
  430. async getSeparator() {
  431. return (await this.locatorFor('.mat-date-range-input-separator')()).text();
  432. }
  433. /** Gets whether the range input is disabled. */
  434. async isDisabled() {
  435. // We consider the input as disabled if both of the sub-inputs are disabled.
  436. const [startDisabled, endDisabled] = await parallel(() => [
  437. this.getStartInput().then(input => input.isDisabled()),
  438. this.getEndInput().then(input => input.isDisabled()),
  439. ]);
  440. return startDisabled && endDisabled;
  441. }
  442. /** Gets whether the range input is required. */
  443. async isRequired() {
  444. return (await this.host()).hasClass('mat-date-range-input-required');
  445. }
  446. /** Opens the calendar associated with the input. */
  447. async isCalendarOpen() {
  448. // `aria-owns` is set on both inputs only if there's an
  449. // open range picker so we can use it as an indicator.
  450. const startHost = await (await this.getStartInput()).host();
  451. return (await startHost.getAttribute('aria-owns')) != null;
  452. }
  453. async _openCalendar() {
  454. // Alt + down arrow is the combination for opening the calendar with the keyboard.
  455. const startHost = await (await this.getStartInput()).host();
  456. return startHost.sendKeys({ alt: true }, TestKey.DOWN_ARROW);
  457. }
  458. }
  459. export { CalendarView as C, DatepickerTriggerHarnessBase as D, MatDatepickerInputHarness as M, MatStartDateHarness as a, MatEndDateHarness as b, MatDateRangeInputHarness as c, MatCalendarHarness as d, MatCalendarCellHarness as e };
  460. //# sourceMappingURL=date-range-input-harness-Bp1T4oUe.mjs.map