scriptcast 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tests/test_cli.py ADDED
@@ -0,0 +1,304 @@
1
+ # tests/test_cli.py
2
+ import json
3
+ import shutil
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+ from click.testing import CliRunner
9
+
10
+ from scriptcast.__main__ import cli
11
+
12
+ requires_agg = pytest.mark.skipif(shutil.which("agg") is None, reason="agg not installed")
13
+
14
+ # ── helpers ────────────────────────────────────────────────────────────────
15
+
16
+ def _minimal_cast(tmp_path: Path, name: str = "demo") -> Path:
17
+ cast = tmp_path / f"{name}.cast"
18
+ cast.write_text(json.dumps({"version": 2, "width": 80, "height": 24}) + "\n")
19
+ return cast
20
+
21
+
22
+ def _minimal_sc(tmp_path: Path, name: str = "demo") -> Path:
23
+ sc = tmp_path / f"{name}.sc"
24
+ sc.write_text(
25
+ json.dumps({"version": 1, "width": 80, "height": 24, "directive-prefix": "SC"}) + "\n"
26
+ )
27
+ return sc
28
+
29
+
30
+ def _sh(tmp_path: Path, content: str = ": SC scene demo\necho hello\n") -> Path:
31
+ script = tmp_path / "demo.sh"
32
+ script.write_text(content)
33
+ return script
34
+
35
+
36
+ # ── no-args / help ─────────────────────────────────────────────────────────
37
+
38
+ def test_no_args_prints_help():
39
+ runner = CliRunner()
40
+ result = runner.invoke(cli, [])
41
+ assert result.exit_code == 0
42
+ assert "Usage" in result.output
43
+
44
+
45
+ # ── error cases ────────────────────────────────────────────────────────────
46
+
47
+ def test_nonexistent_file_errors(tmp_path):
48
+ runner = CliRunner()
49
+ result = runner.invoke(cli, [str(tmp_path / "nope.sh")])
50
+ assert result.exit_code != 0
51
+
52
+
53
+ def test_unsupported_extension_errors(tmp_path):
54
+ f = tmp_path / "demo.txt"
55
+ f.write_text("hello\n")
56
+ runner = CliRunner()
57
+ result = runner.invoke(cli, [str(f)])
58
+ assert result.exit_code != 0
59
+ assert "Unsupported" in result.output
60
+
61
+
62
+ def test_extra_args_after_input_errors(tmp_path):
63
+ script = _sh(tmp_path)
64
+ runner = CliRunner()
65
+ result = runner.invoke(cli, [str(script), "extra"])
66
+ assert result.exit_code != 0
67
+ assert "Unexpected" in result.output
68
+
69
+
70
+ def test_cast_input_with_no_export_errors(tmp_path):
71
+ cast = _minimal_cast(tmp_path)
72
+ runner = CliRunner()
73
+ result = runner.invoke(cli, ["--no-export", str(cast)])
74
+ assert result.exit_code != 0
75
+
76
+
77
+ # ── .sh input (full pipeline) ──────────────────────────────────────────────
78
+
79
+ @requires_agg
80
+ def test_sh_input_end_to_end(tmp_path):
81
+ """scriptcast demo.sh records, generates, exports."""
82
+ script = _sh(tmp_path)
83
+ runner = CliRunner()
84
+ result = runner.invoke(cli, ["--output-dir", str(tmp_path), str(script)])
85
+ assert result.exit_code == 0, result.output
86
+ assert (tmp_path / "demo.cast").exists()
87
+
88
+
89
+ def test_sh_input_no_export_produces_cast(tmp_path):
90
+ """scriptcast demo.sh --no-export produces .cast but no export."""
91
+ script = _sh(tmp_path)
92
+ runner = CliRunner()
93
+ result = runner.invoke(cli, ["--output-dir", str(tmp_path), "--no-export", str(script)])
94
+ assert result.exit_code == 0, result.output
95
+ assert (tmp_path / "demo.cast").exists()
96
+
97
+
98
+ @requires_agg
99
+ def test_sh_input_split_scenes(tmp_path):
100
+ script = _sh(tmp_path, content=(
101
+ ": SC scene alpha\necho a\n"
102
+ ": SC scene beta\necho b\n"
103
+ ))
104
+ runner = CliRunner()
105
+ result = runner.invoke(
106
+ cli, ["--output-dir", str(tmp_path), "--split-scenes", str(script)]
107
+ )
108
+ assert result.exit_code == 0, result.output
109
+ assert (tmp_path / "alpha.cast").exists()
110
+ assert (tmp_path / "beta.cast").exists()
111
+
112
+
113
+ @requires_agg
114
+ def test_sh_input_single_cast_default(tmp_path):
115
+ script = _sh(tmp_path, content=(
116
+ ": SC scene alpha\necho a\n"
117
+ ": SC scene beta\necho b\n"
118
+ ))
119
+ runner = CliRunner()
120
+ result = runner.invoke(cli, ["--output-dir", str(tmp_path), str(script)])
121
+ assert result.exit_code == 0, result.output
122
+ assert (tmp_path / "demo.cast").exists()
123
+ assert not (tmp_path / "alpha.cast").exists()
124
+
125
+
126
+ def test_sh_input_directive_prefix_stored_in_sc(tmp_path):
127
+ script = _sh(tmp_path, content="echo hello\n")
128
+ runner = CliRunner()
129
+ result = runner.invoke(
130
+ cli,
131
+ ["--directive-prefix", "DEMO", "--output-dir", str(tmp_path), "--no-export", str(script)],
132
+ )
133
+ assert result.exit_code == 0, result.output
134
+ header = json.loads((tmp_path / "demo.sc").read_text().splitlines()[0])
135
+ assert header["directive-prefix"] == "DEMO"
136
+
137
+
138
+ # ── .sc input ──────────────────────────────────────────────────────────────
139
+
140
+ def test_sc_input_generates_and_exports(tmp_path):
141
+ sc = _minimal_sc(tmp_path)
142
+ cast = tmp_path / "demo.cast"
143
+ runner = CliRunner()
144
+ with patch("scriptcast.__main__.generate_export", return_value=cast) as mock_exp, \
145
+ patch("scriptcast.__main__.apply_scriptcast_watermark"):
146
+ result = runner.invoke(cli, ["--output-dir", str(tmp_path), str(sc)])
147
+ assert result.exit_code == 0, result.output
148
+ mock_exp.assert_called_once()
149
+
150
+
151
+ def test_sc_input_no_export_produces_cast(tmp_path):
152
+ sc = _minimal_sc(tmp_path)
153
+ runner = CliRunner()
154
+ result = runner.invoke(cli, ["--output-dir", str(tmp_path), "--no-export", str(sc)])
155
+ assert result.exit_code == 0, result.output
156
+ assert (tmp_path / "demo.cast").exists()
157
+
158
+
159
+ # ── .cast input ────────────────────────────────────────────────────────────
160
+
161
+ def test_cast_input_exports(tmp_path):
162
+ cast = _minimal_cast(tmp_path)
163
+ runner = CliRunner()
164
+ with patch("scriptcast.__main__.generate_export", return_value=cast), \
165
+ patch("scriptcast.__main__.apply_scriptcast_watermark"):
166
+ result = runner.invoke(cli, ["--output-dir", str(tmp_path), str(cast)])
167
+ assert result.exit_code == 0, result.output
168
+
169
+
170
+ # ── format flag ────────────────────────────────────────────────────────────
171
+
172
+ def test_format_png_accepted(tmp_path):
173
+ cast = _minimal_cast(tmp_path)
174
+ runner = CliRunner()
175
+ with patch("scriptcast.__main__.generate_export", return_value=cast), \
176
+ patch("scriptcast.__main__.apply_scriptcast_watermark"):
177
+ result = runner.invoke(cli, ["--format", "png", str(cast)])
178
+ assert result.exit_code == 0, result.output
179
+
180
+
181
+ def test_format_gif_accepted(tmp_path):
182
+ cast = _minimal_cast(tmp_path)
183
+ runner = CliRunner()
184
+ with patch("scriptcast.__main__.generate_export", return_value=cast), \
185
+ patch("scriptcast.__main__.apply_scriptcast_watermark"):
186
+ result = runner.invoke(cli, ["--format", "gif", str(cast)])
187
+ assert result.exit_code == 0, result.output
188
+
189
+
190
+ def test_format_apng_rejected(tmp_path):
191
+ cast = _minimal_cast(tmp_path)
192
+ runner = CliRunner()
193
+ result = runner.invoke(cli, ["--format", "apng", str(cast)])
194
+ assert result.exit_code != 0
195
+
196
+
197
+ # ── install subcommand ─────────────────────────────────────────────────────
198
+
199
+ def test_install_command_exists():
200
+ assert "install" in cli.commands
201
+
202
+
203
+ def test_install_command_has_prefix_option():
204
+ param_names = [p.name for p in cli.commands["install"].params]
205
+ assert "prefix" in param_names
206
+
207
+
208
+ def test_install_downloads_agg_and_fonts(tmp_path):
209
+ import io
210
+ import zipfile
211
+ from unittest.mock import MagicMock
212
+
213
+ buf = io.BytesIO()
214
+ with zipfile.ZipFile(buf, "w") as zf:
215
+ zf.writestr("fonts/ttf/JetBrainsMono-Regular.ttf", b"fake-ttf-data")
216
+ zip_bytes = buf.getvalue()
217
+
218
+ def fake_urlretrieve(url, dest):
219
+ dest = Path(dest)
220
+ if "agg" in url:
221
+ dest.write_bytes(b"fake-agg-binary")
222
+ else:
223
+ dest.write_bytes(zip_bytes)
224
+
225
+ fake_release = json.dumps({"assets": [
226
+ {"name": "JetBrainsMono-2.304.zip",
227
+ "browser_download_url": "https://example.com/JetBrainsMono-2.304.zip"},
228
+ ]}).encode()
229
+ mock_resp = MagicMock()
230
+ mock_resp.read.return_value = fake_release
231
+ mock_resp.__enter__ = lambda s: s
232
+ mock_resp.__exit__ = MagicMock(return_value=False)
233
+
234
+ runner = CliRunner()
235
+ with patch("urllib.request.urlretrieve", side_effect=fake_urlretrieve), \
236
+ patch("urllib.request.urlopen", return_value=mock_resp):
237
+ result = runner.invoke(cli, ["install", "--prefix", str(tmp_path)])
238
+
239
+ assert result.exit_code == 0, result.output
240
+ assert (tmp_path / ".agg-real").exists()
241
+ assert (tmp_path / "agg").exists()
242
+ assert (tmp_path / "fonts" / "JetBrainsMono-Regular.ttf").exists()
243
+
244
+
245
+ # ── removed subcommands ────────────────────────────────────────────────────
246
+
247
+ def test_record_subcommand_removed():
248
+ assert "record" not in cli.commands
249
+
250
+
251
+ def test_generate_subcommand_removed():
252
+ assert "generate" not in cli.commands
253
+
254
+
255
+ def test_export_subcommand_removed():
256
+ assert "export" not in cli.commands
257
+
258
+
259
+ # ── --xtrace-log flag ──────────────────────────────────────────────────────
260
+
261
+ def test_xtrace_log_creates_xtrace_file(tmp_path):
262
+ script = _sh(tmp_path)
263
+ runner = CliRunner()
264
+ result = runner.invoke(
265
+ cli, ["--output-dir", str(tmp_path), "--no-export", "--xtrace-log", str(script)]
266
+ )
267
+ assert result.exit_code == 0, result.output
268
+ assert (tmp_path / "demo.xtrace").exists()
269
+
270
+
271
+ def test_xtrace_log_with_sc_input_errors(tmp_path):
272
+ sc = _minimal_sc(tmp_path)
273
+ runner = CliRunner()
274
+ result = runner.invoke(cli, ["--xtrace-log", "--no-export", str(sc)])
275
+ assert result.exit_code != 0
276
+ assert "xtrace-log" in result.output.lower() or "xtrace" in result.output.lower()
277
+
278
+
279
+ def test_xtrace_log_with_cast_input_errors(tmp_path):
280
+ cast = _minimal_cast(tmp_path)
281
+ runner = CliRunner()
282
+ with patch("scriptcast.__main__.generate_export", return_value=cast), \
283
+ patch("scriptcast.__main__.apply_scriptcast_watermark"):
284
+ result = runner.invoke(cli, ["--xtrace-log", str(cast)])
285
+ assert result.exit_code != 0
286
+ assert "xtrace" in result.output.lower()
287
+
288
+
289
+ def test_export_on_frame_callback_is_passed(tmp_path):
290
+ """generate_export is called with a callable on_frame argument."""
291
+ cast = _minimal_cast(tmp_path)
292
+ runner = CliRunner()
293
+ received = {}
294
+
295
+ def fake_generate_export(cast_path, frame_config, format, on_frame=None):
296
+ received["on_frame"] = on_frame
297
+ return cast
298
+
299
+ with patch("scriptcast.__main__.generate_export", side_effect=fake_generate_export), \
300
+ patch("scriptcast.__main__.apply_scriptcast_watermark"):
301
+ result = runner.invoke(cli, [str(cast)])
302
+
303
+ assert result.exit_code == 0, result.output
304
+ assert callable(received.get("on_frame"))
tests/test_config.py ADDED
@@ -0,0 +1,400 @@
1
+ # tests/test_config.py
2
+ from scriptcast.config import ScriptcastConfig
3
+
4
+
5
+ def test_defaults():
6
+ c = ScriptcastConfig()
7
+ assert c.type_speed == 40
8
+ assert c.cmd_wait == 80
9
+ assert c.input_wait == 80
10
+ assert c.exit_wait == 120
11
+ assert c.enter_wait == 80
12
+ assert c.width == 100
13
+ assert c.height == 28
14
+ assert c.terminal_theme == "dark" # was c.theme
15
+ assert c.prompt == "$ "
16
+ assert c.directive_prefix == "SC"
17
+ assert c.trace_prefix == "+"
18
+ assert c.split_scenes is False
19
+
20
+ def test_apply_set_int():
21
+ c = ScriptcastConfig()
22
+ c.apply("set", ["type_speed", "60"])
23
+ assert c.type_speed == 60
24
+
25
+ def test_apply_set_str():
26
+ c = ScriptcastConfig()
27
+ c.apply("set", ["terminal-theme", "light"]) # dash form; was "theme"
28
+ assert c.terminal_theme == "light" # was c.theme
29
+
30
+ def test_apply_old_theme_key_ignored():
31
+ c = ScriptcastConfig()
32
+ c.apply("set", ["theme", "light"]) # old key — no longer valid
33
+ assert c.terminal_theme == "dark" # unchanged
34
+
35
+ def test_apply_unknown_key_ignored():
36
+ c = ScriptcastConfig()
37
+ c.apply("set", ["nonexistent", "value"]) # must not raise
38
+
39
+ def test_apply_non_set_directive_ignored():
40
+ c = ScriptcastConfig()
41
+ c.apply("scene", ["Intro"]) # must not raise
42
+
43
+ def test_copy_is_independent():
44
+ c = ScriptcastConfig()
45
+ c2 = c.copy()
46
+ c2.type_speed = 999
47
+ assert c.type_speed == 40
48
+
49
+ def test_enter_wait_default():
50
+ assert ScriptcastConfig().enter_wait == 80
51
+
52
+ def test_input_wait_default_is_cmd_wait():
53
+ assert ScriptcastConfig().input_wait == 80
54
+
55
+ def test_split_scenes_default():
56
+ assert ScriptcastConfig().split_scenes is False
57
+
58
+ def test_apply_enter_wait():
59
+ c = ScriptcastConfig()
60
+ c.apply("set", ["enter_wait", "200"])
61
+ assert c.enter_wait == 200
62
+
63
+ def test_apply_split_scenes_true():
64
+ c = ScriptcastConfig()
65
+ c.apply("set", ["split_scenes", "true"])
66
+ assert c.split_scenes is True
67
+
68
+ def test_apply_split_scenes_false():
69
+ c = ScriptcastConfig()
70
+ c.split_scenes = True
71
+ c.apply("set", ["split_scenes", "false"])
72
+ assert c.split_scenes is False
73
+
74
+ def test_word_speed_default_is_none():
75
+ assert ScriptcastConfig().word_speed is None
76
+
77
+ def test_apply_word_speed():
78
+ c = ScriptcastConfig()
79
+ c.apply("set", ["word_speed", "80"])
80
+ assert c.word_speed == 80
81
+
82
+
83
+ def test_theme_config_defaults():
84
+ from scriptcast.config import ThemeConfig
85
+ c = ThemeConfig()
86
+ assert c.frame_bar_title == "Terminal"
87
+ # Individual padding sides
88
+ assert c.padding_top == 14
89
+ assert c.padding_right == 14
90
+ assert c.padding_bottom == 14
91
+ assert c.padding_left == 14
92
+ assert c.radius == 12
93
+ assert c.border_color == "ffffff30"
94
+ assert c.border_width == 1
95
+ assert c.background == "1e1b4b,0d3b66"
96
+ # Individual margin sides
97
+ assert c.margin_top is None
98
+ assert c.margin_right is None
99
+ assert c.margin_bottom is None
100
+ assert c.margin_left is None
101
+ assert c.shadow is True
102
+ assert c.shadow_color == "0000004d"
103
+ assert c.shadow_radius == 20
104
+ assert c.shadow_offset_y == 21
105
+ assert c.watermark is None
106
+ assert c.watermark_color == "ffffff"
107
+ assert c.watermark_size is None
108
+ assert c.scriptcast_watermark is True
109
+ assert c.frame is True
110
+
111
+
112
+ def test_theme_config_custom():
113
+ from scriptcast.config import ThemeConfig
114
+ c = ThemeConfig(frame_bar_title="Demo", background="#1a1a2e,#16213e", watermark="hello")
115
+ assert c.frame_bar_title == "Demo"
116
+ assert c.background == "#1a1a2e,#16213e"
117
+ assert c.watermark == "hello"
118
+
119
+
120
+ def test_theme_config_custom_padding():
121
+ from scriptcast.config import ThemeConfig
122
+ c = ThemeConfig(padding_top=10, padding_right=20, padding_bottom=30, padding_left=20)
123
+ assert c.padding_top == 10
124
+ assert c.padding_bottom == 30
125
+
126
+
127
+ def test_theme_config_custom_margin():
128
+ from scriptcast.config import ThemeConfig
129
+ c = ThemeConfig(margin_top=40, margin_bottom=80)
130
+ assert c.margin_top == 40
131
+ assert c.margin_bottom == 80
132
+ assert c.margin_left is None
133
+
134
+
135
+ def test_theme_config_frame_default_true():
136
+ from scriptcast.config import ThemeConfig
137
+ assert ThemeConfig().frame is True
138
+
139
+ def test_theme_config_frame_true():
140
+ from scriptcast.config import ThemeConfig
141
+ assert ThemeConfig(frame=True).frame is True
142
+
143
+
144
+ def test_theme_config_scriptcast_watermark_default():
145
+ from scriptcast.config import ThemeConfig
146
+ assert ThemeConfig().scriptcast_watermark is True
147
+
148
+ def test_theme_config_scriptcast_watermark_disabled():
149
+ from scriptcast.config import ThemeConfig
150
+ assert ThemeConfig(scriptcast_watermark=False).scriptcast_watermark is False
151
+
152
+
153
+ def test_theme_config_apply_string():
154
+ from scriptcast.config import ThemeConfig
155
+ tc = ThemeConfig()
156
+ tc.apply("background", "ff0000,0000ff")
157
+ assert tc.background == "ff0000,0000ff"
158
+
159
+ def test_theme_config_apply_int():
160
+ from scriptcast.config import ThemeConfig
161
+ tc = ThemeConfig()
162
+ tc.apply("radius", "20")
163
+ assert tc.radius == 20
164
+
165
+ def test_theme_config_apply_bool_true():
166
+ from scriptcast.config import ThemeConfig
167
+ tc = ThemeConfig(shadow=False)
168
+ tc.apply("shadow", "true")
169
+ assert tc.shadow is True
170
+
171
+ def test_theme_config_apply_bool_false():
172
+ from scriptcast.config import ThemeConfig
173
+ tc = ThemeConfig(frame=True)
174
+ tc.apply("frame", "false")
175
+ assert tc.frame is False
176
+
177
+ def test_theme_config_apply_bool_scriptcast_watermark():
178
+ from scriptcast.config import ThemeConfig
179
+ tc = ThemeConfig(scriptcast_watermark=True)
180
+ tc.apply("scriptcast-watermark", "false")
181
+ assert tc.scriptcast_watermark is False
182
+
183
+ def test_theme_config_apply_margin_shorthand_one():
184
+ from scriptcast.config import ThemeConfig
185
+ tc = ThemeConfig()
186
+ tc.apply("margin", "60")
187
+ assert (tc.margin_top, tc.margin_right, tc.margin_bottom, tc.margin_left) == (60, 60, 60, 60)
188
+
189
+ def test_theme_config_apply_margin_shorthand_three():
190
+ from scriptcast.config import ThemeConfig
191
+ tc = ThemeConfig()
192
+ tc.apply("margin", "32 32 64")
193
+ assert tc.margin_top == 32
194
+ assert tc.margin_right == 32
195
+ assert tc.margin_bottom == 64
196
+ assert tc.margin_left == 32
197
+
198
+ def test_theme_config_apply_padding_shorthand():
199
+ from scriptcast.config import ThemeConfig
200
+ tc = ThemeConfig()
201
+ tc.apply("padding", "10 20")
202
+ assert (tc.padding_top, tc.padding_bottom) == (10, 10)
203
+ assert (tc.padding_left, tc.padding_right) == (20, 20)
204
+
205
+ def test_theme_config_apply_margin_individual():
206
+ from scriptcast.config import ThemeConfig
207
+ tc = ThemeConfig()
208
+ tc.apply("margin-bottom", "120")
209
+ assert tc.margin_bottom == 120
210
+ assert tc.margin_top is None # unchanged
211
+
212
+ def test_theme_config_apply_frame_bar_bool():
213
+ from scriptcast.config import ThemeConfig
214
+ tc = ThemeConfig(frame_bar=True)
215
+ tc.apply("frame-bar", "false")
216
+ assert tc.frame_bar is False
217
+
218
+ def test_theme_config_apply_frame_bar_title():
219
+ from scriptcast.config import ThemeConfig
220
+ tc = ThemeConfig()
221
+ tc.apply("frame-bar-title", "My Terminal")
222
+ assert tc.frame_bar_title == "My Terminal"
223
+
224
+ def test_theme_config_apply_nullable_none():
225
+ from scriptcast.config import ThemeConfig
226
+ tc = ThemeConfig(background="ff0000")
227
+ tc.apply("background", "none")
228
+ assert tc.background is None
229
+
230
+ def test_theme_config_apply_unknown_key_ignored():
231
+ from scriptcast.config import ThemeConfig
232
+ tc = ThemeConfig()
233
+ tc.apply("nonexistent-key", "value") # must not raise
234
+
235
+
236
+ def test_scriptcast_config_has_theme():
237
+ from scriptcast.config import ScriptcastConfig, ThemeConfig
238
+ sc = ScriptcastConfig()
239
+ assert isinstance(sc.theme, ThemeConfig)
240
+
241
+ def test_scriptcast_config_theme_defaults():
242
+ from scriptcast.config import ScriptcastConfig
243
+ sc = ScriptcastConfig()
244
+ assert sc.theme.frame is True
245
+ assert sc.theme.background == "1e1b4b,0d3b66"
246
+
247
+ def test_scriptcast_config_apply_theme_key():
248
+ from scriptcast.config import ScriptcastConfig
249
+ sc = ScriptcastConfig()
250
+ sc.apply("set", ["theme-radius", "20"])
251
+ assert sc.theme.radius == 20
252
+
253
+ def test_scriptcast_config_apply_theme_background():
254
+ from scriptcast.config import ScriptcastConfig
255
+ sc = ScriptcastConfig()
256
+ sc.apply("set", ["theme-background", "ff0000,0000ff"])
257
+ assert sc.theme.background == "ff0000,0000ff"
258
+
259
+ def test_scriptcast_config_apply_theme_bool():
260
+ from scriptcast.config import ScriptcastConfig
261
+ sc = ScriptcastConfig()
262
+ sc.apply("set", ["theme-frame", "false"])
263
+ assert sc.theme.frame is False
264
+
265
+ def test_scriptcast_config_apply_theme_margin():
266
+ from scriptcast.config import ScriptcastConfig
267
+ sc = ScriptcastConfig()
268
+ sc.apply("set", ["theme-margin", "32 32 64"])
269
+ assert sc.theme.margin_bottom == 64
270
+
271
+ def test_scriptcast_config_apply_terminal_theme_not_delegated():
272
+ """terminal-theme must NOT be routed to ThemeConfig; stays on ScriptcastConfig."""
273
+ from scriptcast.config import ScriptcastConfig
274
+ sc = ScriptcastConfig()
275
+ sc.apply("set", ["terminal-theme", "light"])
276
+ assert sc.terminal_theme == "light"
277
+
278
+ def test_copy_deep_copies_theme():
279
+ from scriptcast.config import ScriptcastConfig
280
+ sc = ScriptcastConfig()
281
+ sc2 = sc.copy()
282
+ sc2.theme.radius = 999
283
+ assert sc.theme.radius == 12 # unchanged
284
+
285
+ def test_copy_is_still_independent_for_scalar():
286
+ from scriptcast.config import ScriptcastConfig
287
+ sc = ScriptcastConfig()
288
+ sc2 = sc.copy()
289
+ sc2.type_speed = 999
290
+ assert sc.type_speed == 40
291
+
292
+ def test_cr_delay_default():
293
+ c = ScriptcastConfig()
294
+ assert c.cr_delay == 0
295
+
296
+ def test_apply_set_cr_delay():
297
+ c = ScriptcastConfig()
298
+ c.apply("set", ["cr-delay", "80"])
299
+ assert c.cr_delay == 80
300
+
301
+
302
+ # ── extract_config_prefix ─────────────────────────────────────────────────
303
+
304
+ def test_extract_config_prefix_empty():
305
+ from scriptcast.config import extract_config_prefix
306
+ assert extract_config_prefix("") == ""
307
+
308
+
309
+ def test_extract_config_prefix_blank_and_comments():
310
+ from scriptcast.config import extract_config_prefix
311
+ src = "# comment\n\n# another\n"
312
+ assert extract_config_prefix(src) == src
313
+
314
+
315
+ def test_extract_config_prefix_var_assignment():
316
+ from scriptcast.config import extract_config_prefix
317
+ src = "GREEN='\\033[32m'\nRESET='\\033[0m'\n"
318
+ assert extract_config_prefix(src) == src
319
+
320
+
321
+ def test_extract_config_prefix_sc_set():
322
+ from scriptcast.config import extract_config_prefix
323
+ src = ": SC set width 80\n: SC set height 24\n"
324
+ assert extract_config_prefix(src) == src
325
+
326
+
327
+ def test_extract_config_prefix_stops_at_scene():
328
+ from scriptcast.config import extract_config_prefix
329
+ src = ": SC set width 80\n: SC scene main\necho hi\n"
330
+ assert extract_config_prefix(src) == ": SC set width 80\n"
331
+
332
+
333
+ def test_extract_config_prefix_stops_at_real_command():
334
+ from scriptcast.config import extract_config_prefix
335
+ src = ": SC set width 80\necho hello\n"
336
+ assert extract_config_prefix(src) == ": SC set width 80\n"
337
+
338
+
339
+ def test_extract_config_prefix_stops_at_filter():
340
+ from scriptcast.config import extract_config_prefix
341
+ src = ": SC set width 80\n: SC filter foo\necho hi\n"
342
+ assert extract_config_prefix(src) == ": SC set width 80\n"
343
+
344
+
345
+ def test_extract_config_prefix_stops_at_type():
346
+ from scriptcast.config import extract_config_prefix
347
+ src = ": SC set width 80\n: SC type hello\n"
348
+ assert extract_config_prefix(src) == ": SC set width 80\n"
349
+
350
+
351
+ def test_extract_config_prefix_record_pause_block_collected():
352
+ from scriptcast.config import extract_config_prefix
353
+ src = (
354
+ ": SC record pause\n"
355
+ "GREEN='\\033[32m'\n"
356
+ ": SC record resume\n"
357
+ ": SC set prompt \"${GREEN} > \"\n"
358
+ )
359
+ assert extract_config_prefix(src) == src
360
+
361
+
362
+ def test_extract_config_prefix_record_pause_real_cmd_inside_collected():
363
+ """Commands inside a record pause block are still collected (explicitly hidden)."""
364
+ from scriptcast.config import extract_config_prefix
365
+ src = (
366
+ ": SC record pause\n"
367
+ "some_real_cmd arg\n"
368
+ ": SC record resume\n"
369
+ ": SC set width 80\n"
370
+ )
371
+ assert extract_config_prefix(src) == src
372
+
373
+
374
+ def test_extract_config_prefix_stops_after_record_resume_at_scene():
375
+ from scriptcast.config import extract_config_prefix
376
+ src = (
377
+ ": SC record pause\n"
378
+ "GREEN='\\033[32m'\n"
379
+ ": SC record resume\n"
380
+ ": SC scene main\n"
381
+ "echo hi\n"
382
+ )
383
+ expected = (
384
+ ": SC record pause\n"
385
+ "GREEN='\\033[32m'\n"
386
+ ": SC record resume\n"
387
+ )
388
+ assert extract_config_prefix(src) == expected
389
+
390
+
391
+ def test_extract_config_prefix_custom_directive_prefix():
392
+ from scriptcast.config import extract_config_prefix
393
+ src = ": DEMO set width 80\n: DEMO scene main\necho hi\n"
394
+ assert extract_config_prefix(src, directive_prefix="DEMO") == ": DEMO set width 80\n"
395
+
396
+
397
+ def test_extract_config_prefix_export_var_assignment():
398
+ from scriptcast.config import extract_config_prefix
399
+ src = "export FOO=bar\n: SC scene main\n"
400
+ assert extract_config_prefix(src, directive_prefix="SC") == "export FOO=bar\n"