In an RxJS marble diagram string such as '--a--b--|', what do the dashes, the letters, | and # represent?
answer
- time drawn as characters
- one dash, one frame
- letters are next values
- bar ends, hash fails
- parentheses share a frame
basics
~20 sIn RxJS marble diagrams each dash is one frame of virtual time, a letter or digit is a next value at that frame, | is completion and # is an error. In '--a--b--|', a emits at frame 2, b at 5, completion at 8.
solid answer
~50 sA marble string draws one Observable's events on a virtual time line that starts at **frame zero**, the first character. Inside `TestScheduler.run` one frame is one virtual millisecond. `-` advances time by one frame; an alphanumeric character is a `next` value emitted on that frame (and it also advances time by one frame); `|` is `complete`; `#` is `error`. So `'--a--b--|'` emits `a` on frame 2, `b` on frame 5 and completes on frame 8. Parentheses group events into the **same** frame, as in `'(ab|)'`. Letters are placeholders: a `values` object such as `{ a: 1, b: 2 }` maps them to real values, and an optional third argument supplies the error value for `#`. In run mode, spaces are ignored and only help alignment, and time progression such as `'a 100ms b'` skips ahead without typing a hundred dashes.
go deeper
Recall the alphabet: dash is a frame, letters are values, bar is complete, hash is error, parentheses group events on one frame.
Explain frame arithmetic, including why values and groups advance time, and how values maps and time progression keep diagrams readable.
Read failing marble output fluently, spot off-by-one-frame expectations, and write diagrams that line up visually for reviewers.
Judge where marble tests pay off, typically time-dependent operator chains, versus plain value assertions that newcomers find easier to maintain.
## What a marble diagram is A **marble diagram** is a picture of an Observable's notifications over time. RxJS's `TestScheduler` (from `rxjs/testing`) turns an ASCII version of that picture into a test fixture or an expectation. Time runs left to right in **frames**; the first character of the string is **frame zero**. Inside `testScheduler.run(callback)` one frame equals **one virtual millisecond**. ## The characters | Character | Meaning | Advances time? | |---|---|---| | `-` | one frame passes, nothing happens | 1 frame | | `a`, `b`, `1` (any alphanumeric) | a `next` notification on this frame | 1 frame | | `\|` | `complete` on this frame | 1 frame | | `#` | `error` on this frame | 1 frame | | `(` ... `)` | a **synchronous group**: everything inside happens on the frame of `(` | the group's full length in characters | | space | ignored in run mode, used to line diagrams up | no | | `100ms`, `2s`, `1.5m` | **time progression**: skip ahead by that amount | that many frames | | `^` | subscription point (only in `hot()` diagrams and subscription marbles) | 1 frame | ## Reading examples - `'--a--b--|'`: `a` on frame 2, `b` on frame 5, completion on frame 8. - `'--a--b--#'`: the same values, then an error on frame 8. - `'-----(a|)'`: `a` and completion together on frame 5. - `'-'` or `'------'`: never emits, never completes, like `NEVER`. - `'|'`: completes immediately with no values, like `EMPTY`. - `'#'`: errors immediately, like a stream created with `throwError`. - `'a 9ms b'`: `a` on frame 0, `b` on frame 10. The value `a` itself advanced one frame, so 9 more reach frame 10. ## Mapping letters to real values The letters are placeholders. The helpers that consume a diagram take a `values` object (or array, indexed by digit) and an optional error value: ```ts import { TestScheduler } from 'rxjs/testing'; testScheduler.run(({ cold, expectObservable }) => { const source = cold('-a-b-#', { a: { id: 1 }, b: { id: 2 } }, new Error('boom')); expectObservable(source).toBe('-a-b-#', { a: { id: 1 }, b: { id: 2 } }, new Error('boom')); }); ``` If you leave out `values`, the character itself is the value (`'a'`), and if you leave out the error, `#` carries the string `'error'`. ## The two quirks everyone trips over 1. **Values take up a frame.** After `a` is emitted, time moves on by one frame. That is why `'a 9ms b'` puts `b` on frame 10, and why time progressions in expectations are often "one less" than the delay being tested. 2. **Groups take up their full width.** `'(abc)'` emits `a`, `b` and `c` on the frame where `(` stands, then advances time by **five** frames, the number of characters including the parentheses. So `'--(abc)-|'` completes on frame 8, not frame 4. The design helps vertical alignment but is a known pain point. ## Source diagrams and expected diagrams The same alphabet is used on both sides of a test. Inputs are built with `cold()` or `hot()`, and the expected output is written for `expectObservable(actual$).toBe(...)`. Because spaces are ignored in run mode, teams indent diagrams so that frames line up vertically: ```ts const source = cold('-a--b--c---|'); const expected = ' -a-----c---|'; ``` Read top to bottom, each column is one frame, so a reviewer can see that `b` was dropped without counting. Diagrams do not have to be the same length; trailing dashes after the last event simply mean time passes with nothing happening. A diagram without `|` or `#` describes a stream that is still open when the test ends, which is how you express "never completes" in an expectation. ## Why the notation is worth learning - Timing becomes visible: `'--a--b--|'` against `'-----b--|'` shows at a glance that a value was dropped. - Tests stay short: a debounce or delay scenario that would need several real timers fits on two lines. - Failures are precise: `TestScheduler` compares frame numbers and notifications, so an off-by-one frame is reported rather than hidden by a generous timeout. ## Run mode versus the legacy form Everything above describes diagrams used **inside** `testScheduler.run()`. Outside it, a frame is 10 virtual milliseconds, spaces count as frames, and time progression syntax is not supported. New tests should always use `run()`.
- Why does '--(abc)-|' complete on frame 8 rather than frame 4?A synchronous group emits everything inside it on the frame of the opening parenthesis, but then advances time by the group's full character count, parentheses included. `(abc)` is five characters, so after two dashes and the group time is at frame 7, and the final dash puts completion on frame 8.
- How do you make a marble diagram emit objects instead of single characters?Pass a `values` map as the second argument, for example `cold('-a-b|', { a: { id: 1 }, b: { id: 2 } })`, and the same map to `toBe()` for the expectation. The letters become keys, and the comparison uses the deep-equality function the `TestScheduler` was constructed with.
saying these in an interview costs you the question
- Each dash in a marble string represents one real second.
- A letter in a marble string does not advance time.
- A group like (abc) occupies a single frame of time.
- The # character marks completion and | marks an error.
- Marble strings can only emit single-character strings as values.