TestScheduler.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. import { __extends, __read, __spreadArray, __values } from "tslib";
  2. import { Observable } from '../Observable';
  3. import { ColdObservable } from './ColdObservable';
  4. import { HotObservable } from './HotObservable';
  5. import { SubscriptionLog } from './SubscriptionLog';
  6. import { VirtualTimeScheduler, VirtualAction } from '../scheduler/VirtualTimeScheduler';
  7. import { COMPLETE_NOTIFICATION, errorNotification, nextNotification } from '../NotificationFactories';
  8. import { dateTimestampProvider } from '../scheduler/dateTimestampProvider';
  9. import { performanceTimestampProvider } from '../scheduler/performanceTimestampProvider';
  10. import { animationFrameProvider } from '../scheduler/animationFrameProvider';
  11. import { immediateProvider } from '../scheduler/immediateProvider';
  12. import { intervalProvider } from '../scheduler/intervalProvider';
  13. import { timeoutProvider } from '../scheduler/timeoutProvider';
  14. var defaultMaxFrame = 750;
  15. var TestScheduler = (function (_super) {
  16. __extends(TestScheduler, _super);
  17. function TestScheduler(assertDeepEqual) {
  18. var _this = _super.call(this, VirtualAction, defaultMaxFrame) || this;
  19. _this.assertDeepEqual = assertDeepEqual;
  20. _this.hotObservables = [];
  21. _this.coldObservables = [];
  22. _this.flushTests = [];
  23. _this.runMode = false;
  24. return _this;
  25. }
  26. TestScheduler.prototype.createTime = function (marbles) {
  27. var indexOf = this.runMode ? marbles.trim().indexOf('|') : marbles.indexOf('|');
  28. if (indexOf === -1) {
  29. throw new Error('marble diagram for time should have a completion marker "|"');
  30. }
  31. return indexOf * TestScheduler.frameTimeFactor;
  32. };
  33. TestScheduler.prototype.createColdObservable = function (marbles, values, error) {
  34. if (marbles.indexOf('^') !== -1) {
  35. throw new Error('cold observable cannot have subscription offset "^"');
  36. }
  37. if (marbles.indexOf('!') !== -1) {
  38. throw new Error('cold observable cannot have unsubscription marker "!"');
  39. }
  40. var messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode);
  41. var cold = new ColdObservable(messages, this);
  42. this.coldObservables.push(cold);
  43. return cold;
  44. };
  45. TestScheduler.prototype.createHotObservable = function (marbles, values, error) {
  46. if (marbles.indexOf('!') !== -1) {
  47. throw new Error('hot observable cannot have unsubscription marker "!"');
  48. }
  49. var messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode);
  50. var subject = new HotObservable(messages, this);
  51. this.hotObservables.push(subject);
  52. return subject;
  53. };
  54. TestScheduler.prototype.materializeInnerObservable = function (observable, outerFrame) {
  55. var _this = this;
  56. var messages = [];
  57. observable.subscribe({
  58. next: function (value) {
  59. messages.push({ frame: _this.frame - outerFrame, notification: nextNotification(value) });
  60. },
  61. error: function (error) {
  62. messages.push({ frame: _this.frame - outerFrame, notification: errorNotification(error) });
  63. },
  64. complete: function () {
  65. messages.push({ frame: _this.frame - outerFrame, notification: COMPLETE_NOTIFICATION });
  66. },
  67. });
  68. return messages;
  69. };
  70. TestScheduler.prototype.expectObservable = function (observable, subscriptionMarbles) {
  71. var _this = this;
  72. if (subscriptionMarbles === void 0) { subscriptionMarbles = null; }
  73. var actual = [];
  74. var flushTest = { actual: actual, ready: false };
  75. var subscriptionParsed = TestScheduler.parseMarblesAsSubscriptions(subscriptionMarbles, this.runMode);
  76. var subscriptionFrame = subscriptionParsed.subscribedFrame === Infinity ? 0 : subscriptionParsed.subscribedFrame;
  77. var unsubscriptionFrame = subscriptionParsed.unsubscribedFrame;
  78. var subscription;
  79. this.schedule(function () {
  80. subscription = observable.subscribe({
  81. next: function (x) {
  82. var value = x instanceof Observable ? _this.materializeInnerObservable(x, _this.frame) : x;
  83. actual.push({ frame: _this.frame, notification: nextNotification(value) });
  84. },
  85. error: function (error) {
  86. actual.push({ frame: _this.frame, notification: errorNotification(error) });
  87. },
  88. complete: function () {
  89. actual.push({ frame: _this.frame, notification: COMPLETE_NOTIFICATION });
  90. },
  91. });
  92. }, subscriptionFrame);
  93. if (unsubscriptionFrame !== Infinity) {
  94. this.schedule(function () { return subscription.unsubscribe(); }, unsubscriptionFrame);
  95. }
  96. this.flushTests.push(flushTest);
  97. var runMode = this.runMode;
  98. return {
  99. toBe: function (marbles, values, errorValue) {
  100. flushTest.ready = true;
  101. flushTest.expected = TestScheduler.parseMarbles(marbles, values, errorValue, true, runMode);
  102. },
  103. toEqual: function (other) {
  104. flushTest.ready = true;
  105. flushTest.expected = [];
  106. _this.schedule(function () {
  107. subscription = other.subscribe({
  108. next: function (x) {
  109. var value = x instanceof Observable ? _this.materializeInnerObservable(x, _this.frame) : x;
  110. flushTest.expected.push({ frame: _this.frame, notification: nextNotification(value) });
  111. },
  112. error: function (error) {
  113. flushTest.expected.push({ frame: _this.frame, notification: errorNotification(error) });
  114. },
  115. complete: function () {
  116. flushTest.expected.push({ frame: _this.frame, notification: COMPLETE_NOTIFICATION });
  117. },
  118. });
  119. }, subscriptionFrame);
  120. },
  121. };
  122. };
  123. TestScheduler.prototype.expectSubscriptions = function (actualSubscriptionLogs) {
  124. var flushTest = { actual: actualSubscriptionLogs, ready: false };
  125. this.flushTests.push(flushTest);
  126. var runMode = this.runMode;
  127. return {
  128. toBe: function (marblesOrMarblesArray) {
  129. var marblesArray = typeof marblesOrMarblesArray === 'string' ? [marblesOrMarblesArray] : marblesOrMarblesArray;
  130. flushTest.ready = true;
  131. flushTest.expected = marblesArray
  132. .map(function (marbles) { return TestScheduler.parseMarblesAsSubscriptions(marbles, runMode); })
  133. .filter(function (marbles) { return marbles.subscribedFrame !== Infinity; });
  134. },
  135. };
  136. };
  137. TestScheduler.prototype.flush = function () {
  138. var _this = this;
  139. var hotObservables = this.hotObservables;
  140. while (hotObservables.length > 0) {
  141. hotObservables.shift().setup();
  142. }
  143. _super.prototype.flush.call(this);
  144. this.flushTests = this.flushTests.filter(function (test) {
  145. if (test.ready) {
  146. _this.assertDeepEqual(test.actual, test.expected);
  147. return false;
  148. }
  149. return true;
  150. });
  151. };
  152. TestScheduler.parseMarblesAsSubscriptions = function (marbles, runMode) {
  153. var _this = this;
  154. if (runMode === void 0) { runMode = false; }
  155. if (typeof marbles !== 'string') {
  156. return new SubscriptionLog(Infinity);
  157. }
  158. var characters = __spreadArray([], __read(marbles));
  159. var len = characters.length;
  160. var groupStart = -1;
  161. var subscriptionFrame = Infinity;
  162. var unsubscriptionFrame = Infinity;
  163. var frame = 0;
  164. var _loop_1 = function (i) {
  165. var nextFrame = frame;
  166. var advanceFrameBy = function (count) {
  167. nextFrame += count * _this.frameTimeFactor;
  168. };
  169. var c = characters[i];
  170. switch (c) {
  171. case ' ':
  172. if (!runMode) {
  173. advanceFrameBy(1);
  174. }
  175. break;
  176. case '-':
  177. advanceFrameBy(1);
  178. break;
  179. case '(':
  180. groupStart = frame;
  181. advanceFrameBy(1);
  182. break;
  183. case ')':
  184. groupStart = -1;
  185. advanceFrameBy(1);
  186. break;
  187. case '^':
  188. if (subscriptionFrame !== Infinity) {
  189. throw new Error("found a second subscription point '^' in a " + 'subscription marble diagram. There can only be one.');
  190. }
  191. subscriptionFrame = groupStart > -1 ? groupStart : frame;
  192. advanceFrameBy(1);
  193. break;
  194. case '!':
  195. if (unsubscriptionFrame !== Infinity) {
  196. throw new Error("found a second unsubscription point '!' in a " + 'subscription marble diagram. There can only be one.');
  197. }
  198. unsubscriptionFrame = groupStart > -1 ? groupStart : frame;
  199. break;
  200. default:
  201. if (runMode && c.match(/^[0-9]$/)) {
  202. if (i === 0 || characters[i - 1] === ' ') {
  203. var buffer = characters.slice(i).join('');
  204. var match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);
  205. if (match) {
  206. i += match[0].length - 1;
  207. var duration = parseFloat(match[1]);
  208. var unit = match[2];
  209. var durationInMs = void 0;
  210. switch (unit) {
  211. case 'ms':
  212. durationInMs = duration;
  213. break;
  214. case 's':
  215. durationInMs = duration * 1000;
  216. break;
  217. case 'm':
  218. durationInMs = duration * 1000 * 60;
  219. break;
  220. default:
  221. break;
  222. }
  223. advanceFrameBy(durationInMs / this_1.frameTimeFactor);
  224. break;
  225. }
  226. }
  227. }
  228. throw new Error("there can only be '^' and '!' markers in a " + "subscription marble diagram. Found instead '" + c + "'.");
  229. }
  230. frame = nextFrame;
  231. out_i_1 = i;
  232. };
  233. var this_1 = this, out_i_1;
  234. for (var i = 0; i < len; i++) {
  235. _loop_1(i);
  236. i = out_i_1;
  237. }
  238. if (unsubscriptionFrame < 0) {
  239. return new SubscriptionLog(subscriptionFrame);
  240. }
  241. else {
  242. return new SubscriptionLog(subscriptionFrame, unsubscriptionFrame);
  243. }
  244. };
  245. TestScheduler.parseMarbles = function (marbles, values, errorValue, materializeInnerObservables, runMode) {
  246. var _this = this;
  247. if (materializeInnerObservables === void 0) { materializeInnerObservables = false; }
  248. if (runMode === void 0) { runMode = false; }
  249. if (marbles.indexOf('!') !== -1) {
  250. throw new Error('conventional marble diagrams cannot have the ' + 'unsubscription marker "!"');
  251. }
  252. var characters = __spreadArray([], __read(marbles));
  253. var len = characters.length;
  254. var testMessages = [];
  255. var subIndex = runMode ? marbles.replace(/^[ ]+/, '').indexOf('^') : marbles.indexOf('^');
  256. var frame = subIndex === -1 ? 0 : subIndex * -this.frameTimeFactor;
  257. var getValue = typeof values !== 'object'
  258. ? function (x) { return x; }
  259. : function (x) {
  260. if (materializeInnerObservables && values[x] instanceof ColdObservable) {
  261. return values[x].messages;
  262. }
  263. return values[x];
  264. };
  265. var groupStart = -1;
  266. var _loop_2 = function (i) {
  267. var nextFrame = frame;
  268. var advanceFrameBy = function (count) {
  269. nextFrame += count * _this.frameTimeFactor;
  270. };
  271. var notification = void 0;
  272. var c = characters[i];
  273. switch (c) {
  274. case ' ':
  275. if (!runMode) {
  276. advanceFrameBy(1);
  277. }
  278. break;
  279. case '-':
  280. advanceFrameBy(1);
  281. break;
  282. case '(':
  283. groupStart = frame;
  284. advanceFrameBy(1);
  285. break;
  286. case ')':
  287. groupStart = -1;
  288. advanceFrameBy(1);
  289. break;
  290. case '|':
  291. notification = COMPLETE_NOTIFICATION;
  292. advanceFrameBy(1);
  293. break;
  294. case '^':
  295. advanceFrameBy(1);
  296. break;
  297. case '#':
  298. notification = errorNotification(errorValue || 'error');
  299. advanceFrameBy(1);
  300. break;
  301. default:
  302. if (runMode && c.match(/^[0-9]$/)) {
  303. if (i === 0 || characters[i - 1] === ' ') {
  304. var buffer = characters.slice(i).join('');
  305. var match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);
  306. if (match) {
  307. i += match[0].length - 1;
  308. var duration = parseFloat(match[1]);
  309. var unit = match[2];
  310. var durationInMs = void 0;
  311. switch (unit) {
  312. case 'ms':
  313. durationInMs = duration;
  314. break;
  315. case 's':
  316. durationInMs = duration * 1000;
  317. break;
  318. case 'm':
  319. durationInMs = duration * 1000 * 60;
  320. break;
  321. default:
  322. break;
  323. }
  324. advanceFrameBy(durationInMs / this_2.frameTimeFactor);
  325. break;
  326. }
  327. }
  328. }
  329. notification = nextNotification(getValue(c));
  330. advanceFrameBy(1);
  331. break;
  332. }
  333. if (notification) {
  334. testMessages.push({ frame: groupStart > -1 ? groupStart : frame, notification: notification });
  335. }
  336. frame = nextFrame;
  337. out_i_2 = i;
  338. };
  339. var this_2 = this, out_i_2;
  340. for (var i = 0; i < len; i++) {
  341. _loop_2(i);
  342. i = out_i_2;
  343. }
  344. return testMessages;
  345. };
  346. TestScheduler.prototype.createAnimator = function () {
  347. var _this = this;
  348. if (!this.runMode) {
  349. throw new Error('animate() must only be used in run mode');
  350. }
  351. var lastHandle = 0;
  352. var map;
  353. var delegate = {
  354. requestAnimationFrame: function (callback) {
  355. if (!map) {
  356. throw new Error('animate() was not called within run()');
  357. }
  358. var handle = ++lastHandle;
  359. map.set(handle, callback);
  360. return handle;
  361. },
  362. cancelAnimationFrame: function (handle) {
  363. if (!map) {
  364. throw new Error('animate() was not called within run()');
  365. }
  366. map.delete(handle);
  367. },
  368. };
  369. var animate = function (marbles) {
  370. var e_1, _a;
  371. if (map) {
  372. throw new Error('animate() must not be called more than once within run()');
  373. }
  374. if (/[|#]/.test(marbles)) {
  375. throw new Error('animate() must not complete or error');
  376. }
  377. map = new Map();
  378. var messages = TestScheduler.parseMarbles(marbles, undefined, undefined, undefined, true);
  379. try {
  380. for (var messages_1 = __values(messages), messages_1_1 = messages_1.next(); !messages_1_1.done; messages_1_1 = messages_1.next()) {
  381. var message = messages_1_1.value;
  382. _this.schedule(function () {
  383. var e_2, _a;
  384. var now = _this.now();
  385. var callbacks = Array.from(map.values());
  386. map.clear();
  387. try {
  388. for (var callbacks_1 = (e_2 = void 0, __values(callbacks)), callbacks_1_1 = callbacks_1.next(); !callbacks_1_1.done; callbacks_1_1 = callbacks_1.next()) {
  389. var callback = callbacks_1_1.value;
  390. callback(now);
  391. }
  392. }
  393. catch (e_2_1) { e_2 = { error: e_2_1 }; }
  394. finally {
  395. try {
  396. if (callbacks_1_1 && !callbacks_1_1.done && (_a = callbacks_1.return)) _a.call(callbacks_1);
  397. }
  398. finally { if (e_2) throw e_2.error; }
  399. }
  400. }, message.frame);
  401. }
  402. }
  403. catch (e_1_1) { e_1 = { error: e_1_1 }; }
  404. finally {
  405. try {
  406. if (messages_1_1 && !messages_1_1.done && (_a = messages_1.return)) _a.call(messages_1);
  407. }
  408. finally { if (e_1) throw e_1.error; }
  409. }
  410. };
  411. return { animate: animate, delegate: delegate };
  412. };
  413. TestScheduler.prototype.createDelegates = function () {
  414. var _this = this;
  415. var lastHandle = 0;
  416. var scheduleLookup = new Map();
  417. var run = function () {
  418. var now = _this.now();
  419. var scheduledRecords = Array.from(scheduleLookup.values());
  420. var scheduledRecordsDue = scheduledRecords.filter(function (_a) {
  421. var due = _a.due;
  422. return due <= now;
  423. });
  424. var dueImmediates = scheduledRecordsDue.filter(function (_a) {
  425. var type = _a.type;
  426. return type === 'immediate';
  427. });
  428. if (dueImmediates.length > 0) {
  429. var _a = dueImmediates[0], handle = _a.handle, handler = _a.handler;
  430. scheduleLookup.delete(handle);
  431. handler();
  432. return;
  433. }
  434. var dueIntervals = scheduledRecordsDue.filter(function (_a) {
  435. var type = _a.type;
  436. return type === 'interval';
  437. });
  438. if (dueIntervals.length > 0) {
  439. var firstDueInterval = dueIntervals[0];
  440. var duration = firstDueInterval.duration, handler = firstDueInterval.handler;
  441. firstDueInterval.due = now + duration;
  442. firstDueInterval.subscription = _this.schedule(run, duration);
  443. handler();
  444. return;
  445. }
  446. var dueTimeouts = scheduledRecordsDue.filter(function (_a) {
  447. var type = _a.type;
  448. return type === 'timeout';
  449. });
  450. if (dueTimeouts.length > 0) {
  451. var _b = dueTimeouts[0], handle = _b.handle, handler = _b.handler;
  452. scheduleLookup.delete(handle);
  453. handler();
  454. return;
  455. }
  456. throw new Error('Expected a due immediate or interval');
  457. };
  458. var immediate = {
  459. setImmediate: function (handler) {
  460. var handle = ++lastHandle;
  461. scheduleLookup.set(handle, {
  462. due: _this.now(),
  463. duration: 0,
  464. handle: handle,
  465. handler: handler,
  466. subscription: _this.schedule(run, 0),
  467. type: 'immediate',
  468. });
  469. return handle;
  470. },
  471. clearImmediate: function (handle) {
  472. var value = scheduleLookup.get(handle);
  473. if (value) {
  474. value.subscription.unsubscribe();
  475. scheduleLookup.delete(handle);
  476. }
  477. },
  478. };
  479. var interval = {
  480. setInterval: function (handler, duration) {
  481. if (duration === void 0) { duration = 0; }
  482. var handle = ++lastHandle;
  483. scheduleLookup.set(handle, {
  484. due: _this.now() + duration,
  485. duration: duration,
  486. handle: handle,
  487. handler: handler,
  488. subscription: _this.schedule(run, duration),
  489. type: 'interval',
  490. });
  491. return handle;
  492. },
  493. clearInterval: function (handle) {
  494. var value = scheduleLookup.get(handle);
  495. if (value) {
  496. value.subscription.unsubscribe();
  497. scheduleLookup.delete(handle);
  498. }
  499. },
  500. };
  501. var timeout = {
  502. setTimeout: function (handler, duration) {
  503. if (duration === void 0) { duration = 0; }
  504. var handle = ++lastHandle;
  505. scheduleLookup.set(handle, {
  506. due: _this.now() + duration,
  507. duration: duration,
  508. handle: handle,
  509. handler: handler,
  510. subscription: _this.schedule(run, duration),
  511. type: 'timeout',
  512. });
  513. return handle;
  514. },
  515. clearTimeout: function (handle) {
  516. var value = scheduleLookup.get(handle);
  517. if (value) {
  518. value.subscription.unsubscribe();
  519. scheduleLookup.delete(handle);
  520. }
  521. },
  522. };
  523. return { immediate: immediate, interval: interval, timeout: timeout };
  524. };
  525. TestScheduler.prototype.run = function (callback) {
  526. var prevFrameTimeFactor = TestScheduler.frameTimeFactor;
  527. var prevMaxFrames = this.maxFrames;
  528. TestScheduler.frameTimeFactor = 1;
  529. this.maxFrames = Infinity;
  530. this.runMode = true;
  531. var animator = this.createAnimator();
  532. var delegates = this.createDelegates();
  533. animationFrameProvider.delegate = animator.delegate;
  534. dateTimestampProvider.delegate = this;
  535. immediateProvider.delegate = delegates.immediate;
  536. intervalProvider.delegate = delegates.interval;
  537. timeoutProvider.delegate = delegates.timeout;
  538. performanceTimestampProvider.delegate = this;
  539. var helpers = {
  540. cold: this.createColdObservable.bind(this),
  541. hot: this.createHotObservable.bind(this),
  542. flush: this.flush.bind(this),
  543. time: this.createTime.bind(this),
  544. expectObservable: this.expectObservable.bind(this),
  545. expectSubscriptions: this.expectSubscriptions.bind(this),
  546. animate: animator.animate,
  547. };
  548. try {
  549. var ret = callback(helpers);
  550. this.flush();
  551. return ret;
  552. }
  553. finally {
  554. TestScheduler.frameTimeFactor = prevFrameTimeFactor;
  555. this.maxFrames = prevMaxFrames;
  556. this.runMode = false;
  557. animationFrameProvider.delegate = undefined;
  558. dateTimestampProvider.delegate = undefined;
  559. immediateProvider.delegate = undefined;
  560. intervalProvider.delegate = undefined;
  561. timeoutProvider.delegate = undefined;
  562. performanceTimestampProvider.delegate = undefined;
  563. }
  564. };
  565. TestScheduler.frameTimeFactor = 10;
  566. return TestScheduler;
  567. }(VirtualTimeScheduler));
  568. export { TestScheduler };
  569. //# sourceMappingURL=TestScheduler.js.map