jasmine-marbles.umd.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('rxjs/testing'), require('lodash'), require('rxjs')) :
  3. typeof define === 'function' && define.amd ? define(['exports', 'rxjs/testing', 'lodash', 'rxjs'], factory) :
  4. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['jasmine-marbles'] = {}, global.Rx, global._, global.Rx));
  5. }(this, (function (exports, testing, lodash, rxjs) { 'use strict';
  6. /* istanbul ignore file */
  7. /**
  8. * @see https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/observableMatcher.ts
  9. */
  10. function stringify(x) {
  11. return JSON.stringify(x, function (_key, value) {
  12. if (Array.isArray(value)) {
  13. return ('[' +
  14. value.map(function (i) {
  15. return '\n\t' + stringify(i);
  16. }) +
  17. '\n]');
  18. }
  19. return value;
  20. })
  21. .replace(/\\"/g, '"')
  22. .replace(/\\t/g, '\t')
  23. .replace(/\\n/g, '\n');
  24. }
  25. /**
  26. * @see https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/observableMatcher.ts
  27. */
  28. function deleteErrorNotificationStack(marble) {
  29. const { notification } = marble;
  30. if (notification) {
  31. const { kind, error } = notification;
  32. if (kind === 'E' && error instanceof Error) {
  33. notification.error = { name: error.name, message: error.message };
  34. }
  35. }
  36. return marble;
  37. }
  38. /**
  39. * @see https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/observableMatcher.ts
  40. */
  41. function observableMatcher(actual, expected) {
  42. if (Array.isArray(actual) && Array.isArray(expected)) {
  43. actual = actual.map(deleteErrorNotificationStack);
  44. expected = expected.map(deleteErrorNotificationStack);
  45. const passed = lodash.isEqual(actual, expected);
  46. if (passed) {
  47. expect(passed).toBe(true);
  48. return;
  49. }
  50. let message = '\nExpected \n';
  51. actual.forEach((x) => (message += `\t${stringify(x)}\n`));
  52. message += '\t\nto deep equal \n';
  53. expected.forEach((x) => (message += `\t${stringify(x)}\n`));
  54. fail(message);
  55. }
  56. else {
  57. expect(actual).toEqual(expected);
  58. }
  59. }
  60. let scheduler;
  61. function initTestScheduler() {
  62. scheduler = new testing.TestScheduler(observableMatcher);
  63. scheduler['runMode'] = true;
  64. }
  65. function getTestScheduler() {
  66. if (scheduler) {
  67. return scheduler;
  68. }
  69. throw new Error('No test scheduler initialized');
  70. }
  71. function resetTestScheduler() {
  72. scheduler = null;
  73. }
  74. class TestColdObservable extends rxjs.Observable {
  75. constructor(marbles, values, error) {
  76. super();
  77. this.marbles = marbles;
  78. this.values = values;
  79. this.error = error;
  80. const scheduler = getTestScheduler();
  81. const cold = scheduler.createColdObservable(marbles, values, error);
  82. this.source = cold;
  83. }
  84. getSubscriptions() {
  85. return this.source['subscriptions'];
  86. }
  87. }
  88. class TestHotObservable extends rxjs.Observable {
  89. constructor(marbles, values, error) {
  90. super();
  91. this.marbles = marbles;
  92. this.values = values;
  93. this.error = error;
  94. const scheduler = getTestScheduler();
  95. const hot = scheduler.createHotObservable(marbles, values, error);
  96. this.source = hot;
  97. }
  98. getSubscriptions() {
  99. return this.source['subscriptions'];
  100. }
  101. }
  102. function mapSymbolsToNotifications(marbles, messagesArg) {
  103. const messages = messagesArg.slice();
  104. const result = {};
  105. for (let i = 0; i < marbles.length; i++) {
  106. const symbol = marbles[i];
  107. switch (symbol) {
  108. case ' ':
  109. case '-':
  110. case '^':
  111. case '(':
  112. case ')':
  113. break;
  114. case '#':
  115. case '|': {
  116. messages.shift();
  117. break;
  118. }
  119. default: {
  120. if ((symbol.match(/^[0-9]$/) && i === 0) || marbles[i - 1] === ' ') {
  121. const buffer = marbles.slice(i);
  122. const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);
  123. if (match) {
  124. i += match[0].length - 1;
  125. }
  126. break;
  127. }
  128. const message = messages.shift();
  129. result[symbol] = message.notification;
  130. }
  131. }
  132. }
  133. return result;
  134. }
  135. function unparseMarble(result, assignSymbolFn) {
  136. const FRAME_TIME_FACTOR = 10; // need to be up to date with `TestScheduler.frameTimeFactor`
  137. let frames = 0;
  138. let marble = '';
  139. let isInGroup = false;
  140. let groupMembersAmount = 0;
  141. let index = 0;
  142. const isNextMessageInTheSameFrame = () => {
  143. const nextMessage = result[index + 1];
  144. return nextMessage && nextMessage.frame === result[index].frame;
  145. };
  146. result.forEach((testMessage, i) => {
  147. index = i;
  148. const framesDiff = testMessage.frame - frames;
  149. const emptyFramesAmount = framesDiff > 0 ? framesDiff / FRAME_TIME_FACTOR : 0;
  150. marble += '-'.repeat(emptyFramesAmount);
  151. if (isNextMessageInTheSameFrame()) {
  152. if (!isInGroup) {
  153. marble += '(';
  154. }
  155. isInGroup = true;
  156. }
  157. switch (testMessage.notification.kind) {
  158. case 'N':
  159. marble += assignSymbolFn(testMessage.notification);
  160. break;
  161. case 'E':
  162. marble += '#';
  163. break;
  164. case 'C':
  165. marble += '|';
  166. break;
  167. }
  168. if (isInGroup) {
  169. groupMembersAmount += 1;
  170. }
  171. if (!isNextMessageInTheSameFrame() && isInGroup) {
  172. marble += ')';
  173. isInGroup = false;
  174. frames += (groupMembersAmount + 1) * FRAME_TIME_FACTOR;
  175. groupMembersAmount = 0;
  176. }
  177. else {
  178. frames = testMessage.frame + FRAME_TIME_FACTOR;
  179. }
  180. });
  181. return marble;
  182. }
  183. /*
  184. * Based on source code found in rxjs library
  185. * https://github.com/ReactiveX/rxjs/blob/master/src/testing/TestScheduler.ts
  186. *
  187. */
  188. function materializeInnerObservable(observable, outerFrame) {
  189. const messages = [];
  190. const scheduler = getTestScheduler();
  191. observable.subscribe({
  192. next: (value) => {
  193. messages.push({
  194. frame: scheduler.frame - outerFrame,
  195. notification: {
  196. kind: 'N',
  197. value,
  198. error: undefined,
  199. },
  200. });
  201. },
  202. error: (error) => {
  203. messages.push({
  204. frame: scheduler.frame - outerFrame,
  205. notification: {
  206. kind: 'E',
  207. value: undefined,
  208. error,
  209. },
  210. });
  211. },
  212. complete: () => {
  213. messages.push({
  214. frame: scheduler.frame - outerFrame,
  215. notification: {
  216. kind: 'C',
  217. value: undefined,
  218. error: undefined,
  219. },
  220. });
  221. },
  222. });
  223. return messages;
  224. }
  225. function toHaveSubscriptionsComparer(actual, marbles) {
  226. const marblesArray = typeof marbles === 'string' ? [marbles] : marbles;
  227. const results = marblesArray.map((marbles) => testing.TestScheduler.parseMarblesAsSubscriptions(marbles));
  228. expect(results).toEqual(actual.getSubscriptions());
  229. return { pass: true, message: () => '' };
  230. }
  231. function toBeObservableComparer(actual, fixture) {
  232. const results = [];
  233. const scheduler = getTestScheduler();
  234. scheduler.schedule(() => {
  235. actual.subscribe({
  236. next: (x) => {
  237. let value = x;
  238. // Support Observable-of-Observables
  239. if (x instanceof rxjs.Observable) {
  240. value = materializeInnerObservable(value, scheduler.frame);
  241. }
  242. results.push({
  243. frame: scheduler.frame,
  244. notification: {
  245. kind: 'N',
  246. value,
  247. error: undefined,
  248. },
  249. });
  250. },
  251. error: (error) => {
  252. results.push({
  253. frame: scheduler.frame,
  254. notification: {
  255. kind: 'E',
  256. value: undefined,
  257. error,
  258. },
  259. });
  260. },
  261. complete: () => {
  262. results.push({
  263. frame: scheduler.frame,
  264. notification: {
  265. kind: 'C',
  266. value: undefined,
  267. error: undefined,
  268. },
  269. });
  270. },
  271. });
  272. });
  273. scheduler.flush();
  274. const expected = testing.TestScheduler.parseMarbles(fixture.marbles, fixture.values, fixture.error, true, true);
  275. try {
  276. expect(results).toEqual(expected);
  277. return { pass: true, message: () => '' };
  278. }
  279. catch (e) {
  280. const mapNotificationToSymbol = buildNotificationToSymbolMapper(fixture.marbles, expected, lodash.isEqual);
  281. const receivedMarble = unparseMarble(results, mapNotificationToSymbol);
  282. const message = formatMessage(fixture.marbles, expected, receivedMarble, results);
  283. return { pass: false, message: () => message };
  284. }
  285. }
  286. function buildNotificationToSymbolMapper(expectedMarbles, expectedMessages, equalityFn) {
  287. const symbolsToNotificationsMap = mapSymbolsToNotifications(expectedMarbles, expectedMessages);
  288. return (notification) => {
  289. const mapped = Object.keys(symbolsToNotificationsMap).find((key) => equalityFn(symbolsToNotificationsMap[key], notification));
  290. return mapped || '?';
  291. };
  292. }
  293. function formatMessage(expectedMarbles, expectedMessages, receivedMarbles, receivedMessages) {
  294. return `
  295. Expected: ${expectedMarbles},
  296. Received: ${receivedMarbles},
  297. Expected:
  298. ${JSON.stringify(expectedMessages)}
  299. Received:
  300. ${JSON.stringify(receivedMessages)},
  301. `;
  302. }
  303. function hot(marbles, values, error) {
  304. return new TestHotObservable(marbles.trim(), values, error);
  305. }
  306. function cold(marbles, values, error) {
  307. return new TestColdObservable(marbles.trim(), values, error);
  308. }
  309. function time(marbles) {
  310. return getTestScheduler().createTime(marbles.trim());
  311. }
  312. function addMatchers() {
  313. /**
  314. * expect.extend is an API exposed by jest-circus,
  315. * the default runner as of Jest v27. If that method
  316. * is not available, assume we're in a Jasmine test
  317. * environment.
  318. */
  319. if (!expect.extend) {
  320. jasmine.addMatchers({
  321. toHaveSubscriptions: () => ({
  322. compare: toHaveSubscriptionsComparer,
  323. }),
  324. toBeObservable: (_utils) => ({
  325. compare: toBeObservableComparer,
  326. }),
  327. });
  328. }
  329. else {
  330. expect.extend({
  331. toHaveSubscriptions: toHaveSubscriptionsComparer,
  332. toBeObservable: toBeObservableComparer,
  333. });
  334. }
  335. }
  336. function setupEnvironment() {
  337. beforeAll(() => addMatchers());
  338. beforeEach(() => initTestScheduler());
  339. afterEach(() => {
  340. getTestScheduler().flush();
  341. resetTestScheduler();
  342. });
  343. }
  344. setupEnvironment();
  345. exports.addMatchers = addMatchers;
  346. exports.cold = cold;
  347. exports.getTestScheduler = getTestScheduler;
  348. exports.hot = hot;
  349. exports.initTestScheduler = initTestScheduler;
  350. exports.resetTestScheduler = resetTestScheduler;
  351. exports.setupEnvironment = setupEnvironment;
  352. exports.time = time;
  353. Object.defineProperty(exports, '__esModule', { value: true });
  354. })));