Skip to content

Commit 8d09b7a

Browse files
committed
Add Playwright browser tests for copy button and search features
Add comprehensive Playwright tests that verify: - Copy buttons are visible in messages and index items - Clicking copy button changes text to "Copied!" for 2 seconds - Copy button actually copies content to clipboard - Search modal exists and search box is hidden on file:// protocol - Timestamps are formatted by JavaScript Tests are automatically skipped if Playwright browsers are not installed, making CI/local development seamless without requiring browser setup. Added pytest-playwright to dev dependencies in pyproject.toml.
1 parent bcf8af2 commit 8d09b7a

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ dev = [
3535
"pytest>=9.0.2",
3636
"pytest-httpx>=0.35.0",
3737
"syrupy>=5.0.0",
38+
"pytest-playwright>=0.7.0",
3839
]

tests/test_playwright.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Playwright browser tests for copy button and search functionality.
2+
3+
These tests are skipped if Playwright browsers are not installed.
4+
Install browsers with: playwright install chromium
5+
"""
6+
7+
import json
8+
import tempfile
9+
from pathlib import Path
10+
11+
import pytest
12+
13+
# Check if Playwright browsers are available
14+
try:
15+
from playwright.sync_api import sync_playwright
16+
17+
def _check_browser_available():
18+
try:
19+
with sync_playwright() as p:
20+
browser = p.chromium.launch()
21+
browser.close()
22+
return True
23+
except Exception:
24+
return False
25+
26+
PLAYWRIGHT_AVAILABLE = _check_browser_available()
27+
except ImportError:
28+
PLAYWRIGHT_AVAILABLE = False
29+
30+
pytestmark = pytest.mark.skipif(
31+
not PLAYWRIGHT_AVAILABLE,
32+
reason="Playwright browsers not available. Install with: playwright install chromium",
33+
)
34+
35+
36+
@pytest.fixture
37+
def generated_html(tmp_path):
38+
"""Generate HTML files from the sample session for testing."""
39+
from claude_code_transcripts import generate_html
40+
41+
fixture_path = Path(__file__).parent / "sample_session.json"
42+
output_dir = tmp_path / "output"
43+
output_dir.mkdir()
44+
generate_html(fixture_path, output_dir, github_repo="example/project")
45+
return output_dir
46+
47+
48+
@pytest.fixture
49+
def page(generated_html):
50+
"""Create a Playwright page for testing."""
51+
from playwright.sync_api import sync_playwright
52+
53+
with sync_playwright() as p:
54+
browser = p.chromium.launch()
55+
page = browser.new_page()
56+
yield page, generated_html
57+
browser.close()
58+
59+
60+
class TestCopyButtonPlaywright:
61+
"""Playwright tests for the copy button feature."""
62+
63+
def test_copy_button_visible_in_message(self, page):
64+
"""Test that copy buttons are visible in messages."""
65+
page_obj, output_dir = page
66+
page_obj.goto(f"file://{output_dir}/page-001.html")
67+
68+
# Wait for copy buttons to be rendered
69+
copy_buttons = page_obj.locator("copy-button")
70+
assert copy_buttons.count() > 0, "Copy buttons should be present in messages"
71+
72+
def test_copy_button_visible_in_index(self, page):
73+
"""Test that copy buttons are visible in index items."""
74+
page_obj, output_dir = page
75+
page_obj.goto(f"file://{output_dir}/index.html")
76+
77+
# Wait for copy buttons to be rendered
78+
copy_buttons = page_obj.locator("copy-button")
79+
assert copy_buttons.count() > 0, "Copy buttons should be present in index"
80+
81+
def test_copy_button_click_changes_text(self, page):
82+
"""Test that clicking a copy button changes the text to 'Copied!'."""
83+
page_obj, output_dir = page
84+
page_obj.goto(f"file://{output_dir}/page-001.html")
85+
86+
# Grant clipboard permissions
87+
page_obj.context.grant_permissions(["clipboard-write", "clipboard-read"])
88+
89+
# Find first copy button and click it
90+
copy_button = page_obj.locator("copy-button").first
91+
copy_button.wait_for(state="attached")
92+
93+
# Get the shadow DOM button
94+
button = copy_button.locator("button")
95+
original_text = button.inner_text()
96+
97+
# Click the button
98+
button.click()
99+
100+
# Check that text changed to "Copied!"
101+
assert (
102+
button.inner_text() == "Copied!"
103+
), "Button text should change to 'Copied!' after click"
104+
105+
# Wait 2.5 seconds and check it reverts
106+
page_obj.wait_for_timeout(2500)
107+
assert (
108+
button.inner_text() == original_text
109+
), "Button text should revert after 2 seconds"
110+
111+
def test_copy_button_copies_markdown_content(self, page):
112+
"""Test that clicking a copy button actually copies content."""
113+
page_obj, output_dir = page
114+
page_obj.goto(f"file://{output_dir}/page-001.html")
115+
116+
# Grant clipboard permissions
117+
page_obj.context.grant_permissions(["clipboard-write", "clipboard-read"])
118+
119+
# Find a copy button with data-content (markdown)
120+
copy_button = page_obj.locator("copy-button[data-content]").first
121+
copy_button.wait_for(state="attached")
122+
123+
# Get the content that should be copied
124+
expected_content = copy_button.get_attribute("data-content")
125+
126+
# Click the button
127+
button = copy_button.locator("button")
128+
button.click()
129+
130+
# Read from clipboard
131+
clipboard_content = page_obj.evaluate("navigator.clipboard.readText()")
132+
133+
assert (
134+
clipboard_content == expected_content
135+
), "Clipboard should contain the data-content value"
136+
137+
138+
class TestSearchPlaywright:
139+
"""Playwright tests for the search feature."""
140+
141+
def test_search_box_hidden_by_default(self, page):
142+
"""Test that search box is hidden by default (progressive enhancement)."""
143+
page_obj, output_dir = page
144+
# Load the page with JS disabled to test progressive enhancement
145+
page_obj.context.set_offline(False)
146+
page_obj.goto(f"file://{output_dir}/index.html", wait_until="domcontentloaded")
147+
148+
# Note: With file:// protocol, search is hidden
149+
# This test verifies the file:// protocol behavior
150+
search_box = page_obj.locator("#search-box")
151+
# On file:// protocol, search box should remain hidden
152+
assert not search_box.is_visible() or search_box.evaluate(
153+
"el => getComputedStyle(el).display === 'none'"
154+
)
155+
156+
def test_search_modal_exists(self, page):
157+
"""Test that the search modal element exists."""
158+
page_obj, output_dir = page
159+
page_obj.goto(f"file://{output_dir}/index.html")
160+
161+
# The modal should exist in the DOM
162+
modal = page_obj.locator("#search-modal")
163+
assert modal.count() == 1, "Search modal should exist"
164+
165+
166+
class TestTimestampFormatting:
167+
"""Test that timestamps are formatted correctly by JavaScript."""
168+
169+
def test_timestamps_formatted(self, page):
170+
"""Test that timestamps are formatted by JavaScript."""
171+
page_obj, output_dir = page
172+
page_obj.goto(f"file://{output_dir}/page-001.html")
173+
174+
# Wait for JS to run
175+
page_obj.wait_for_load_state("networkidle")
176+
177+
# Get a timestamp element
178+
time_element = page_obj.locator("time[data-timestamp]").first
179+
time_element.wait_for(state="attached")
180+
181+
# The text should be formatted (not the raw ISO timestamp)
182+
text = time_element.inner_text()
183+
# Raw format is like "2025-01-01T12:00:00.000Z"
184+
# Formatted should be like "Jan 1 12:00" or just "12:00"
185+
assert "T" not in text, "Timestamp should be formatted, not raw ISO format"
186+
assert "Z" not in text, "Timestamp should be formatted, not raw ISO format"

0 commit comments

Comments
 (0)