用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/jhd3197/after-effects-automation --skill ae-test-writer命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | ae-test-writer |
| description | Write unit tests following the project's existing patterns and conventions |
This skill covers writing tests for the After Effects automation project. Tests use Python's built-in unittest framework and live in the tests/ directory.
Every test file follows this exact import pattern:
"""
Test description
"""
import unittest
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from ae_automation import Client
class TestMyFeature(unittest.TestCase):
"""Tests for [feature description]."""
def setUp(self):
"""Set up test fixtures."""
self.client = Client()
def test_something(self):
"""Test that something works correctly."""
result = self.client.someMethod()
self.assertEqual(result, expected)
if __name__ == '__main__':
unittest.main()
Key points:
sys.path.insert is required because tests run from the repo rootClient from ae_automation, not individual mixinsThe Client() constructor is lightweight -- it does NOT launch After Effects. It only:
json2.js and framework.js into JS_FRAMEWORKThis means every test class can safely create a Client in setUp() without needing AE installed:
def setUp(self):
self.client = Client()
Some test classes also include teardown:
def setUp(self):
self.client = None
self.client = Client()
def tearDown(self):
del self.client
For tests that require Windows-specific functionality (e.g., process checking via TASKLIST, AE interaction):
@unittest.skipUnless(sys.platform == 'win32', "Requires Windows")
class TestWindowsFeature(unittest.TestCase):
...
The project also has a shared decorator in tests/conftest.py:
from tests.conftest import skip_unless_windows
@skip_unless_windows
class TestWindowsFeature(unittest.TestCase):
...
Use the inline @unittest.skipUnless for clarity, or import from conftest.py for consistency with existing tests.
For testing flows that would normally interact with After Effects, use unittest.mock:
import unittest
from unittest.mock import patch, MagicMock
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from ae_automation import Client
class TestBotFlow(unittest.TestCase):
"""Test bot flow without launching After Effects."""
def setUp(self):
self.client = Client()
@patch.object(Client, 'startAfterEffect')
def test_startbot_calls_start_after_effect(self, mock_start):
"""Verify startBot delegates to startAfterEffect."""
import json
import tempfile
import os
config = {
"project": {
"project_file": "test.aep",
"comp_name": "TestComp",
"comp_fps": 30,
"comp_width": 1920,
"comp_height": 1080,
"output_file": "out.mp4",
"output_dir": ".",
"debug": True
},
"timeline": []
}
tmp = tempfile.NamedTemporaryFile(
mode=, suffix=, delete=
)
:
json.dump(config, tmp)
tmp.close()
.client.startBot(tmp.name)
mock_start.assert_called_once()
:
os.unlink(tmp.name)
Client.startAfterEffect -- prevents AE launchClient.runScript -- prevents JSX executionClient._execute_script_in_running_ae -- prevents queue writesClient.process_exists -- avoids Windows TASKLIST dependencyUse self.subTest() when testing multiple inputs with the same assertion logic:
def test_slugify_various_inputs(self):
"""Test slug generation with various inputs."""
test_cases = [
("Hello World", "hello-world"),
("Test_Case", "test_case"),
("UPPER case", "upper-case"),
("special!@#chars", "specialchars"),
]
for input_str, expected in test_cases:
with self.subTest(input=input_str):
result = self.client.slug(input_str)
self.assertEqual(result, expected)
def test_hex_to_rgba_colors(self):
"""Test hex color conversion to RGBA."""
test_cases = [
("#FF0000", "1.0,0.0,0.0,1"),
("#00FF00", "0.0,1.0,0.0,1"),
("#0000FF", "0.0,0.0,1.0,1"),
]
for hex_color, expected in test_cases:
with self.subTest(hex=hex_color):
result = self.client.hexToRGBA(hex_color)
self.assertEqual(result, expected)
Each subTest runs independently -- a failure in one doesn't stop the others, and the failure message includes the subTest parameters.
Create a helper function for building test configs with sensible defaults:
def create_test_config(**overrides):
"""Create a test configuration with defaults."""
config = {
"project": {
"project_file": "test.aep",
"comp_name": "TestComp",
"comp_fps": 30,
"comp_width": 1920,
"comp_height": 1080,
"output_file": "output.mp4",
"output_dir": "./output",
"renderComp": False,
"debug": True,
"resources": []
},
"timeline": []
}
config["project"].update(overrides)
return config
Usage:
def test_custom_fps(self):
config = create_test_config(comp_fps=60)
self.assertEqual(config["project"]["comp_fps"], 60)
def test_debug_mode(self):
config = create_test_config(debug=False)
self.assertFalse(config["project"]["debug"])
Place the helper at the module level (outside any class) so all test classes in the file can use it.
slug(), hexToRGBA(), sanitize_text_for_ae(), file_get_contents().jsx files exist and contain expected placeholdersrunScript() execution resultsFor AE-dependent flows, use mocks (see mock section above).
When tests create temporary files, always clean up with try/finally:
def test_config_loading(self):
"""Test loading a config from a temp file."""
import tempfile
import json
import os
config = create_test_config()
tmp = tempfile.NamedTemporaryFile(
mode='w', suffix='.json', delete=False
)
try:
json.dump(config, tmp)
tmp.close()
# Test the actual functionality
loaded = self.client.startBot(tmp.name)
# ... assertions ...
finally:
os.unlink(tmp.name)
Do NOT rely on tearDown for temp file cleanup -- if setUp or the test itself fails before creating the file reference, tearDown would crash.
test_<feature>.py -- e.g., test_client.py, test_utils.py, test_config.pyTest<Feature><Aspect> -- e.g., TestClientInitialization, TestConfigurationParsing, TestUtilityFunctionstest_<what_is_being_tested> -- e.g., test_slug_basic, test_hex_to_rgba_red| File | Classes | What it tests |
|---|---|---|
test_client.py | TestClientInitialization, TestClientCacheFolder | Client construction, attribute availability |
test_config.py | TestConfigurationParsing, TestTimeFormatParsing | Config loading, field validation, time format parsing |
test_jsx_integration.py | TestJSXScripts, TestJavaScriptFramework, TestScriptGeneration | JSX file existence, framework function detection, placeholder validation |
test_utils.py | TestUtilityFunctions, TestProcessChecking, TestSanitizeText | slug, hexToRGBA, process_exists, sanitize_text_for_ae |
test_integration.py | TestCheckIfItemExists, TestGetResourceDuration, TestStartBotFlow | Mock-based integration tests |
# Run all tests
python -m unittest discover tests -v
# Run a specific test file
python -m unittest tests.test_client -v
# Run a specific test class
python -m unittest tests.test_client.TestClientInitialization -v
# Run a specific test method
python -m unittest tests.test_client.TestClientInitialization.test_client_creation -v
"""
Tests for layer visibility toggle feature.
"""
import unittest
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent.parent))
from ae_automation import Client
def create_test_config(**overrides):
"""Create a test configuration with defaults."""
config = {
"project": {
"project_file": "test.aep",
"comp_name": "TestComp",
"comp_fps": 30,
"comp_width": 1920,
"comp_height": 1080,
"output_file": "output.mp4",
"output_dir": "./output",
"renderComp": False,
"debug": True,
"resources": []
},
"timeline": []
}
config["project"].update(overrides)
return config
class TestToggleVisibility(unittest.TestCase):
"""Tests for the layer visibility toggle feature."""
def setUp(self):
self.client = Client()
@patch.object()
():
.client.toggleLayerVisibility(, , )
mock_run.assert_called_once()
args = mock_run.call_args
.assertEqual(args[][], )
replacements = args[][]
.assertEqual(replacements[], )
.assertEqual(replacements[], )
.assertEqual(replacements[], )
():
.client.toggleLayerVisibility(, , )
args = mock_run.call_args
replacements = args[][]
.assertEqual(replacements[], )
():
replacements = {
: ,
: ,
: ,
}
key replacements:
.subTest(key=key):
.assertTrue(
key.startswith() key.endswith(),
)
(unittest.TestCase):
():
.client = Client()
():
action = {
: ,
: ,
: ,
: ,
}
required_fields = [, , ]
field required_fields:
.subTest(field=field):
.assertIn(field, action)
__name__ == :
unittest.main()
基于 SOC 职业分类