Class: Tuile::FakeScreen
- Defined in:
- lib/tuile/fake_screen.rb,
sig/tuile.rbs
Overview
Testing only — a screen which doesn't paint anything, so the TTY running the tests is not painted over. It runs no event loop, so Screen#check_locked admits the thread that called Screen.fake: a spec mutating the UI from a spawned thread raises, exactly as an app would.
Intended for unit-testing individual components: instantiate a component,
mutate it, and assert against #prints or #invalidated?. It does not
run an event loop, so it is not suitable for system-testing whole apps
— for that, drive the real script through a PTY (see spec/examples/).
It also turns the stale-rect diagnostic on (StrictLayout): a rect
read before the layout has settled raises here, where in an app it would
quietly answer the previous pass's rectangle. Tuile.strict_layout = false
opts a suite out; without_strict_layout opts one read out.
Call Screen.fake to initialize the fake screen easily. Typical usage:
before { Screen.fake }
after { Screen.close }
it "paints its content" do
label = Component::Label.new.tap { |l| l.text = "hi" }
Screen.instance.content = Component::Window.new("Greeting").tap { |w| w.content = label }
Screen.instance.repaint
assert_includes Screen.instance.prints.join, "hi"
end
Constant Summary collapse
- EDITING_KEYS =
Instance Attribute Summary collapse
- #prints ⇒ ::Array[String] readonly
Attributes inherited from Screen
#background_color, #buffer, #canvas, #color_depth, #color_scheme, #event_queue, #focused, #locale, #pane, #size, #theme, #theme_def
Instance Method Summary collapse
-
#background_color=(color) ⇒ void
Plays the terminal answering the OSC 11 re-probe, so a spec can exercise app code that derives colors from Screen#background_color:.
- #clear ⇒ void
-
#click(x, y, button: :left) ⇒ void
Plays a whole click at a screen cell — the press, then the release that ends its grab:.
-
#coerce_point(point) ⇒ Point
@param
point. -
#detect_background ⇒ TerminalBackground::Result
No terminal probing in tests: skip TerminalBackground.detect (which would write an OSC 11 query to the test runner's TTY and steal its input) and pin the deterministic default.
-
#detect_color_depth ⇒ Symbol
Pins the depth rather than reading the test runner's environment, so a spec asserting flushed bytes gets the same answer on a truecolor terminal, under
TERM=dumbin CI, and inside tmux. -
#detect_locale ⇒ Locale
Pins the conventions instead of probing, so a spec asserting a painted date gets the same answer under
LC_TIME=en_DKas underLANG=C— and so no example pays for alocale(1)subprocess. -
#dispatch(event) ⇒ Object
Settles the layout on the way in as well as out.
-
#drag(*points, button: :left) ⇒ void
Plays a whole drag: the press at the first point, one move per point after it, and the release at the last.
-
#emit(str) ⇒ void
Captures the assembled repaint frame instead of writing to the test runner's TTY.
-
#initialize(width: 160, height: 50) ⇒ FakeScreen
constructor
The terminal starts at the size given, as if it had always been that big: nothing is dispatched and nothing repaints.
-
#invalidated?(component) ⇒ Boolean
@param
component— the component to check. - #invalidated_clear ⇒ void
-
#move(x, y, button: nil) ⇒ void
Moves the pointer, firing the enter/exit hooks the new position implies — or, while a press is grabbed, one Component#handle_mouse_drag.
-
#paste(text) ⇒ Boolean
Pastes
textinto the focused component, as a real terminal would with bracketed paste on:. -
#press(x, y, button: :left) ⇒ void
Half a #click, for a spec about the grab — what is claimed, what the drag does, what the release lands on.
-
#print(*args) ⇒ void
Doesn't print anything: collects all strings in #prints.
-
#release(x, y) ⇒ void
The other half of #press.
-
#resize_terminal(width, height) ⇒ void
The terminal reporting a new size, routed as its own report would be: the pane takes the whole screen, and every popup is placed again.
-
#scroll(direction, x, y) ⇒ void
One wheel notch over a cell.
Methods inherited from Screen
#add_popup, #beep, #canvas_for, #check_locked, #clip_for, #clipped?, close, #close, #content, #content=, #cursor_position, #cursor_sequence, #cycle_focus, #event_loop, fake, #fire_focus_hooks, #flush_layout, #focus_next, #focus_previous, #follow_up_focus, #grabbed, #handle_background_color, #handle_color_scheme, #handle_key?, #handle_mouse, #handle_paste, #has_popup?, #hovered, instance, instance?, #invalidate, #invalidate_layout, listener, #needs_full_repaint, #on_error, #on_focus_changed, #popups, #register_global_shortcut, #remove_popup, #repaint, #resize, #restyle, #run_event_loop, #settle, #size_pane, #state, #unregister_global_shortcut, #warn_if_unseen
Methods included from Listeners::Declare
Constructor Details
#initialize(width: 160, height: 50) ⇒ FakeScreen
The terminal starts at the size given, as if it had always been that big: nothing is dispatched and nothing repaints. To exercise a change of size, the SIGWINCH path, call #resize_terminal instead.
@param width — the terminal's columns.
@param height — the terminal's rows.
36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/tuile/fake_screen.rb', line 36 def initialize(width: 160, height: 50) super() # `Tuile.strict_layout` defaults to `:raise` under this screen; this puts # the check it answers through into `Component#rect`. StrictLayout.install @event_queue = FakeEventQueue.new @size = Size.new(width, height) # super sized both to the test runner's TTY. @buffer.resize(@size) size_pane @prints = [] end |
Instance Attribute Details
#prints ⇒ ::Array[String] (readonly)
@return — whatever #print / #emit produced so far.
Component painting lands in Screen#buffer, not here — assert on
Buffer#row_text / Buffer#row_ansi / Buffer#cell for content, and
on prints for cursor and housekeeping escapes.
53 54 55 |
# File 'lib/tuile/fake_screen.rb', line 53 def prints @prints end |
Instance Method Details
#background_color=(color) ⇒ void
This method returns an undefined value.
Plays the terminal answering the OSC 11 re-probe, so a spec can exercise app code that derives colors from Screen#background_color:
Screen.instance.background_color = Color.rgb(30, 30, 46)
Takes the same path a real reply does — a changed color fires Component#handle_theme_changed across the tree and invalidates it. There is no such writer on Screen: the value is a report from the terminal, not a setting.
@param color
110 111 112 |
# File 'lib/tuile/fake_screen.rb', line 110 def background_color=(color) handle_background_color(color) end |
#clear ⇒ void
This method returns an undefined value.
56 57 58 |
# File 'lib/tuile/fake_screen.rb', line 56 def clear @prints.clear end |
#click(x, y, button: :left) ⇒ void
This method returns an undefined value.
Plays a whole click at a screen cell — the press, then the release that ends its grab:
screen.click(save_button.absolute_rect.left, save_button.absolute_rect.top)
Routed exactly as the terminal's own report would be (Mouse::Router), so it focuses, dismisses popups and bubbles.
@param x — 0-based column.
@param y — 0-based row.
@param button — :left, :middle or :right.
125 126 127 128 |
# File 'lib/tuile/fake_screen.rb', line 125 def click(x, y, button: :left) press(x, y, button: ) release(x, y) end |
#coerce_point(point) ⇒ Point
@param point
205 206 207 208 209 210 211 212 213 214 |
# File 'lib/tuile/fake_screen.rb', line 205 def coerce_point(point) case point when Point then point when Array raise ArgumentError, "expected [x, y], got #{point.inspect}" unless point.size == 2 Point.new(point[0], point[1]) else raise ArgumentError, "expected a Point or [x, y], got #{point.inspect}" end end |
#detect_background ⇒ TerminalBackground::Result
No terminal probing in tests: skip TerminalBackground.detect (which would write an OSC 11 query to the test runner's TTY and steal its input) and pin the deterministic default. The color is nil — the case every app must handle anyway — until a spec assigns one through #background_color=.
222 |
# File 'lib/tuile/fake_screen.rb', line 222 def detect_background = TerminalBackground::Result.new(scheme: :dark, color: nil) |
#detect_color_depth ⇒ Symbol
Pins the depth rather than reading the test runner's environment, so a
spec asserting flushed bytes gets the same answer on a truecolor
terminal, under TERM=dumb in CI, and inside tmux. A spec exercising
degradation builds its own Buffer with the depth it wants.
229 |
# File 'lib/tuile/fake_screen.rb', line 229 def detect_color_depth = :truecolor |
#detect_locale ⇒ Locale
Pins the conventions instead of probing, so a spec asserting a painted
date gets the same answer under LC_TIME=en_DK as under LANG=C — and
so no example pays for a locale(1) subprocess. A spec exercising
detection assigns Screen#locale= or calls Locale.from_keywords with
canned answers.
237 |
# File 'lib/tuile/fake_screen.rb', line 237 def detect_locale = Locale::ISO |
#dispatch(event) ⇒ Object
Settles the layout on the way in as well as out. A spec mutates between
gestures, where the loop would have settled at the end of the previous
dispatch and there is none — and routing a press reads rects, so without
this a click hit-tests what the last mutation left half-finished.
@param event — see Screen#dispatch.
@return — whatever the handler returned.
198 199 200 201 |
# File 'lib/tuile/fake_screen.rb', line 198 def dispatch(event) flush_layout super end |
#drag(*points, button: :left) ⇒ void
This method returns an undefined value.
Plays a whole drag: the press at the first point, one move per point after it, and the release at the last.
screen.drag([2, 1], [3, 2], [4, 3]) # three reports, a diagonal
screen.drag(canvas.rect.top_left, [9, 9]) # the coarsest drag there is
Reports only the points given, as the wire does: at ~84 reports a
second (R_mouse_reporting) a quick drag genuinely skips cells, so
interpolating them would let a spec assert a continuity no terminal
delivers. A one-point drag is a #click — use that.
@param points — two or more positions, as Points or [x, y] pairs.
@param button — :left, :middle or :right.
181 182 183 184 185 186 187 188 |
# File 'lib/tuile/fake_screen.rb', line 181 def drag(*points, button: :left) raise ArgumentError, "a drag needs at least two points, got #{points.size}" if points.size < 2 path = points.map { coerce_point(_1) } press(path.first.x, path.first.y, button: ) path.drop(1).each { move(_1.x, _1.y, button: ) } release(path.last.x, path.last.y) end |
#emit(str) ⇒ void
This method returns an undefined value.
Captures the assembled repaint frame instead of writing to the test runner's TTY. Lands in #prints so cursor/sync escapes can be asserted; painted content is read from Screen#buffer.
@param str
72 73 74 |
# File 'lib/tuile/fake_screen.rb', line 72 def emit(str) @prints << str end |
#invalidated?(component) ⇒ Boolean
@param component — the component to check.
92 |
# File 'lib/tuile/fake_screen.rb', line 92 def invalidated?(component) = @invalidated.include?(component) |
#invalidated_clear ⇒ void
This method returns an undefined value.
95 96 97 |
# File 'lib/tuile/fake_screen.rb', line 95 def invalidated_clear @invalidated.clear end |
#move(x, y, button: nil) ⇒ void
This method returns an undefined value.
Moves the pointer, firing the enter/exit hooks the new position implies — or, while a press is grabbed, one Component#handle_mouse_drag.
@param x — 0-based column.
@param y — 0-based row.
@param button — the button held while moving, if any.
164 |
# File 'lib/tuile/fake_screen.rb', line 164 def move(x, y, button: nil) = dispatch(Mouse::MoveEvent.new(, x, y)) |
#paste(text) ⇒ Boolean
Pastes text into the focused component, as a real terminal would with
bracketed paste on:
area.focus
Screen.instance.paste("one\r\ntwo")
area.text # => "one\ntwo" — one mutation, no ENTER anywhere
Goes through Keys.normalize_paste first, so a spec can hand it the
CR-flavored line endings terminals actually deliver and still assert
against \n.
@param text
@return — true if some component consumed it.
88 |
# File 'lib/tuile/fake_screen.rb', line 88 def paste(text) = dispatch(EventQueue::PasteEvent.new(Keys.normalize_paste(text))) |
#press(x, y, button: :left) ⇒ void
This method returns an undefined value.
Half a #click, for a spec about the grab — what is claimed, what the drag does, what the release lands on.
@param x — 0-based column.
@param y — 0-based row.
@param button — :left, :middle or :right.
143 |
# File 'lib/tuile/fake_screen.rb', line 143 def press(x, y, button: :left) = dispatch(Mouse::DownEvent.new(, x, y)) |
#print(*args) ⇒ void
This method returns an undefined value.
Doesn't print anything: collects all strings in #prints.
@param args
63 64 65 |
# File 'lib/tuile/fake_screen.rb', line 63 def print(*args) @prints += args end |
#release(x, y) ⇒ void
This method returns an undefined value.
The other half of #press.
@param x — 0-based column.
@param y — 0-based row.
149 |
# File 'lib/tuile/fake_screen.rb', line 149 def release(x, y) = dispatch(Mouse::UpEvent.new(x, y)) |
#resize_terminal(width, height) ⇒ void
This method returns an undefined value.
The terminal reporting a new size, routed as its own report would be: the pane takes the whole screen, and every popup is placed again.
@param width
@param height
135 |
# File 'lib/tuile/fake_screen.rb', line 135 def resize_terminal(width, height) = dispatch(EventQueue::TTYSizeEvent.new(width, height)) |
#scroll(direction, x, y) ⇒ void
This method returns an undefined value.
One wheel notch over a cell.
@param direction — :up, :down, :left or :right.
@param x — 0-based column.
@param y — 0-based row.
156 |
# File 'lib/tuile/fake_screen.rb', line 156 def scroll(direction, x, y) = dispatch(Mouse::ScrollEvent.new(direction, x, y)) |