Compare commits
16 Commits
testdrive-
...
4d899d0690
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d899d0690 | |||
| dcb51b7e3a | |||
| d0432dbe0d | |||
| 45e4c7a6e9 | |||
| 01e5c811ab | |||
| 9fe2960842 | |||
| 7be37df3e4 | |||
| 21189f7664 | |||
| ddd8189576 | |||
| 2e6f292e48 | |||
| a1476a98b5 | |||
| 304959b3ee | |||
| 83086b3773 | |||
| 82eef76366 | |||
| 2838135450 | |||
| d592c5b8b3 |
323
.claude/capabilities/issue-facade.md
Normal file
323
.claude/capabilities/issue-facade.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# Issue Facade - Agent Integration Context
|
||||
|
||||
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.**
|
||||
|
||||
## Critical: DO NOT Bypass This Capability
|
||||
|
||||
⚠️ **IMPORTANT:** If you need to work with issues, **you MUST use this capability**. Do NOT:
|
||||
- ❌ Make direct API calls to Gitea/GitHub/GitLab
|
||||
- ❌ Use platform CLIs (gh, glab, etc.)
|
||||
- ❌ Import platform libraries (PyGithub, python-gitlab, etc.)
|
||||
- ❌ Parse HTML/scrape issue tracker web UIs
|
||||
|
||||
**Why?** Bypassing this capability causes:
|
||||
- Credential management chaos (tokens scattered everywhere)
|
||||
- Inconsistent issue state across agents
|
||||
- Massive token waste (redundant API calls)
|
||||
- Platform lock-in (can't switch Gitea → GitHub easily)
|
||||
- Race conditions in multi-agent scenarios
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Check if Capability is Available
|
||||
|
||||
```bash
|
||||
# Verify installation
|
||||
issue --version
|
||||
# or
|
||||
python -c "from issue_tracker.backends.gitea import GiteaBackend; print('OK')"
|
||||
```
|
||||
|
||||
### Basic Usage (Python)
|
||||
|
||||
```python
|
||||
from issue_tracker.backends.gitea import GiteaBackend
|
||||
from issue_tracker.core.models import Issue, Label, IssueState, User, Comment
|
||||
from issue_tracker.core.interfaces import IssueFilter
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
|
||||
# Connect (assumes backend is configured)
|
||||
backend = GiteaBackend()
|
||||
backend.connect({
|
||||
'base_url': os.environ['GITEA_URL'],
|
||||
'token': os.environ['GITEA_API_TOKEN'],
|
||||
'owner': os.environ['GITEA_OWNER'],
|
||||
'repo': os.environ['GITEA_REPO']
|
||||
})
|
||||
|
||||
# List issues for me
|
||||
my_issues = backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
assignee='my-agent-id',
|
||||
labels=['needs-implementation']
|
||||
))
|
||||
|
||||
# Create issue
|
||||
new_issue = Issue(
|
||||
id=None, number=0,
|
||||
title="Implement feature X",
|
||||
description="Details...",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
labels=[Label(name="feature"), Label(name="priority:high")]
|
||||
)
|
||||
created = backend.create_issue(new_issue)
|
||||
|
||||
# Update issue
|
||||
created.state = IssueState.IN_PROGRESS
|
||||
created.assignees = [User(id="agent-id", username="agent-id")]
|
||||
backend.update_issue(created)
|
||||
|
||||
# Add comment
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body="Implementation started. Working on database schema.",
|
||||
author=User(id="agent-id", username="agent-id"),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
backend.add_comment(created.id, comment)
|
||||
|
||||
# Close when done
|
||||
created.state = IssueState.CLOSED
|
||||
created.closed_at = datetime.now(timezone.utc)
|
||||
backend.update_issue(created)
|
||||
```
|
||||
|
||||
### Basic Usage (CLI)
|
||||
|
||||
```bash
|
||||
# List my open issues
|
||||
issue list --state=open --assignee=agent-id --format=json
|
||||
|
||||
# Create issue
|
||||
issue create "Implement feature X" \
|
||||
--label=feature \
|
||||
--label=priority:high \
|
||||
--description="Details here"
|
||||
|
||||
# Update state
|
||||
issue edit 42 --state=in_progress --assignee=agent-id
|
||||
|
||||
# Add comment
|
||||
issue comment 42 "Implementation started"
|
||||
|
||||
# Close
|
||||
issue close 42 --comment="Completed successfully"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Find Work
|
||||
|
||||
```python
|
||||
# Get next available task
|
||||
available_tasks = backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=['ready', 'needs-implementation']
|
||||
))
|
||||
|
||||
# Filter to unassigned
|
||||
unassigned = [t for t in available_tasks if not t.assignees]
|
||||
|
||||
if unassigned:
|
||||
task = unassigned[0]
|
||||
# Claim it...
|
||||
```
|
||||
|
||||
### Pattern 2: Claim Issue (Prevent Race Conditions)
|
||||
|
||||
```python
|
||||
def claim_issue(issue: Issue, agent_id: str) -> bool:
|
||||
"""Claim an issue safely."""
|
||||
# Check if already claimed
|
||||
if issue.assignees:
|
||||
return False # Already taken
|
||||
|
||||
# Claim it
|
||||
issue.state = IssueState.IN_PROGRESS
|
||||
issue.assignees = [User(id=agent_id, username=agent_id)]
|
||||
backend.update_issue(issue)
|
||||
|
||||
# Announce claim
|
||||
backend.add_comment(issue.id, Comment(
|
||||
id=None,
|
||||
body=f"🤖 Claimed by {agent_id}",
|
||||
author=User(id=agent_id, username=agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
))
|
||||
return True
|
||||
```
|
||||
|
||||
### Pattern 3: Progress Updates
|
||||
|
||||
```python
|
||||
def report_progress(issue: Issue, message: str, agent_id: str):
|
||||
"""Report progress on an issue."""
|
||||
backend.add_comment(issue.id, Comment(
|
||||
id=None,
|
||||
body=f"**Progress Update:**\n\n{message}",
|
||||
author=User(id=agent_id, username=agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 4: Agent-to-Agent Communication
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def post_agent_message(issue_id: str, msg_type: str, data: dict, agent_id: str):
|
||||
"""Post structured message for other agents."""
|
||||
message = {
|
||||
'type': msg_type,
|
||||
'agent': agent_id,
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
'data': data
|
||||
}
|
||||
backend.add_comment(issue_id, Comment(
|
||||
id=None,
|
||||
body=f"```agent-message\n{json.dumps(message, indent=2)}\n```",
|
||||
author=User(id=agent_id, username=agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
))
|
||||
|
||||
def read_agent_messages(issue_id: str, msg_type: str = None):
|
||||
"""Read messages from other agents."""
|
||||
comments = backend.get_comments(issue_id)
|
||||
messages = []
|
||||
for comment in comments:
|
||||
if '```agent-message' in comment.body:
|
||||
try:
|
||||
json_str = comment.body.split('```agent-message\n')[1].split('\n```')[0]
|
||||
msg = json.loads(json_str)
|
||||
if msg_type is None or msg['type'] == msg_type:
|
||||
messages.append(msg)
|
||||
except:
|
||||
continue
|
||||
return messages
|
||||
```
|
||||
|
||||
## Configuration Check
|
||||
|
||||
Before using issue tracking, verify configuration:
|
||||
|
||||
```python
|
||||
def verify_issue_backend() -> bool:
|
||||
"""Verify issue backend is configured."""
|
||||
try:
|
||||
backend = GiteaBackend()
|
||||
backend.connect({
|
||||
'base_url': os.environ['GITEA_URL'],
|
||||
'token': os.environ['GITEA_API_TOKEN'],
|
||||
'owner': os.environ['GITEA_OWNER'],
|
||||
'repo': os.environ['GITEA_REPO']
|
||||
})
|
||||
return backend.test_connection()
|
||||
except Exception as e:
|
||||
print(f"Issue backend not configured: {e}")
|
||||
return False
|
||||
|
||||
# Use it
|
||||
if not verify_issue_backend():
|
||||
print("ERROR: Issue tracking not available. Check configuration.")
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
from issue_tracker.backends.gitea.backend import GiteaAPIError
|
||||
|
||||
try:
|
||||
issue = backend.get_issue_by_number(42)
|
||||
except GiteaAPIError as e:
|
||||
if e.status_code == 404:
|
||||
print("Issue not found")
|
||||
elif e.status_code == 401:
|
||||
print("Authentication failed - check GITEA_API_TOKEN")
|
||||
elif e.status_code == 429:
|
||||
print("Rate limited - wait and retry")
|
||||
else:
|
||||
print(f"API error: {e}")
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use filters** instead of fetching all issues:
|
||||
```python
|
||||
# BAD: Get all, filter in Python
|
||||
all_issues = backend.list_issues()
|
||||
my_issues = [i for i in all_issues if i.assignees and i.assignees[0].username == 'me']
|
||||
|
||||
# GOOD: Filter at backend
|
||||
my_issues = backend.list_issues(IssueFilter(assignee='me'))
|
||||
```
|
||||
|
||||
2. **Use JSON output** for CLI parsing:
|
||||
```bash
|
||||
issue list --format=json | jq '.[] | select(.state == "open")'
|
||||
```
|
||||
|
||||
3. **Batch comments** instead of rapid-fire updates
|
||||
|
||||
4. **Check local cache** before querying (if available)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Backend not configured"
|
||||
```bash
|
||||
# Check config
|
||||
issue backend list
|
||||
|
||||
# If empty, configure
|
||||
export GITEA_API_TOKEN="your-token"
|
||||
issue backend add myproject gitea
|
||||
issue backend set-default myproject
|
||||
```
|
||||
|
||||
### "Authentication failed"
|
||||
```bash
|
||||
# Verify token
|
||||
curl -H "Authorization: token $GITEA_API_TOKEN" $GITEA_URL/api/v1/user
|
||||
```
|
||||
|
||||
### "Issue not found"
|
||||
```python
|
||||
# Use get_issue_by_number, not get_issue
|
||||
issue = backend.get_issue_by_number(42) # Correct
|
||||
# issue = backend.get_issue("42") # Wrong - needs backend_id
|
||||
```
|
||||
|
||||
## Full Documentation
|
||||
|
||||
- **Integration Guide:** `AGENT_INTEGRATION.md` (comprehensive patterns and strategies)
|
||||
- **API Reference:** `CLAUDE.md` (for developers extending the capability)
|
||||
- **Examples:** `examples/agents/` (working agent implementations)
|
||||
- **Roadmap:** `ROADMAP.md` (upcoming features)
|
||||
|
||||
## Current Limitations (v1.0)
|
||||
|
||||
Be aware of these limitations:
|
||||
|
||||
1. **Manual Configuration:** Backend must be configured before use (auto-detect in v1.1)
|
||||
2. **User Context:** Uses hardcoded user for CLI operations (agent identity in v1.2)
|
||||
3. **No Built-in Locking:** Use assignee + comment workaround for claiming (native in v1.2)
|
||||
4. **Basic Conflicts:** Manual resolution required for complex sync conflicts (advanced in v2.0)
|
||||
|
||||
Workarounds are documented in `AGENT_INTEGRATION.md`.
|
||||
|
||||
## Questions?
|
||||
|
||||
If you're unsure whether to use this capability for something:
|
||||
|
||||
**ASK:** "Does this involve creating, reading, updating, or searching issues?"
|
||||
- **YES** → Use this capability
|
||||
- **NO** → You can use other methods
|
||||
|
||||
**Example:**
|
||||
- "Create an issue for the bug I found" → **Use issue-facade**
|
||||
- "Read the project README" → Don't need issue-facade
|
||||
- "Check if issue #42 exists" → **Use issue-facade**
|
||||
- "Clone the repository" → Don't need issue-facade
|
||||
6
.claude/commands/use-issues.md
Normal file
6
.claude/commands/use-issues.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Use the issue-facade capability for all issue tracking operations.
|
||||
|
||||
**Quick reference:** See `.claude/capabilities/issue-facade.md`
|
||||
**Examples:** See `capabilities/issue-facade/examples/agents/`
|
||||
|
||||
**DO NOT use direct API calls or platform CLIs!**
|
||||
8
.claude/context/capabilities.md
Normal file
8
.claude/context/capabilities.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Available Capabilities
|
||||
|
||||
## Issue Tracking: issue-facade
|
||||
|
||||
**CRITICAL:** Always use this for issue operations. Never bypass with direct API calls.
|
||||
|
||||
**Docs:** `.claude/capabilities/issue-facade.md`
|
||||
**Usage:** `/use-issues`
|
||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -2,12 +2,13 @@
|
||||
path = wiki
|
||||
url = http://92.205.130.254:32166/coulomb/markitect_project.wiki.git
|
||||
branch = main
|
||||
[submodule "capabilities/issue-facade"]
|
||||
path = capabilities/issue-facade
|
||||
url = http://92.205.130.254:32166/coulomb/issue-facade.git
|
||||
[submodule "capabilities/kaizen-agentic"]
|
||||
path = capabilities/kaizen-agentic
|
||||
url = http://92.205.130.254:32166/coulomb/kaizen-agentic.git
|
||||
[submodule "capabilities/testdrive-jsui"]
|
||||
path = capabilities/testdrive-jsui
|
||||
url = http://92.205.130.254:32166/coulomb/testdrive-jsui.git
|
||||
[submodule "_issue-tracking/issue-facade"]
|
||||
path = _issue-tracking/issue-facade
|
||||
url = http://92.205.130.254:32166/coulomb/issue-facade.git
|
||||
branch = main
|
||||
|
||||
1
_issue-tracking/issue-facade
Submodule
1
_issue-tracking/issue-facade
Submodule
Submodule _issue-tracking/issue-facade added at f89772ac79
51
capabilities/DETACHED-issue-facade.yaml
Normal file
51
capabilities/DETACHED-issue-facade.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
# Detachment Manifest
|
||||
# This file records the removal of the issue-facade capability
|
||||
# Use this information to re-integrate with updated architecture
|
||||
|
||||
detachment:
|
||||
timestamp: 2025-12-17T21:23:14Z
|
||||
capability_name: issue-facade
|
||||
capability_family: issue-tracking
|
||||
integration_pattern: capabilities-directory
|
||||
original_location: /home/worsch/markitect_project/capabilities/issue-facade
|
||||
|
||||
capability_metadata:
|
||||
spec_file: CAPABILITY-issue-tracking.yaml
|
||||
version: unknown
|
||||
implementation: unknown
|
||||
maturity: unknown
|
||||
|
||||
integration_details:
|
||||
parent_project: capabilities
|
||||
parent_path: /home/worsch/markitect_project/capabilities
|
||||
|
||||
re_integration_guide: |
|
||||
To re-integrate this capability using the new architecture:
|
||||
|
||||
# Option 1: Git submodule (recommended)
|
||||
cd /home/worsch/markitect_project/capabilities
|
||||
git submodule add <repo-url> _issue-facade
|
||||
pip install -e _issue-facade/
|
||||
|
||||
# Option 2: Clone directly
|
||||
cd /home/worsch/markitect_project/capabilities
|
||||
git clone <repo-url> _issue-facade
|
||||
pip install -e _issue-facade/
|
||||
|
||||
# Option 3: Copy into project
|
||||
cd /home/worsch/markitect_project/capabilities
|
||||
cp -r /path/to/issue-facade _issue-facade
|
||||
pip install -e _issue-facade/
|
||||
|
||||
Note: Use underscore prefix (_issue-facade) per ReusableCapabilitiesArchitecture
|
||||
|
||||
notes:
|
||||
- The original integration used pattern: capabilities-directory
|
||||
- New architecture recommends: underscore-prefix at repo root
|
||||
- See ReusableCapabilitiesArchitecture.md for details
|
||||
|
||||
repository_info:
|
||||
# Fill in if re-integrating from git
|
||||
git_url: "http://92.205.130.254:32166/coulomb/issue-facade.git" # e.g., https://github.com/markitect/issue-facade
|
||||
git_branch: "main" # e.g., main
|
||||
git_commit: "35daa514e59788250847cd706c43ea78f24c5c1d" # Optional: specific commit to use
|
||||
Submodule capabilities/issue-facade deleted from 34a8bc7d4c
Submodule capabilities/testdrive-jsui updated: 891d785533...b8f13b4ae5
158
examples/design-patterns/CopyFirstMigration.md
Normal file
158
examples/design-patterns/CopyFirstMigration.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Design Principle: Copy First Migration
|
||||
|
||||
## Meta
|
||||
- **Name:** Copy First Migration
|
||||
- **ShortName:** CopyFirst
|
||||
- **Version:** 0.1
|
||||
- **Status:** Draft
|
||||
- **Tags:** refactoring, migration, safety, testing, legacy
|
||||
- **RelatedPrinciples:** Don’t Repeat Yourself, Safe Refactoring, Test Pyramid, Capability-Based Testing
|
||||
|
||||
---
|
||||
|
||||
## Intent
|
||||
Enable safe refactoring and structural migration of codebases by preserving
|
||||
existing, working functionality until the new implementation is fully verified.
|
||||
|
||||
This principle prioritizes **reversibility, confidence, and continuity** over
|
||||
speed or elegance.
|
||||
|
||||
---
|
||||
|
||||
## CoreStatement
|
||||
Never move code directly; always copy first and delete only after verified
|
||||
behavioral equivalence is established.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### InScope
|
||||
- Large-scale refactors or directory restructurings
|
||||
- Technology or language migrations (e.g. JS → new JS layout, JS → Python integration)
|
||||
- Legacy code stabilization
|
||||
- Safety-critical or business-critical systems
|
||||
- Situations with incomplete test coverage
|
||||
|
||||
### OutOfScope
|
||||
- Greenfield development
|
||||
- Trivial refactors with full and trusted test coverage
|
||||
- One-off throwaway scripts
|
||||
- Performance-driven rewrites where duplication is unacceptable
|
||||
|
||||
---
|
||||
|
||||
## InterpretationGuidelines
|
||||
|
||||
### What “Copy First” Means
|
||||
- The original code remains untouched and functional
|
||||
- The new version is treated as **experimental until proven**
|
||||
- Deletion is a **final, explicit act**, not an implicit side effect
|
||||
|
||||
### Common Misinterpretations
|
||||
- “This is inefficient because it duplicates code”
|
||||
→ Duplication is intentional and temporary
|
||||
- “Moving files is faster”
|
||||
→ Speed is not the optimization target here
|
||||
- “Tests alone are enough”
|
||||
→ Tests are necessary but not sufficient without behavioral comparison
|
||||
|
||||
---
|
||||
|
||||
## DetectionHeuristics
|
||||
|
||||
### Structural Signals
|
||||
- Files or modules being relocated across directories or packages
|
||||
- Parallel implementations during migration
|
||||
- Introduction of a new architectural boundary
|
||||
|
||||
### Semantic Signals
|
||||
- Code paths that must remain behaviorally identical
|
||||
- Business rules with high regression risk
|
||||
- Legacy logic that is poorly documented but relied upon
|
||||
|
||||
### Change-Cost Signals
|
||||
- Rollbacks are expensive or disruptive
|
||||
- Failures would impact production or customers
|
||||
- Migration spans multiple commits or teams
|
||||
|
||||
---
|
||||
|
||||
## DiagnosticQuestions
|
||||
1. What breaks if this migration is wrong?
|
||||
2. Do we have a known-good reference implementation?
|
||||
3. Can both old and new code paths run in parallel?
|
||||
4. How quickly can we revert if a defect is found?
|
||||
5. What is the minimal proof of behavioral equivalence?
|
||||
|
||||
---
|
||||
|
||||
## RecommendedActions
|
||||
|
||||
### Low-Risk Actions
|
||||
- Copy files to the new location instead of moving
|
||||
- Preserve original imports and entry points
|
||||
- Add logging or tracing for comparison
|
||||
|
||||
### Medium-Risk Actions
|
||||
- Introduce dual-track execution (old + new)
|
||||
- Add integration tests targeting both implementations
|
||||
- Compare outputs, side effects, and error behavior
|
||||
|
||||
### High-Risk Actions
|
||||
- Switch production usage to the new implementation
|
||||
- Remove old code only after full verification
|
||||
- Collapse duplicated paths once confidence is established
|
||||
|
||||
---
|
||||
|
||||
## AcceptanceCriteria
|
||||
- Original code remains functional until final removal
|
||||
- New code passes all existing tests
|
||||
- New integration tests validate identical behavior
|
||||
- Dual-track comparisons show no regressions
|
||||
- Deletion of old code is deliberate and reversible up to the final step
|
||||
|
||||
---
|
||||
|
||||
## AntiPatterns
|
||||
- Moving files directly without a fallback
|
||||
- Refactoring and migration in a single irreversible step
|
||||
- Deleting “unused” code before equivalence is proven
|
||||
- Assuming test parity guarantees behavioral parity
|
||||
- Big-bang migrations without rollback paths
|
||||
|
||||
---
|
||||
|
||||
## Tradeoffs
|
||||
Applying Copy First Migration intentionally:
|
||||
- Introduces temporary duplication
|
||||
- Increases short-term codebase size
|
||||
- Slows perceived progress
|
||||
|
||||
These costs are justified by dramatically reduced risk and higher confidence
|
||||
during complex migrations.
|
||||
|
||||
---
|
||||
|
||||
## AgentUsage
|
||||
|
||||
### When to Apply This Lens
|
||||
- During directory, module, or architecture migrations
|
||||
- When refactoring legacy or poorly understood code
|
||||
- When safety and uptime matter more than speed
|
||||
- When rollback must remain possible at all times
|
||||
|
||||
### When to Suspend This Lens
|
||||
- In greenfield projects
|
||||
- When full test coverage and confidence already exist
|
||||
- For trivial mechanical refactors
|
||||
|
||||
### Expected Agent Output
|
||||
- Identification of migration boundaries
|
||||
- Copy-first migration plan with explicit stages
|
||||
- Test strategy (unit, integration, dual-track)
|
||||
- Rollback points and deletion criteria
|
||||
- Clear signal for when old code may be removed
|
||||
|
||||
xxx
|
||||
135
examples/design-patterns/DesignPrincipleSchema.json
Normal file
135
examples/design-patterns/DesignPrincipleSchema.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Schema for DesignPrinciples",
|
||||
"description": "JSON schema describing the markdown structure of OperationalKnowledge DesignPrinciples",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"description": "Document heading structure",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"description": "Headings at level 1",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"level"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"description": "Headings at level 2",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"level"
|
||||
]
|
||||
},
|
||||
"minItems": 4,
|
||||
"maxItems": 12
|
||||
},
|
||||
"level_3": {
|
||||
"type": "array",
|
||||
"description": "Headings at level 3",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"level"
|
||||
]
|
||||
},
|
||||
"minItems": 0,
|
||||
"maxItems": 40
|
||||
}
|
||||
}
|
||||
},
|
||||
"paragraphs": {
|
||||
"type": "array",
|
||||
"description": "Text paragraphs",
|
||||
"minItems": 8,
|
||||
"maxItems": 120
|
||||
},
|
||||
"lists": {
|
||||
"type": "array",
|
||||
"description": "Lists (ordered and unordered)",
|
||||
"minItems": 0,
|
||||
"maxItems": 20
|
||||
},
|
||||
"emphasis": {
|
||||
"type": "array",
|
||||
"description": "Text emphasis (bold, italic)",
|
||||
"minItems": 0,
|
||||
"maxItems": 120
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Document structure metadata",
|
||||
"properties": {
|
||||
"total_elements": {
|
||||
"type": "integer",
|
||||
"const": 115
|
||||
},
|
||||
"structure_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "All structural element types found",
|
||||
"const": [
|
||||
"paragraph_close",
|
||||
"heading_close",
|
||||
"hr",
|
||||
"bullet_list_open",
|
||||
"paragraph_open",
|
||||
"heading_open",
|
||||
"ordered_list_open",
|
||||
"ordered_list_close",
|
||||
"inline",
|
||||
"list_item_close",
|
||||
"list_item_open",
|
||||
"bullet_list_close"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
examples/design-patterns/DontRepeatYourself.md
Normal file
160
examples/design-patterns/DontRepeatYourself.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Design Principle: Don’t Repeat Yourself (DRY)
|
||||
|
||||
## Meta
|
||||
- **Name:** Don’t Repeat Yourself
|
||||
- **ShortName:** DRY
|
||||
- **Version:** 0.1
|
||||
- **Status:** Stable
|
||||
- **Tags:** maintainability, refactoring, architecture, quality
|
||||
- **RelatedPrinciples:** Single Responsibility, YAGNI, Separation of Concerns
|
||||
|
||||
---
|
||||
|
||||
## Intent
|
||||
Reduce maintenance cost and behavioral drift by ensuring that each piece of
|
||||
knowledge, rule, or decision logic has a single authoritative representation
|
||||
in the codebase.
|
||||
|
||||
---
|
||||
|
||||
## CoreStatement
|
||||
A codebase violates DRY when the same knowledge is expressed in multiple places
|
||||
such that a change would require edits in more than one location or risks
|
||||
inconsistent behavior.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### InScope
|
||||
- Business rules and decision logic
|
||||
- Algorithms and validation logic
|
||||
- Data schemas, DTOs, and field definitions
|
||||
- Configuration values and feature flags
|
||||
- Repeated workflows or orchestration logic
|
||||
- Test setup and invariant test scenarios
|
||||
|
||||
### OutOfScope
|
||||
- Superficial textual similarity without shared meaning
|
||||
- Intentional duplication for isolation or clarity
|
||||
- Early-stage exploratory code where abstractions are not yet clear
|
||||
- Performance-driven duplication with explicit justification
|
||||
|
||||
---
|
||||
|
||||
## InterpretationGuidelines
|
||||
|
||||
### What “Repeat” Means
|
||||
DRY is about **duplication of knowledge**, not duplication of text.
|
||||
|
||||
Examples of knowledge duplication:
|
||||
- The same validation rule implemented in multiple services
|
||||
- Identical conditional logic controlling the same behavior
|
||||
- The same data structure defined independently in multiple modules
|
||||
|
||||
### Common Misinterpretations
|
||||
- “Any repeated code is bad” (false)
|
||||
- “DRY means maximum abstraction” (false)
|
||||
- “Utility modules automatically improve DRY” (often false)
|
||||
|
||||
---
|
||||
|
||||
## DetectionHeuristics
|
||||
|
||||
### Structural Signals
|
||||
- Functions with highly similar bodies and signatures
|
||||
- Repeated constants, strings, regexes, or SQL fragments
|
||||
- Parallel modules with mirrored internal structure
|
||||
|
||||
### Semantic Signals
|
||||
- Identical error messages or validation rules in different layers
|
||||
- Repeated mapping logic between the same concepts
|
||||
- Copy-paste variations differing only in naming
|
||||
|
||||
### Change-Cost Signals
|
||||
- A requirement change touches multiple files for the same reason
|
||||
- Fixes applied in one location but missing in others
|
||||
- Tests failing inconsistently after partial updates
|
||||
|
||||
---
|
||||
|
||||
## DiagnosticQuestions
|
||||
1. Is this duplication representing the same rule or policy?
|
||||
2. If this rule changes, how many places must be updated?
|
||||
3. Is the duplicated logic stable or likely to evolve?
|
||||
4. Are the differences intentional or accidental?
|
||||
5. Where is the natural “source of truth” for this knowledge?
|
||||
6. Would abstraction reduce or increase cognitive load?
|
||||
|
||||
---
|
||||
|
||||
## RecommendedActions
|
||||
|
||||
### Low-Risk Refactors
|
||||
- Extract constants or configuration values
|
||||
- Centralize literals and error messages
|
||||
- Introduce shared test fixtures or helpers
|
||||
|
||||
### Medium-Risk Refactors
|
||||
- Extract pure helper functions
|
||||
- Introduce shared domain services or modules
|
||||
- Unify schema/type definitions
|
||||
|
||||
### High-Risk Refactors
|
||||
- Introduce strategy/template patterns
|
||||
- Merge parallel subsystems
|
||||
- Redesign domain boundaries to align ownership of rules
|
||||
|
||||
---
|
||||
|
||||
## AcceptanceCriteria
|
||||
- Each rule or behavior has a single authoritative implementation
|
||||
- Required changes affect fewer locations than before
|
||||
- Naming reflects domain meaning, not technical convenience
|
||||
- Tests pass without behavior regression
|
||||
- Coupling does not increase unintentionally
|
||||
|
||||
---
|
||||
|
||||
## AntiPatterns
|
||||
- “God” utility modules with unrelated helpers
|
||||
- Over-generalized abstractions with many parameters
|
||||
- Shared code across domains that should evolve independently
|
||||
- Premature abstraction of coincidental similarities
|
||||
- Hiding meaningful differences behind generic interfaces
|
||||
|
||||
---
|
||||
|
||||
## Tradeoffs
|
||||
Applying DRY may:
|
||||
- Increase indirection
|
||||
- Reduce local readability
|
||||
- Introduce coupling between modules
|
||||
|
||||
These costs are acceptable only when outweighed by reduced change cost
|
||||
and increased behavioral consistency.
|
||||
|
||||
---
|
||||
|
||||
## AgentUsage
|
||||
|
||||
### When to Apply This Lens
|
||||
- During refactoring or maintenance work
|
||||
- When change requests repeatedly touch similar code
|
||||
- When bugs recur due to partial updates
|
||||
- During architectural consolidation
|
||||
|
||||
### When to Suspend This Lens
|
||||
- During early exploration or prototyping
|
||||
- When future variability is unclear
|
||||
- When isolation is more valuable than reuse
|
||||
|
||||
### Expected Agent Output
|
||||
- Identified DRY violations with locations
|
||||
- Rationale for why duplication matters
|
||||
- Volatility assessment (stable vs evolving)
|
||||
- Recommended refactor type and target
|
||||
- Risk notes and minimal patch sequence
|
||||
|
||||
xxx
|
||||
|
||||
@@ -1284,11 +1284,25 @@ MISSING: {len(missing_components)} components
|
||||
html_content = markdown_content_with_dogtag.replace('\n\n', '</p><p>').replace('\n', '<br>')
|
||||
html_content = f'<p>{html_content}</p>'
|
||||
|
||||
# Generate or read CSS content
|
||||
if css:
|
||||
# If css is a file path, read it
|
||||
css_path = Path(css)
|
||||
if css_path.exists() and css_path.is_file():
|
||||
css_content = f'<style>\n{css_path.read_text(encoding="utf-8")}\n</style>'
|
||||
else:
|
||||
# Assume it's raw CSS content
|
||||
css_content = f'<style>\n{css}\n</style>'
|
||||
else:
|
||||
# Use template-based CSS generation
|
||||
css_content = self._get_template_css(template, image_max_width, image_max_height)
|
||||
|
||||
# Replace template placeholders using safe string replacement
|
||||
# This avoids conflicts with CSS curly braces
|
||||
html_template = template_content.replace('{title}', title)
|
||||
html_template = html_template.replace('{version}', version_str)
|
||||
html_template = html_template.replace('{content}', html_content)
|
||||
html_template = html_template.replace('{css_content}', css_content)
|
||||
|
||||
return html_template
|
||||
|
||||
@@ -1302,8 +1316,18 @@ MISSING: {len(missing_components)} components
|
||||
|
||||
template_content = template_path.read_text(encoding='utf-8')
|
||||
|
||||
# Generate CSS
|
||||
css_content = self._get_template_css(template, image_max_width, image_max_height) if not css else css
|
||||
# Generate or read CSS content
|
||||
if css:
|
||||
# If css is a file path, read it
|
||||
css_path = Path(css)
|
||||
if css_path.exists() and css_path.is_file():
|
||||
css_content = f'<style>\n{css_path.read_text(encoding="utf-8")}\n</style>'
|
||||
else:
|
||||
# Assume it's raw CSS content
|
||||
css_content = f'<style>\n{css}\n</style>'
|
||||
else:
|
||||
# Use template-based CSS generation
|
||||
css_content = self._get_template_css(template, image_max_width, image_max_height)
|
||||
|
||||
# Create configuration object - ONLY dynamic data interface
|
||||
config = {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* DebugPanel Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Handles debug message display and management for client-side debugging.
|
||||
*
|
||||
* Dependencies:
|
||||
* - None (standalone component)
|
||||
*/
|
||||
|
||||
/**
|
||||
* DebugPanel - Manages debug message display and interaction
|
||||
*/
|
||||
class DebugPanel {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
this.isActive = false;
|
||||
this.maxMessages = 1000; // Keep last 1000 messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a debug message
|
||||
*/
|
||||
addMessage(message, category = 'INFO') {
|
||||
const messageObj = {
|
||||
message,
|
||||
category,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
};
|
||||
|
||||
this.messages.push(messageObj);
|
||||
|
||||
// Keep only last maxMessages
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages = this.messages.slice(-this.maxMessages);
|
||||
}
|
||||
|
||||
// Auto-update if panel is visible
|
||||
if (this.isActive) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the debug panel on/off
|
||||
*/
|
||||
toggle() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isActive) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the debug panel
|
||||
*/
|
||||
show() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugContainer.style.display = 'block';
|
||||
debugButton.textContent = '🔍 Debug (ON)';
|
||||
debugButton.style.background = '#28a745';
|
||||
this.isActive = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the debug panel
|
||||
*/
|
||||
hide() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugContainer.style.display = 'none';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
debugButton.style.background = '#6c757d';
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the debug panel with current messages
|
||||
*/
|
||||
update() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
if (!debugContainer || !this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.messages.length === 0) {
|
||||
debugContainer.innerHTML = '<div style="color: #6c757d; font-style: italic; padding: 12px;">No debug messages yet. Click sections to generate debug output.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the last 50 messages in reverse order (newest first)
|
||||
const recentMessages = this.messages.slice(-50).reverse();
|
||||
|
||||
const messagesHtml = recentMessages.map(msg => {
|
||||
const categoryColor = {
|
||||
'INFO': '#17a2b8',
|
||||
'WARNING': '#ffc107',
|
||||
'ERROR': '#dc3545',
|
||||
'SUCCESS': '#28a745',
|
||||
'DEBUG': '#6f42c1'
|
||||
}[msg.category] || '#6c757d';
|
||||
|
||||
return `
|
||||
<div style="margin-bottom: 6px; padding: 4px; border-left: 3px solid ${categoryColor}; background: white; border-radius: 2px;">
|
||||
<span style="color: #6c757d; font-size: 11px;">[${msg.timestamp}]</span>
|
||||
<span style="color: ${categoryColor}; font-weight: bold;">${msg.category}:</span>
|
||||
<span style="color: #333;">${msg.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
debugContainer.innerHTML = `
|
||||
<div style="margin-bottom: 8px; padding: 6px; background: #e9ecef; border-radius: 4px; font-weight: bold; color: #495057;">
|
||||
Debug Messages (${this.messages.length} total, showing last ${recentMessages.length})
|
||||
<button id="debug-clear-btn" style="float: right; background: #dc3545; color: white; border: none; padding: 2px 6px; border-radius: 2px; font-size: 11px; cursor: pointer;">Clear</button>
|
||||
</div>
|
||||
<div style="max-height: 250px; overflow-y: auto;">
|
||||
${messagesHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listener for clear button
|
||||
const clearBtn = debugContainer.querySelector('#debug-clear-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
this.clear();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom to show newest messages
|
||||
const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all debug messages
|
||||
*/
|
||||
clear() {
|
||||
this.messages = [];
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of messages
|
||||
*/
|
||||
getMessageCount() {
|
||||
return this.messages.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent messages
|
||||
*/
|
||||
getRecentMessages(count = 10) {
|
||||
return this.messages.slice(-count);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DebugPanel };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DebugPanel = DebugPanel;
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
/**
|
||||
* DocumentControls Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Handles the floating control panel and document-level actions.
|
||||
*
|
||||
* Dependencies:
|
||||
* - None (standalone component)
|
||||
*/
|
||||
|
||||
/**
|
||||
* DocumentControls - Manages the floating control panel and its buttons
|
||||
*/
|
||||
class DocumentControls {
|
||||
constructor() {
|
||||
this.controlPanel = null;
|
||||
this.buttons = new Map();
|
||||
this.eventHandlers = new Map();
|
||||
this.isVisible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the control panel and add it to the DOM
|
||||
*/
|
||||
create() {
|
||||
if (this.controlPanel) {
|
||||
this.destroy(); // Remove existing panel
|
||||
}
|
||||
|
||||
// Also remove any existing panel with the same ID in the DOM
|
||||
const existingPanel = document.getElementById('markitect-global-controls');
|
||||
if (existingPanel && existingPanel.parentNode) {
|
||||
existingPanel.parentNode.removeChild(existingPanel);
|
||||
}
|
||||
|
||||
// Create the floating control panel
|
||||
this.controlPanel = document.createElement('div');
|
||||
this.controlPanel.id = 'markitect-global-controls';
|
||||
this.controlPanel.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(248, 249, 250, 0.95);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(8px);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
`;
|
||||
|
||||
// Add title
|
||||
const title = document.createElement('div');
|
||||
title.style.cssText = `
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #495057;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 4px;
|
||||
`;
|
||||
title.textContent = 'Document Controls';
|
||||
|
||||
// Create button container
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.id = 'button-container';
|
||||
buttonContainer.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
this.controlPanel.appendChild(title);
|
||||
this.controlPanel.appendChild(buttonContainer);
|
||||
|
||||
// Add default buttons
|
||||
this.addDefaultButtons();
|
||||
|
||||
// Add debug messages container
|
||||
this.addDebugContainer();
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(this.controlPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default buttons to the control panel
|
||||
*/
|
||||
addDefaultButtons() {
|
||||
// Save Document button
|
||||
this.addButton('save-document', '💾 Save Document', '#28a745');
|
||||
|
||||
// Reset All button
|
||||
this.addButton('reset-all', '🔄 Reset All', '#ffc107', '#212529');
|
||||
|
||||
// Show Status button
|
||||
this.addButton('show-status', '📊 Show Status', '#17a2b8');
|
||||
|
||||
// Debug button
|
||||
this.addButton('toggle-debug', '🔍 Debug', '#6c757d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add debug container to the control panel
|
||||
*/
|
||||
addDebugContainer() {
|
||||
const debugContainer = document.createElement('div');
|
||||
debugContainer.id = 'debug-messages-container';
|
||||
debugContainer.style.cssText = `
|
||||
margin-top: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
padding: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
display: none;
|
||||
`;
|
||||
|
||||
this.controlPanel.appendChild(debugContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a button to the control panel
|
||||
*/
|
||||
addButton(id, text, backgroundColor, textColor = 'white') {
|
||||
const buttonContainer = this.controlPanel.querySelector('#button-container');
|
||||
if (!buttonContainer) {
|
||||
throw new Error('Button container not found. Call create() first.');
|
||||
}
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.id = id;
|
||||
button.textContent = text;
|
||||
button.style.cssText = `
|
||||
background: ${backgroundColor};
|
||||
color: ${textColor};
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
`;
|
||||
|
||||
buttonContainer.appendChild(button);
|
||||
this.buttons.set(id, button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a button from the control panel
|
||||
*/
|
||||
removeButton(id) {
|
||||
const button = this.buttons.get(id);
|
||||
if (button && button.parentNode) {
|
||||
button.parentNode.removeChild(button);
|
||||
this.buttons.delete(id);
|
||||
this.eventHandlers.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set event handlers for buttons
|
||||
*/
|
||||
setEventHandlers(handlers) {
|
||||
for (const [buttonId, handler] of Object.entries(handlers)) {
|
||||
const button = this.buttons.get(buttonId);
|
||||
if (button) {
|
||||
// Remove existing handler if any
|
||||
if (this.eventHandlers.has(buttonId)) {
|
||||
button.removeEventListener('click', this.eventHandlers.get(buttonId));
|
||||
}
|
||||
|
||||
// Add new handler
|
||||
button.addEventListener('click', handler);
|
||||
this.eventHandlers.set(buttonId, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the control panel
|
||||
*/
|
||||
show() {
|
||||
if (this.controlPanel) {
|
||||
this.controlPanel.style.display = 'block';
|
||||
this.isVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the control panel
|
||||
*/
|
||||
hide() {
|
||||
if (this.controlPanel) {
|
||||
this.controlPanel.style.display = 'none';
|
||||
this.isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status display (can be extended as needed)
|
||||
*/
|
||||
updateStatus(status) {
|
||||
// This method can be extended to show status information
|
||||
// For now, it just stores the status for potential display
|
||||
this.lastStatus = status;
|
||||
|
||||
// Could update a status indicator in the panel if needed
|
||||
if (status && this.controlPanel) {
|
||||
const title = this.controlPanel.querySelector('div');
|
||||
if (title) {
|
||||
const statusText = `Document Controls (${status.totalSections} sections, ${status.editingSections} editing)`;
|
||||
// Could update title or add status indicator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the control panel element
|
||||
*/
|
||||
getControlPanel() {
|
||||
return this.controlPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the control panel and clean up
|
||||
*/
|
||||
destroy() {
|
||||
if (this.controlPanel && this.controlPanel.parentNode) {
|
||||
this.controlPanel.parentNode.removeChild(this.controlPanel);
|
||||
}
|
||||
|
||||
// Clean up references
|
||||
this.controlPanel = null;
|
||||
this.buttons.clear();
|
||||
this.eventHandlers.clear();
|
||||
this.isVisible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the control panel is visible
|
||||
*/
|
||||
isVisible() {
|
||||
return this.isVisible && this.controlPanel && this.controlPanel.style.display !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all button IDs
|
||||
*/
|
||||
getButtonIds() {
|
||||
return Array.from(this.buttons.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific button by ID
|
||||
*/
|
||||
getButton(id) {
|
||||
return this.buttons.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DocumentControls };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DocumentControls = DocumentControls;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,168 +0,0 @@
|
||||
/**
|
||||
* Configuration Loader - Clean interface between Python and JavaScript
|
||||
*
|
||||
* This module provides the ONLY interface for Python-generated data.
|
||||
* All dynamic data from Python must be passed through this JSON configuration.
|
||||
*/
|
||||
|
||||
class MarkitectConfig {
|
||||
constructor() {
|
||||
this.config = null;
|
||||
this.loaded = false;
|
||||
|
||||
// Simple immediate loading - if script is loaded, DOM is ready
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
try {
|
||||
const configElement = document.getElementById('markitect-config');
|
||||
if (!configElement) {
|
||||
throw new Error('Markitect configuration not found - missing markitect-config script element');
|
||||
}
|
||||
|
||||
this.config = JSON.parse(configElement.textContent);
|
||||
this.loaded = true;
|
||||
console.log('✅ Markitect configuration loaded successfully');
|
||||
|
||||
// Validate required fields
|
||||
this.validateConfig();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load Markitect configuration:', error);
|
||||
this.config = this.getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
const required = ['markdownContent', 'mode'];
|
||||
const missing = required.filter(key => !(key in this.config));
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.warn('⚠️ Missing required config fields:', missing);
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
markdownContent: '# Default Content\n\nConfiguration failed to load.',
|
||||
markdownContentWithDogtag: '# Default Content\n\nConfiguration failed to load.',
|
||||
dogtagContent: '',
|
||||
mode: 'edit',
|
||||
theme: 'github',
|
||||
keyboardShortcuts: true,
|
||||
autosave: false,
|
||||
sections: true,
|
||||
originalFilename: 'document',
|
||||
version: 'Markitect v0.8.1',
|
||||
repoName: 'Markitect',
|
||||
base64References: {}
|
||||
};
|
||||
}
|
||||
|
||||
// Getter methods for clean access
|
||||
get markdownContent() {
|
||||
return this.config.markdownContent || '';
|
||||
}
|
||||
|
||||
get markdownContentWithDogtag() {
|
||||
return this.config.markdownContentWithDogtag || this.markdownContent;
|
||||
}
|
||||
|
||||
get dogtagContent() {
|
||||
return this.config.dogtagContent || '';
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.config.mode || 'edit';
|
||||
}
|
||||
|
||||
get isEditMode() {
|
||||
return this.mode === 'edit';
|
||||
}
|
||||
|
||||
get isInsertMode() {
|
||||
return this.mode === 'insert';
|
||||
}
|
||||
|
||||
get theme() {
|
||||
return this.config.theme || 'github';
|
||||
}
|
||||
|
||||
get originalFilename() {
|
||||
return this.config.originalFilename || 'document';
|
||||
}
|
||||
|
||||
get version() {
|
||||
return this.config.version || 'Markitect v0.8.1';
|
||||
}
|
||||
|
||||
get repoName() {
|
||||
return this.config.repoName || 'Markitect';
|
||||
}
|
||||
|
||||
get keyboardShortcuts() {
|
||||
return this.config.keyboardShortcuts !== false;
|
||||
}
|
||||
|
||||
get base64References() {
|
||||
return this.config.base64References || {};
|
||||
}
|
||||
|
||||
get restrictedHeadingLevels() {
|
||||
return this.config.restrictedHeadingLevels || [1, 2, 3];
|
||||
}
|
||||
|
||||
// Check if config is ready for access
|
||||
isReady() {
|
||||
return this.loaded && this.config !== null;
|
||||
}
|
||||
|
||||
// Wait for config to be ready
|
||||
waitForReady(callback, maxWait = 5000) {
|
||||
const startTime = Date.now();
|
||||
const checkReady = () => {
|
||||
if (this.isReady()) {
|
||||
callback();
|
||||
} else if (Date.now() - startTime < maxWait) {
|
||||
setTimeout(checkReady, 50);
|
||||
} else {
|
||||
console.error('❌ Configuration loading timeout after', maxWait, 'ms');
|
||||
callback(); // Call anyway with default config
|
||||
}
|
||||
};
|
||||
checkReady();
|
||||
}
|
||||
|
||||
// Get full editor configuration object
|
||||
getEditorConfig() {
|
||||
if (!this.isReady()) {
|
||||
console.warn('⚠️ Configuration not ready, using defaults');
|
||||
return this.getDefaultConfig();
|
||||
}
|
||||
|
||||
return {
|
||||
mode: this.mode,
|
||||
theme: this.theme,
|
||||
keyboardShortcuts: this.keyboardShortcuts,
|
||||
autosave: this.config.autosave || false,
|
||||
sections: this.config.sections !== false,
|
||||
originalFilename: this.originalFilename,
|
||||
version: this.version,
|
||||
repoName: this.repoName,
|
||||
restrictedHeadingLevels: this.restrictedHeadingLevels
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global configuration instance
|
||||
window.markitectConfig = new MarkitectConfig();
|
||||
|
||||
// Legacy compatibility - expose common config values globally
|
||||
window.editorConfig = window.markitectConfig.getEditorConfig();
|
||||
window.markitectBase64References = window.markitectConfig.base64References;
|
||||
|
||||
// Export for module use
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = MarkitectConfig;
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
/**
|
||||
* Independent Debug System for Markitect
|
||||
* Uses IndexedDB for persistence and provides selection-based filtering
|
||||
*/
|
||||
class MarkitectDebugSystem {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.messages = [];
|
||||
this.maxMessages = 1000;
|
||||
this.isEnabled = true;
|
||||
this.subscribers = [];
|
||||
|
||||
// Selection and filtering system
|
||||
this.selectionCriteria = {
|
||||
includeDocumentEvents: true,
|
||||
includeSystemEvents: false,
|
||||
includeControlEvents: true,
|
||||
includeEditingEvents: true,
|
||||
includeNavigationEvents: false,
|
||||
includedHeadings: new Set(), // Track which document headings to monitor
|
||||
excludedSources: new Set(['ContentsControl', 'DocumentNavigator'])
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Initialize IndexedDB for persistence
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('MarkitectDebugDB', 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
this.loadMessages().then(resolve);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
if (!db.objectStoreNames.contains('messages')) {
|
||||
const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true });
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
store.createIndex('category', 'category', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Add a debug message with selection filtering
|
||||
async addMessage(message, category = 'INFO', source = 'System', context = {}) {
|
||||
// Check if this message should be included based on selection criteria
|
||||
if (!this.shouldIncludeMessage(message, category, source, context)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messageObj = {
|
||||
timestamp: new Date().toISOString(),
|
||||
message: String(message),
|
||||
category: category.toUpperCase(),
|
||||
source: String(source),
|
||||
context: context || {},
|
||||
id: null // Will be set by IndexedDB
|
||||
};
|
||||
|
||||
// Store in IndexedDB if available
|
||||
if (this.db) {
|
||||
try {
|
||||
await this.saveMessage(messageObj);
|
||||
} catch (error) {
|
||||
console.warn('Failed to save debug message to IndexedDB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Store in memory
|
||||
this.messages.unshift(messageObj);
|
||||
|
||||
// Limit memory storage
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages = this.messages.slice(0, this.maxMessages);
|
||||
}
|
||||
|
||||
// Notify subscribers
|
||||
this.notifySubscribers(messageObj);
|
||||
|
||||
// Console output for development
|
||||
const consoleMethod = category.toLowerCase() === 'error' ? 'error' :
|
||||
category.toLowerCase() === 'warning' ? 'warn' : 'log';
|
||||
console[consoleMethod](`[${source}] ${message}`, context);
|
||||
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
// Selection filtering logic
|
||||
shouldIncludeMessage(message, category, source, context) {
|
||||
if (!this.isEnabled) return false;
|
||||
|
||||
const eventType = context.eventType || 'UNKNOWN';
|
||||
const criteria = this.selectionCriteria;
|
||||
|
||||
// Check event type filters
|
||||
switch (eventType.toUpperCase()) {
|
||||
case 'DOCUMENT':
|
||||
if (!criteria.includeDocumentEvents) return false;
|
||||
break;
|
||||
case 'SYSTEM':
|
||||
if (!criteria.includeSystemEvents) return false;
|
||||
break;
|
||||
case 'CONTROL':
|
||||
if (!criteria.includeControlEvents) return false;
|
||||
break;
|
||||
case 'EDITING':
|
||||
if (!criteria.includeEditingEvents) return false;
|
||||
break;
|
||||
case 'NAVIGATION':
|
||||
if (!criteria.includeNavigationEvents) return false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check excluded sources
|
||||
if (criteria.excludedSources.has(source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check heading-specific filtering
|
||||
if (context.sectionId && criteria.includedHeadings.size > 0) {
|
||||
const sectionElement = document.getElementById(context.sectionId);
|
||||
if (sectionElement) {
|
||||
const heading = sectionElement.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
if (heading && !criteria.includedHeadings.has(heading.textContent.trim())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save message to IndexedDB
|
||||
async saveMessage(messageObj) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['messages'], 'readwrite');
|
||||
const store = transaction.objectStore('messages');
|
||||
const request = store.add(messageObj);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load messages from IndexedDB
|
||||
async loadMessages() {
|
||||
if (!this.db) return [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['messages'], 'readonly');
|
||||
const store = transaction.objectStore('messages');
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.messages = request.result.reverse(); // Most recent first
|
||||
resolve(this.messages);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear all messages
|
||||
async clearMessages() {
|
||||
this.messages = [];
|
||||
|
||||
if (this.db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['messages'], 'readwrite');
|
||||
const store = transaction.objectStore('messages');
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get filtered messages
|
||||
getMessages(filter = {}) {
|
||||
let filteredMessages = [...this.messages];
|
||||
|
||||
if (filter.category) {
|
||||
filteredMessages = filteredMessages.filter(msg =>
|
||||
msg.category.toLowerCase() === filter.category.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.source) {
|
||||
filteredMessages = filteredMessages.filter(msg =>
|
||||
msg.source.toLowerCase().includes(filter.source.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.since) {
|
||||
const sinceDate = new Date(filter.since);
|
||||
filteredMessages = filteredMessages.filter(msg =>
|
||||
new Date(msg.timestamp) >= sinceDate
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.limit) {
|
||||
filteredMessages = filteredMessages.slice(0, filter.limit);
|
||||
}
|
||||
|
||||
return filteredMessages;
|
||||
}
|
||||
|
||||
// Update selection criteria
|
||||
updateSelectionCriteria(updates) {
|
||||
Object.assign(this.selectionCriteria, updates);
|
||||
this.notifySubscribers({ type: 'criteria-updated', criteria: this.selectionCriteria });
|
||||
}
|
||||
|
||||
// Add heading to monitoring
|
||||
addHeadingToMonitoring(headingText) {
|
||||
this.selectionCriteria.includedHeadings.add(headingText);
|
||||
}
|
||||
|
||||
// Remove heading from monitoring
|
||||
removeHeadingFromMonitoring(headingText) {
|
||||
this.selectionCriteria.includedHeadings.delete(headingText);
|
||||
}
|
||||
|
||||
// Scan document for available headings
|
||||
scanDocumentHeadings() {
|
||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
return Array.from(headings)
|
||||
.map(h => h.textContent.trim())
|
||||
.filter(text => text.length > 0 && !text.toLowerCase().includes('control'));
|
||||
}
|
||||
|
||||
// Subscribe to debug messages
|
||||
subscribe(callback) {
|
||||
this.subscribers.push(callback);
|
||||
return () => {
|
||||
const index = this.subscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.subscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify all subscribers
|
||||
notifySubscribers(message) {
|
||||
this.subscribers.forEach(callback => {
|
||||
try {
|
||||
callback(message);
|
||||
} catch (error) {
|
||||
console.error('Debug subscriber error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle debug system
|
||||
setEnabled(enabled) {
|
||||
this.isEnabled = enabled;
|
||||
this.addMessage(
|
||||
`Debug system ${enabled ? 'enabled' : 'disabled'}`,
|
||||
'INFO',
|
||||
'DebugSystem',
|
||||
{ eventType: 'SYSTEM' }
|
||||
);
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
const stats = {
|
||||
total: this.messages.length,
|
||||
byCategory: {},
|
||||
bySource: {},
|
||||
enabled: this.isEnabled,
|
||||
criteria: { ...this.selectionCriteria }
|
||||
};
|
||||
|
||||
this.messages.forEach(msg => {
|
||||
stats.byCategory[msg.category] = (stats.byCategory[msg.category] || 0) + 1;
|
||||
stats.bySource[msg.source] = (stats.bySource[msg.source] || 0) + 1;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and expose globally
|
||||
window.MarkitectDebugSystem = new MarkitectDebugSystem();
|
||||
@@ -1,544 +0,0 @@
|
||||
/**
|
||||
* SectionManager Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Manages the collection of sections and their state transitions.
|
||||
*
|
||||
* Dependencies:
|
||||
* - EditState enum (imported)
|
||||
* - SectionType enum (imported)
|
||||
* - Section class (imported)
|
||||
* - debug function (imported)
|
||||
*/
|
||||
|
||||
// Import dependencies - these will be separate modules
|
||||
const EditState = Object.freeze({
|
||||
ORIGINAL: 'original',
|
||||
EDITING: 'editing',
|
||||
MODIFIED: 'modified',
|
||||
SAVED: 'saved'
|
||||
});
|
||||
|
||||
const SectionType = Object.freeze({
|
||||
HEADING: 'heading',
|
||||
PARAGRAPH: 'paragraph',
|
||||
LIST: 'list',
|
||||
CODE: 'code',
|
||||
QUOTE: 'quote',
|
||||
TABLE: 'table',
|
||||
HR: 'hr',
|
||||
IMAGE: 'image'
|
||||
});
|
||||
|
||||
// Debug function (will be extracted to utils)
|
||||
function debug(message, category = 'INFO') {
|
||||
// Simple console debug for now - will be enhanced later
|
||||
console.log(`DEBUG ${category}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section Class - manages individual section state and content
|
||||
*/
|
||||
class Section {
|
||||
constructor(id, markdown, type) {
|
||||
this.id = id;
|
||||
this.originalMarkdown = markdown;
|
||||
this.currentMarkdown = markdown;
|
||||
this.editingMarkdown = markdown;
|
||||
this.pendingMarkdown = null;
|
||||
this.type = type;
|
||||
this.state = EditState.ORIGINAL;
|
||||
this.domElement = null;
|
||||
this.lastSaved = null;
|
||||
this.created = new Date();
|
||||
}
|
||||
|
||||
static generateId(markdown, position, strategy = 'hash', parentId = null) {
|
||||
return this.generateIdWithStrategy(markdown, position, strategy, parentId);
|
||||
}
|
||||
|
||||
static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) {
|
||||
const sanitizedContent = this.sanitizeContentForId(markdown);
|
||||
const normalizedContent = this.normalizeContentForHashing(sanitizedContent);
|
||||
const sectionType = this.detectType(markdown);
|
||||
|
||||
switch (strategy) {
|
||||
case 'timestamp':
|
||||
return this.generateTimestampId(normalizedContent, position, sectionType);
|
||||
case 'sequential':
|
||||
return this.generateSequentialId(normalizedContent, position, sectionType);
|
||||
case 'hierarchical':
|
||||
return this.generateHierarchicalId(normalizedContent, position, parentId);
|
||||
case 'hash':
|
||||
default:
|
||||
return this.generateAdvancedId(normalizedContent, position, sectionType);
|
||||
}
|
||||
}
|
||||
|
||||
static generateAdvancedId(content, position, sectionType) {
|
||||
const contentHash = this.generateCryptoHash(content);
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
const positionHex = position.toString(16).padStart(2, '0');
|
||||
|
||||
return `section-${typePrefix}-${contentHash}-${positionHex}`;
|
||||
}
|
||||
|
||||
static generateCryptoHash(content) {
|
||||
let hash = 0;
|
||||
if (content.length === 0) return '00000000';
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
|
||||
return hexHash.substring(0, 8);
|
||||
}
|
||||
|
||||
static normalizeContentForHashing(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
static sanitizeContentForId(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/[^\w\s\-_.#]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
|
||||
return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
|
||||
}
|
||||
|
||||
static generateSequentialId(content, position, sectionType = 'paragraph') {
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
const seqNumber = (position || 0).toString().padStart(3, '0');
|
||||
const contentHash = this.generateCryptoHash(content || '').substring(0, 4);
|
||||
|
||||
return `section-${typePrefix}-seq${seqNumber}-${contentHash}`;
|
||||
}
|
||||
|
||||
static generateHierarchicalId(content, position, parentId = null) {
|
||||
const contentHash = this.generateCryptoHash(content || '').substring(0, 6);
|
||||
|
||||
if (parentId) {
|
||||
const childIndex = (position || 0).toString().padStart(2, '0');
|
||||
return `${parentId}-child-${childIndex}-${contentHash}`;
|
||||
} else {
|
||||
return `section-root-${position || 0}-${contentHash}`;
|
||||
}
|
||||
}
|
||||
|
||||
static detectType(markdown) {
|
||||
if (!markdown || typeof markdown !== 'string') {
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
const content = markdown.replace(/^\n+|\n+$/g, '');
|
||||
if (!content) {
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
const trimmed = content.trim();
|
||||
|
||||
// Detection order matters - most specific first
|
||||
if (this.isHeading(trimmed)) {
|
||||
return SectionType.HEADING;
|
||||
}
|
||||
|
||||
if (this.isImage(trimmed)) {
|
||||
return SectionType.IMAGE;
|
||||
}
|
||||
|
||||
if (this.isCodeBlock(trimmed)) {
|
||||
return SectionType.CODE;
|
||||
}
|
||||
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
static isHeading(trimmed) {
|
||||
const headingPattern = /^#{1,6}\s+.+/;
|
||||
return headingPattern.test(trimmed);
|
||||
}
|
||||
|
||||
static isImage(trimmed) {
|
||||
const imagePattern = /!\[.*?\]\([^)]+\)/;
|
||||
return imagePattern.test(trimmed);
|
||||
}
|
||||
|
||||
static isCodeBlock(trimmed) {
|
||||
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes('```') || trimmed.includes('~~~')) {
|
||||
const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
|
||||
if (codeBlockPattern.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
if (this.state === EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is already being edited`);
|
||||
}
|
||||
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
|
||||
this.state = EditState.EDITING;
|
||||
return this.editingMarkdown;
|
||||
}
|
||||
|
||||
updateContent(markdown) {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.editingMarkdown = markdown;
|
||||
}
|
||||
|
||||
acceptChanges() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.currentMarkdown = this.editingMarkdown;
|
||||
this.editingMarkdown = null;
|
||||
this.pendingMarkdown = null;
|
||||
this.state = EditState.SAVED;
|
||||
this.lastSaved = new Date();
|
||||
return this.currentMarkdown;
|
||||
}
|
||||
|
||||
cancelChanges() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.editingMarkdown = null;
|
||||
if (this.pendingMarkdown !== null) {
|
||||
this.state = EditState.MODIFIED;
|
||||
return this.pendingMarkdown;
|
||||
} else if (this.lastSaved !== null) {
|
||||
this.state = EditState.SAVED;
|
||||
return this.currentMarkdown;
|
||||
} else {
|
||||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||||
return this.currentMarkdown;
|
||||
}
|
||||
}
|
||||
|
||||
stopEditing() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
|
||||
this.pendingMarkdown = this.editingMarkdown;
|
||||
this.state = EditState.MODIFIED;
|
||||
} else {
|
||||
this.pendingMarkdown = null;
|
||||
if (this.lastSaved !== null) {
|
||||
this.state = EditState.SAVED;
|
||||
} else {
|
||||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||||
}
|
||||
}
|
||||
|
||||
this.editingMarkdown = null;
|
||||
return this.state;
|
||||
}
|
||||
|
||||
resetToOriginal() {
|
||||
this.currentMarkdown = this.originalMarkdown;
|
||||
this.editingMarkdown = this.originalMarkdown;
|
||||
this.pendingMarkdown = null;
|
||||
this.state = EditState.ORIGINAL;
|
||||
return this.originalMarkdown;
|
||||
}
|
||||
|
||||
isEditing() {
|
||||
return this.state === EditState.EDITING;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return this.currentMarkdown !== this.originalMarkdown;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
id: this.id,
|
||||
state: this.state,
|
||||
hasChanges: this.hasChanges(),
|
||||
isEditing: this.isEditing(),
|
||||
contentLength: this.currentMarkdown.length,
|
||||
lastSaved: this.lastSaved,
|
||||
type: this.type,
|
||||
originalLength: this.originalMarkdown.length,
|
||||
currentLength: this.currentMarkdown.length
|
||||
};
|
||||
}
|
||||
|
||||
isImage() {
|
||||
return this.type === SectionType.IMAGE;
|
||||
}
|
||||
|
||||
redetectType(content = null) {
|
||||
const markdown = content || this.currentMarkdown;
|
||||
const oldType = this.type;
|
||||
this.type = Section.detectType(markdown);
|
||||
|
||||
if (oldType !== this.type) {
|
||||
debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
|
||||
}
|
||||
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SectionManager - Manages the collection of sections
|
||||
*/
|
||||
class SectionManager {
|
||||
constructor() {
|
||||
this.sections = new Map();
|
||||
this.listeners = new Map();
|
||||
this.statusInterval = null;
|
||||
this.lastStatusUpdate = new Date().toISOString();
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
createSectionsFromMarkdown(markdownContent) {
|
||||
// Split content into blocks separated by double newlines
|
||||
const blocks = markdownContent.split(/\n\s*\n/);
|
||||
const sections = [];
|
||||
let position = 0;
|
||||
|
||||
for (const block of blocks) {
|
||||
const trimmedBlock = block.trim();
|
||||
if (!trimmedBlock) continue;
|
||||
|
||||
// Check if this block should be split further
|
||||
const lines = trimmedBlock.split('\n');
|
||||
let currentSection = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const isHeading = /^#{1,6}\s/.test(line.trim());
|
||||
const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
|
||||
|
||||
// Each heading or image starts a new section
|
||||
if ((isHeading || isImage) && currentSection.trim()) {
|
||||
// Save the previous section
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
currentSection = line;
|
||||
} else {
|
||||
if (currentSection) currentSection += '\n';
|
||||
currentSection += line;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the final section from this block
|
||||
if (currentSection.trim()) {
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('sections-created', { sections, count: sections.length });
|
||||
return sections;
|
||||
}
|
||||
|
||||
startEditing(sectionId) {
|
||||
debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
|
||||
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
if (section.isEditing()) {
|
||||
debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
|
||||
return section.editingMarkdown;
|
||||
}
|
||||
|
||||
debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
|
||||
const content = section.startEdit();
|
||||
|
||||
debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
|
||||
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
||||
debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
updateContent(sectionId, markdown) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const oldType = section.type;
|
||||
section.updateContent(markdown);
|
||||
const newType = section.redetectType(markdown);
|
||||
|
||||
const eventData = {
|
||||
sectionId,
|
||||
markdown,
|
||||
section: section.getStatus(),
|
||||
typeChanged: oldType !== newType,
|
||||
oldType,
|
||||
newType
|
||||
};
|
||||
|
||||
this.emit('content-updated', eventData);
|
||||
|
||||
if (oldType !== newType) {
|
||||
this.emit('section-type-changed', {
|
||||
sectionId,
|
||||
oldType,
|
||||
newType,
|
||||
section: section.getStatus()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
acceptChanges(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.acceptChanges();
|
||||
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
cancelChanges(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.cancelChanges();
|
||||
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
resetSection(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.resetToOriginal();
|
||||
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
getDocumentMarkdown() {
|
||||
const sortedSections = Array.from(this.sections.values())
|
||||
.sort((a, b) => a.created - b.created);
|
||||
|
||||
return sortedSections.map(section => section.currentMarkdown).join('\n\n');
|
||||
}
|
||||
|
||||
getAllSections() {
|
||||
return Array.from(this.sections.values());
|
||||
}
|
||||
|
||||
getDocumentStatus() {
|
||||
const sections = Array.from(this.sections.values());
|
||||
const editingSections = sections.filter(section => section.isEditing).length;
|
||||
|
||||
return {
|
||||
totalSections: sections.length,
|
||||
editingSections: editingSections
|
||||
};
|
||||
}
|
||||
|
||||
extractHeadings(content) {
|
||||
if (!content) return [];
|
||||
const lines = content.split('\n');
|
||||
return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
|
||||
}
|
||||
|
||||
handleSectionSplit(sectionId, newContent) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
// Remove the original section
|
||||
this.sections.delete(sectionId);
|
||||
|
||||
// Create new sections from the content
|
||||
const newSections = this.createSectionsFromMarkdown(newContent);
|
||||
|
||||
// Emit section-split event
|
||||
this.emit('section-split', {
|
||||
originalSectionId: sectionId,
|
||||
newSections: newSections,
|
||||
count: newSections.length
|
||||
});
|
||||
|
||||
return newSections;
|
||||
}
|
||||
|
||||
createSectionsFromContent(content) {
|
||||
return this.createSectionsFromMarkdown(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SectionManager, Section, EditState, SectionType };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SectionManager = SectionManager;
|
||||
window.Section = Section;
|
||||
window.EditState = EditState;
|
||||
window.SectionType = SectionType;
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/**
|
||||
* Main Markitect JavaScript Entry Point - Clean Architecture Version
|
||||
*
|
||||
* Uses ONLY the JSON configuration interface - NO Python-generated JavaScript!
|
||||
* Initializes all controls and systems when document is ready
|
||||
* Implements graceful degradation for missing dependencies
|
||||
*/
|
||||
|
||||
// Main application module
|
||||
const MarkitectMain = {
|
||||
initialized: false,
|
||||
config: null,
|
||||
|
||||
// Initialize the complete application
|
||||
initialize: function() {
|
||||
if (this.initialized) {
|
||||
console.log('⚠️ MarkitectMain already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🚀 MarkitectMain initializing...');
|
||||
|
||||
try {
|
||||
// Get configuration - if not loaded, use defaults
|
||||
this.config = window.markitectConfig;
|
||||
if (!this.config || !this.config.loaded) {
|
||||
console.warn('⚠️ Configuration not loaded, proceeding with defaults');
|
||||
this.config = {
|
||||
markdownContent: document.querySelector('#markdown-content')?.textContent || '',
|
||||
mode: 'edit',
|
||||
theme: 'github'
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize core systems
|
||||
this.initializeCoreComponents();
|
||||
this.initializeControlPanels();
|
||||
this.setupEventHandlers();
|
||||
this.renderContent();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('✅ MarkitectMain initialization complete');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ MarkitectMain initialization failed:', error);
|
||||
this.fallbackMode();
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize core modular components
|
||||
initializeCoreComponents: function() {
|
||||
console.log('🔧 Initializing core components...');
|
||||
|
||||
const container = document.getElementById('markdown-content') || document.body;
|
||||
|
||||
// Initialize section manager
|
||||
if (typeof SectionManager !== 'undefined') {
|
||||
this.sectionManager = new SectionManager();
|
||||
console.log('✅ SectionManager initialized');
|
||||
} else {
|
||||
throw new Error('SectionManager not available');
|
||||
}
|
||||
|
||||
// Initialize DOM renderer
|
||||
if (typeof DOMRenderer !== 'undefined') {
|
||||
this.domRenderer = new DOMRenderer(this.sectionManager, container);
|
||||
console.log('✅ DOMRenderer initialized');
|
||||
} else {
|
||||
throw new Error('DOMRenderer not available');
|
||||
}
|
||||
|
||||
// Initialize debug panel
|
||||
if (typeof DebugPanel !== 'undefined') {
|
||||
this.debugPanel = new DebugPanel();
|
||||
console.log('✅ DebugPanel initialized');
|
||||
}
|
||||
|
||||
// Legacy DocumentControls removed - functionality now in enhanced control panels
|
||||
},
|
||||
|
||||
// Initialize enhanced control panels with compass positioning
|
||||
initializeControlPanels: function() {
|
||||
console.log('🎛️ Initializing enhanced control panels with compass positioning...');
|
||||
|
||||
// ContentsControl (West)
|
||||
if (typeof ContentsControl !== 'undefined') {
|
||||
this.contentsControl = new ContentsControl();
|
||||
this.contentsControl.config.position = 'w';
|
||||
this.contentsControl.show();
|
||||
window.contentsControl = this.contentsControl;
|
||||
console.log('✅ ContentsControl initialized (West) with enhanced ControlBase');
|
||||
}
|
||||
|
||||
// StatusControl (East)
|
||||
if (typeof StatusControl !== 'undefined') {
|
||||
this.statusControl = new StatusControl();
|
||||
this.statusControl.config.position = 'e';
|
||||
this.statusControl.show();
|
||||
window.statusControl = this.statusControl;
|
||||
console.log('✅ StatusControl initialized (East) with enhanced ControlBase');
|
||||
}
|
||||
|
||||
// DebugControl (Southeast)
|
||||
if (typeof DebugControl !== 'undefined') {
|
||||
this.debugControl = new DebugControl();
|
||||
this.debugControl.config.position = 'se';
|
||||
this.debugControl.show();
|
||||
window.debugControl = this.debugControl;
|
||||
console.log('✅ DebugControl initialized (Southeast) with enhanced ControlBase');
|
||||
}
|
||||
|
||||
// EditControl (Northeast)
|
||||
if (typeof EditControl !== 'undefined') {
|
||||
this.editControl = new EditControl();
|
||||
this.editControl.config.position = 'ne';
|
||||
this.editControl.show();
|
||||
window.editControl = this.editControl;
|
||||
console.log('✅ EditControl initialized (Northeast) with enhanced ControlBase');
|
||||
}
|
||||
},
|
||||
|
||||
// Setup core event handlers (enhanced control panels handle their own events)
|
||||
setupEventHandlers: function() {
|
||||
console.log('🔌 Setting up core event handlers...');
|
||||
|
||||
// Setup section manager event handlers for debug panel
|
||||
if (this.sectionManager && this.debugPanel) {
|
||||
this.sectionManager.on('sections-created', (data) => {
|
||||
this.debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
|
||||
});
|
||||
|
||||
this.sectionManager.on('edit-started', (data) => {
|
||||
this.debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
|
||||
});
|
||||
|
||||
this.sectionManager.on('changes-accepted', (data) => {
|
||||
this.debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS');
|
||||
this.updateSectionDOM(data.sectionId);
|
||||
});
|
||||
|
||||
this.sectionManager.on('changes-cancelled', (data) => {
|
||||
this.debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING');
|
||||
});
|
||||
}
|
||||
|
||||
// Make core components available globally for enhanced controls
|
||||
window.sectionManager = this.sectionManager;
|
||||
window.domRenderer = this.domRenderer;
|
||||
window.debugPanel = this.debugPanel;
|
||||
|
||||
console.log('✅ Core event handlers and global references set up');
|
||||
},
|
||||
|
||||
// Render content using the configuration
|
||||
renderContent: function() {
|
||||
console.log('📄 Rendering markdown content...');
|
||||
|
||||
const markdownToRender = this.config.markdownContent || '';
|
||||
if (markdownToRender.trim()) {
|
||||
const sections = this.sectionManager.createSectionsFromMarkdown(markdownToRender);
|
||||
this.domRenderer.renderAllSections(sections);
|
||||
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO');
|
||||
}
|
||||
console.log(`✅ Rendered ${sections.length} sections`);
|
||||
} else {
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage('No markdown content to initialize', 'WARNING');
|
||||
}
|
||||
console.warn('⚠️ No markdown content to render');
|
||||
}
|
||||
},
|
||||
|
||||
// Update section DOM after changes
|
||||
updateSectionDOM: function(sectionId) {
|
||||
try {
|
||||
const section = this.sectionManager.sections.get(sectionId);
|
||||
if (section) {
|
||||
const sectionElement = this.domRenderer.findSectionElement(sectionId);
|
||||
if (sectionElement) {
|
||||
const newElement = this.domRenderer.renderSection(section);
|
||||
sectionElement.parentNode.replaceChild(newElement, sectionElement);
|
||||
|
||||
if (this.debugPanel) {
|
||||
this.debugPanel.addMessage(`DOM updated for section: ${sectionId}`, 'INFO');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to update section DOM:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Fallback mode if initialization fails
|
||||
fallbackMode: function() {
|
||||
console.warn('⚠️ Running in fallback mode');
|
||||
|
||||
// Basic content rendering fallback
|
||||
const contentDiv = document.getElementById('markdown-content');
|
||||
if (contentDiv && this.config && this.config.markdownContent) {
|
||||
const basicHtml = this.config.markdownContent
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
contentDiv.innerHTML = `<p>${basicHtml}</p>`;
|
||||
console.log('✅ Fallback content rendered');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Make components globally available for debugging
|
||||
window.MarkitectMain = MarkitectMain;
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Small delay to ensure config is loaded
|
||||
setTimeout(() => MarkitectMain.initialize(), 100);
|
||||
});
|
||||
} else {
|
||||
// DOM already ready
|
||||
setTimeout(() => MarkitectMain.initialize(), 100);
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
/**
|
||||
* Main Markitect JavaScript Entry Point
|
||||
* Initializes all controls and systems when document is ready
|
||||
* Implements graceful degradation for missing dependencies
|
||||
* Supports Fail Fast strict mode for development
|
||||
*/
|
||||
|
||||
// Development mode detection
|
||||
const MARKITECT_STRICT_MODE = (
|
||||
window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.search.includes('strict=true') ||
|
||||
window.markitectStrictMode === true
|
||||
);
|
||||
|
||||
// Utility functions for safe initialization
|
||||
const MarkitectMain = {
|
||||
// Safe dependency checking with timeout
|
||||
checkDependencies: function() {
|
||||
const dependencies = {
|
||||
debugSystem: !!window.MarkitectDebugSystem,
|
||||
control: !!window.Control,
|
||||
statusControl: !!window.StatusControl,
|
||||
debugControl: !!window.DebugControl,
|
||||
contentsControl: !!window.ContentsControl,
|
||||
editControl: !!window.EditControl
|
||||
};
|
||||
|
||||
console.log('📋 Dependency check results:', dependencies);
|
||||
return dependencies;
|
||||
},
|
||||
|
||||
// Safe logging that works even without debug system
|
||||
safeLog: function(message, level = 'INFO', component = 'Main', data = {}) {
|
||||
console.log(`[${level}] ${component}: ${message}`);
|
||||
|
||||
// In strict mode, throw on errors for immediate development feedback
|
||||
if (MARKITECT_STRICT_MODE && level === 'ERROR') {
|
||||
console.error(`🚨 STRICT MODE: Throwing error for immediate diagnosis`);
|
||||
throw new Error(`${component}: ${message}`);
|
||||
}
|
||||
|
||||
// Try to use debug system if available
|
||||
if (window.MarkitectDebugSystem && window.MarkitectDebugSystem.addMessage) {
|
||||
try {
|
||||
window.MarkitectDebugSystem.addMessage(message, level, component, { ...data, eventType: 'SYSTEM' });
|
||||
} catch (error) {
|
||||
console.warn('Debug system logging failed:', error);
|
||||
if (MARKITECT_STRICT_MODE) {
|
||||
throw error; // Fail fast in development
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Safe control initialization with fallbacks
|
||||
initializeControl: function(controlClass, controlName, icon = '🔧') {
|
||||
const timeout = setTimeout(() => {
|
||||
const message = `${controlName} initialization timed out`;
|
||||
console.warn(message);
|
||||
if (MARKITECT_STRICT_MODE) {
|
||||
throw new Error(message); // Fail fast in development
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
if (!controlClass) {
|
||||
const message = `${controlName} class not available, skipping`;
|
||||
this.safeLog(message, MARKITECT_STRICT_MODE ? 'ERROR' : 'WARNING');
|
||||
clearTimeout(timeout);
|
||||
return null;
|
||||
}
|
||||
|
||||
const controlInstance = new controlClass();
|
||||
if (!controlInstance || typeof controlInstance.createControl !== 'function') {
|
||||
throw new Error(`Invalid ${controlName} instance`);
|
||||
}
|
||||
|
||||
const element = controlInstance.createControl();
|
||||
if (!element) {
|
||||
throw new Error(`${controlName} failed to create element`);
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
this.safeLog(`${controlName} initialized successfully`, 'SUCCESS');
|
||||
return controlInstance;
|
||||
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
this.safeLog(`${controlName} initialization failed: ${error.message}`, 'ERROR');
|
||||
|
||||
// Create minimal fallback control if core Control class exists
|
||||
if (window.Control && controlName === 'StatusControl') {
|
||||
return this.createFallbackControl(controlName, icon);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Create minimal fallback control for essential controls
|
||||
createFallbackControl: function(name, icon) {
|
||||
try {
|
||||
const fallback = Object.create(window.Control);
|
||||
fallback.config = {
|
||||
icon: icon,
|
||||
title: `${name} (Fallback)`,
|
||||
className: `${name.toLowerCase()}-fallback`,
|
||||
defaultContent: `${name} is running in fallback mode due to initialization issues.`,
|
||||
ariaLabel: `${name} Fallback Control`,
|
||||
position: 'e'
|
||||
};
|
||||
|
||||
const element = fallback.createControl();
|
||||
if (element) {
|
||||
this.safeLog(`${name} fallback control created`, 'INFO');
|
||||
return { control: fallback };
|
||||
}
|
||||
} catch (error) {
|
||||
this.safeLog(`Fallback control creation failed: ${error.message}`, 'ERROR');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Main initialization with comprehensive error handling
|
||||
initialize: function() {
|
||||
this.safeLog('🚀 Initializing Markitect controls and systems...', 'INFO');
|
||||
|
||||
// Check dependencies first
|
||||
const deps = this.checkDependencies();
|
||||
|
||||
if (!deps.control) {
|
||||
this.safeLog('❌ Core Control system not available, cannot initialize UI controls', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
const initializedControls = {};
|
||||
let successCount = 0;
|
||||
let totalAttempts = 0;
|
||||
|
||||
// Initialize controls with graceful degradation
|
||||
const controlsToInit = [
|
||||
{ class: window.StatusControl, name: 'StatusControl', key: 'statusControl', icon: '📊', essential: true },
|
||||
{ class: window.DebugControl, name: 'DebugControl', key: 'debugControl', icon: '🪲', essential: false },
|
||||
{ class: window.ContentsControl, name: 'ContentsControl', key: 'contentsControl', icon: '☰', essential: false },
|
||||
{ class: window.EditControl, name: 'EditControl', key: 'editControl', icon: '✏️', essential: false }
|
||||
];
|
||||
|
||||
controlsToInit.forEach(({ class: controlClass, name, key, icon, essential }) => {
|
||||
totalAttempts++;
|
||||
const instance = this.initializeControl(controlClass, name, icon);
|
||||
|
||||
if (instance) {
|
||||
initializedControls[key] = instance.control || instance;
|
||||
window[key] = initializedControls[key];
|
||||
successCount++;
|
||||
} else if (essential) {
|
||||
this.safeLog(`Essential control ${name} failed to initialize`, 'ERROR');
|
||||
}
|
||||
});
|
||||
|
||||
// Report initialization results
|
||||
const successRate = Math.round((successCount / totalAttempts) * 100);
|
||||
if (successCount === totalAttempts) {
|
||||
this.safeLog('✅ All controls initialized successfully', 'SUCCESS');
|
||||
} else if (successCount > 0) {
|
||||
this.safeLog(`⚠️ Partial initialization: ${successCount}/${totalAttempts} controls (${successRate}%) initialized`, 'WARNING');
|
||||
} else {
|
||||
this.safeLog('❌ No controls could be initialized', 'ERROR');
|
||||
}
|
||||
|
||||
// Set up global error handlers for runtime protection
|
||||
this.setupErrorHandlers();
|
||||
|
||||
this.safeLog(`✅ Markitect initialization complete (${successCount}/${totalAttempts} controls active)`, 'INFO');
|
||||
},
|
||||
|
||||
// Set up global error handlers
|
||||
setupErrorHandlers: function() {
|
||||
// Catch unhandled errors
|
||||
window.addEventListener('error', (event) => {
|
||||
this.safeLog(`Unhandled error: ${event.message} at ${event.filename}:${event.lineno}`, 'ERROR');
|
||||
});
|
||||
|
||||
// Catch unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
this.safeLog(`Unhandled promise rejection: ${event.reason}`, 'ERROR');
|
||||
event.preventDefault(); // Prevent console spam
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready with additional safety
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => MarkitectMain.initialize(), 100); // Brief delay for dependencies
|
||||
});
|
||||
} else {
|
||||
// DOM already loaded
|
||||
setTimeout(() => MarkitectMain.initialize(), 100);
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
/**
|
||||
* DocumentNavigator Plugin Definition
|
||||
*
|
||||
* Plugin definition for the Substack-style document navigation widget.
|
||||
* Provides floating table of contents with smooth scrolling and scroll spy.
|
||||
*/
|
||||
export default {
|
||||
name: 'DocumentNavigator',
|
||||
version: '1.0.0',
|
||||
description: 'Substack-style floating document navigation with table of contents',
|
||||
author: 'Markitect Core',
|
||||
category: 'navigation',
|
||||
|
||||
// Dependencies that must be loaded first
|
||||
dependencies: ['UIWidget'],
|
||||
|
||||
// Mixins to apply (none required for this widget)
|
||||
mixins: [],
|
||||
|
||||
// Lazy load the actual widget class
|
||||
async load() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
return DocumentNavigator;
|
||||
},
|
||||
|
||||
// Default configuration
|
||||
defaultOptions: {
|
||||
position: 'left', // 'left' or 'right' side
|
||||
collapsed: true, // Start in collapsed state
|
||||
autoHide: true, // Hide on mobile devices
|
||||
maxHeadingLevel: 3, // Include H1, H2, H3
|
||||
enableScrollSpy: true, // Highlight current section
|
||||
smoothScroll: true, // Smooth scroll to headings
|
||||
animationDuration: 300, // Animation timing in ms
|
||||
minHeadings: 2, // Minimum headings to show widget
|
||||
theme: 'default', // Theme variant
|
||||
|
||||
// Layout options
|
||||
width: '280px', // Expanded width
|
||||
collapsedWidth: '40px', // Collapsed width
|
||||
offset: { // Position offset
|
||||
top: '80px',
|
||||
side: '20px'
|
||||
},
|
||||
|
||||
// Accessibility
|
||||
enableKeyboard: true, // Keyboard navigation support
|
||||
ariaLabel: 'Document Navigation'
|
||||
},
|
||||
|
||||
// Plugin lifecycle hooks
|
||||
async onLoad(instance, options) {
|
||||
console.log('DocumentNavigator plugin loaded:', {
|
||||
headings: instance.headings.length,
|
||||
position: options.position,
|
||||
collapsed: options.collapsed
|
||||
});
|
||||
|
||||
// Auto-initialize after load
|
||||
await instance.initialize();
|
||||
|
||||
return instance;
|
||||
},
|
||||
|
||||
async onUnload(instance) {
|
||||
console.log('DocumentNavigator plugin unloading');
|
||||
await instance.destroy();
|
||||
},
|
||||
|
||||
// Feature flags and capabilities
|
||||
capabilities: {
|
||||
draggable: false, // Not draggable (fixed position)
|
||||
resizable: false, // Not resizable (fixed width)
|
||||
themeable: true, // Supports themes
|
||||
persistent: false, // Rebuilds on page changes
|
||||
responsive: true, // Responsive behavior
|
||||
keyboard: true, // Keyboard accessible
|
||||
scrollSpy: true, // Scroll spy functionality
|
||||
smoothScroll: true // Smooth scroll navigation
|
||||
},
|
||||
|
||||
// Integration requirements
|
||||
requirements: {
|
||||
container: true, // Requires container element
|
||||
headings: true, // Requires document headings
|
||||
scrollable: true // Requires scrollable content
|
||||
},
|
||||
|
||||
// Event types emitted by this widget
|
||||
events: [
|
||||
'rendered', // Widget rendered to DOM
|
||||
'navigate', // User navigated to heading
|
||||
'toggle', // Widget expanded/collapsed
|
||||
'theme-changed', // Theme was changed
|
||||
'destroyed' // Widget was destroyed
|
||||
],
|
||||
|
||||
// CSS classes used by this widget
|
||||
cssClasses: [
|
||||
'document-navigator', // Main widget class
|
||||
'navigator-toggle', // Toggle button
|
||||
'navigator-list', // Navigation list
|
||||
'navigator-item', // Navigation items
|
||||
'navigator-link', // Navigation links
|
||||
'navigator-header', // List header
|
||||
'navigator-close', // Close button
|
||||
'navigator-empty' // Empty state
|
||||
],
|
||||
|
||||
// Theme variants
|
||||
themes: {
|
||||
default: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#e1e5e9',
|
||||
textColor: '#333',
|
||||
activeColor: '#1976d2',
|
||||
activeBackground: '#e3f2fd'
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: 'rgba(45, 45, 45, 0.95)',
|
||||
borderColor: '#555',
|
||||
textColor: '#e0e0e0',
|
||||
activeColor: '#64b5f6',
|
||||
activeBackground: '#1e3a8a'
|
||||
},
|
||||
minimal: {
|
||||
backgroundColor: 'rgba(248, 249, 250, 0.90)',
|
||||
borderColor: '#dee2e6',
|
||||
textColor: '#495057',
|
||||
activeColor: '#007bff',
|
||||
activeBackground: '#e7f1ff'
|
||||
}
|
||||
},
|
||||
|
||||
// Usage examples
|
||||
examples: {
|
||||
basic: {
|
||||
description: 'Basic document navigator on the left side',
|
||||
code: `
|
||||
const navigator = await widgetSystem.createWidget('DocumentNavigator');
|
||||
await navigator.show();
|
||||
`
|
||||
},
|
||||
customized: {
|
||||
description: 'Customized navigator with specific options',
|
||||
code: `
|
||||
const navigator = await widgetSystem.createWidget('DocumentNavigator', {
|
||||
position: 'right',
|
||||
collapsed: false,
|
||||
maxHeadingLevel: 4,
|
||||
theme: 'dark'
|
||||
});
|
||||
await navigator.show();
|
||||
`
|
||||
},
|
||||
withContainer: {
|
||||
description: 'Navigator for specific container content',
|
||||
code: `
|
||||
const container = document.getElementById('article-content');
|
||||
const navigator = await widgetSystem.createWidget('DocumentNavigator', {
|
||||
container: container,
|
||||
minHeadings: 1
|
||||
});
|
||||
await navigator.show();
|
||||
`
|
||||
}
|
||||
},
|
||||
|
||||
// Development and testing helpers
|
||||
dev: {
|
||||
testHeadingStructure() {
|
||||
// Helper to create test content with headings
|
||||
const testContent = `
|
||||
<h1>Chapter 1: Introduction</h1>
|
||||
<p>Lorem ipsum content...</p>
|
||||
<h2>Section 1.1: Overview</h2>
|
||||
<h3>Subsection 1.1.1: Details</h3>
|
||||
<h2>Section 1.2: Implementation</h2>
|
||||
<h1>Chapter 2: Advanced Topics</h1>
|
||||
<h2>Section 2.1: Performance</h2>
|
||||
`;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = testContent;
|
||||
container.style.cssText = 'height: 2000px; padding: 2rem;';
|
||||
document.body.appendChild(container);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
async createTestInstance(options = {}) {
|
||||
// Helper to create test instance with sample content
|
||||
const container = this.testHeadingStructure();
|
||||
|
||||
const navigator = new (await this.load())({
|
||||
container,
|
||||
collapsed: false,
|
||||
...options
|
||||
});
|
||||
|
||||
await navigator.initialize();
|
||||
await navigator.render();
|
||||
|
||||
return { navigator, container };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test Runner for JavaScript Refactoring
|
||||
*
|
||||
* Drives component extraction and testing during architecture refactoring.
|
||||
* Ensures all functionality remains stable while achieving separation of concerns.
|
||||
*/
|
||||
|
||||
class RefactorTestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
this.currentSuite = null;
|
||||
this.setupDOM();
|
||||
}
|
||||
|
||||
setupDOM() {
|
||||
// Set up minimal DOM environment for testing
|
||||
if (typeof document === 'undefined') {
|
||||
const { JSDOM } = require('jsdom');
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
||||
url: 'http://localhost',
|
||||
pretendToBeVisual: true,
|
||||
resources: 'usable'
|
||||
});
|
||||
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
global.HTMLElement = dom.window.HTMLElement;
|
||||
global.Event = dom.window.Event;
|
||||
global.CustomEvent = dom.window.CustomEvent;
|
||||
|
||||
// Only set navigator if it doesn't exist
|
||||
if (typeof global.navigator === 'undefined') {
|
||||
global.navigator = dom.window.navigator;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe(suiteName, fn) {
|
||||
console.log(`\n📁 ${suiteName}`);
|
||||
this.currentSuite = suiteName;
|
||||
fn();
|
||||
this.currentSuite = null;
|
||||
}
|
||||
|
||||
it(testName, fn) {
|
||||
const fullName = this.currentSuite ? `${this.currentSuite}: ${testName}` : testName;
|
||||
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✅ ${testName}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.log(` ❌ ${testName}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.log(` Stack: ${error.stack.split('\n')[1]?.trim()}`);
|
||||
}
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
expect(actual) {
|
||||
return {
|
||||
toBe: (expected) => {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
},
|
||||
toBeTruthy: () => {
|
||||
if (!actual) {
|
||||
throw new Error(`Expected truthy value, got ${actual}`);
|
||||
}
|
||||
},
|
||||
toBeFalsy: () => {
|
||||
if (actual) {
|
||||
throw new Error(`Expected falsy value, got ${actual}`);
|
||||
}
|
||||
},
|
||||
toEqual: (expected) => {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||||
}
|
||||
},
|
||||
toContain: (expected) => {
|
||||
if (!actual.includes(expected)) {
|
||||
throw new Error(`Expected ${actual} to contain ${expected}`);
|
||||
}
|
||||
},
|
||||
toHaveProperty: (property) => {
|
||||
if (!(property in actual)) {
|
||||
throw new Error(`Expected object to have property ${property}`);
|
||||
}
|
||||
},
|
||||
toBeInstanceOf: (expectedClass) => {
|
||||
if (!(actual instanceof expectedClass)) {
|
||||
throw new Error(`Expected instance of ${expectedClass.name}, got ${actual.constructor.name}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a component can be extracted from the monolith without breaking functionality
|
||||
*/
|
||||
testComponentExtraction(componentName, extractFn, originalTests) {
|
||||
this.describe(`Component Extraction: ${componentName}`, () => {
|
||||
this.it('should extract without syntax errors', () => {
|
||||
try {
|
||||
const component = extractFn();
|
||||
this.expect(component).toBeTruthy();
|
||||
} catch (error) {
|
||||
throw new Error(`Component extraction failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.it('should maintain original API', () => {
|
||||
const component = extractFn();
|
||||
originalTests.forEach(test => {
|
||||
try {
|
||||
test(component);
|
||||
} catch (error) {
|
||||
throw new Error(`API compatibility test failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test component integration after extraction
|
||||
*/
|
||||
testComponentIntegration(components, integrationTests) {
|
||||
this.describe('Component Integration', () => {
|
||||
integrationTests.forEach((test, index) => {
|
||||
this.it(`integration test ${index + 1}`, () => {
|
||||
test(components);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup test environment with mock dependencies
|
||||
*/
|
||||
setupTestEnvironment() {
|
||||
// Create test container
|
||||
const container = document.createElement('div');
|
||||
container.id = 'test-container';
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Mock any global dependencies
|
||||
global.mockSectionManager = {
|
||||
sections: new Map(),
|
||||
createSectionsFromMarkdown: () => [],
|
||||
startEditing: () => true,
|
||||
stopEditing: () => true,
|
||||
getAllSections: () => []
|
||||
};
|
||||
|
||||
return { container };
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup test environment
|
||||
*/
|
||||
cleanupTestEnvironment() {
|
||||
const container = document.getElementById('test-container');
|
||||
if (container) {
|
||||
container.remove();
|
||||
}
|
||||
|
||||
// Clear any global mocks
|
||||
delete global.mockSectionManager;
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('🧪 TDD Refactoring Test Runner Starting...\n');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Run all collected tests
|
||||
// Tests will be added by importing component test files
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
console.log(`\n📊 Test Results:`);
|
||||
console.log(` ✅ Passed: ${this.passed}`);
|
||||
console.log(` ❌ Failed: ${this.failed}`);
|
||||
console.log(` ⏱️ Duration: ${duration}ms`);
|
||||
|
||||
if (this.failed > 0) {
|
||||
console.log(`\n❌ ${this.failed} test(s) failed. Refactoring should not proceed.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`\n✅ All tests passed! Refactoring is safe to continue.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in component tests
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { RefactorTestRunner };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.RefactorTestRunner = RefactorTestRunner;
|
||||
}
|
||||
|
||||
module.exports = RefactorTestRunner;
|
||||
@@ -1,521 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Comprehensive Component Integration Test
|
||||
*
|
||||
* Tests that extracted components work together properly.
|
||||
* Verifies the complete workflow: Section Creation → Rendering → Editing → Saving
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Component Integration Tests', () => {
|
||||
|
||||
runner.it('should load all extracted components', () => {
|
||||
try {
|
||||
// Load extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
|
||||
runner.expect(sectionModule.SectionManager).toBeTruthy();
|
||||
runner.expect(sectionModule.Section).toBeTruthy();
|
||||
runner.expect(domModule.DOMRenderer).toBeTruthy();
|
||||
runner.expect(domModule.FloatingMenu).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = sectionModule.SectionManager;
|
||||
global.ExtractedSection = sectionModule.Section;
|
||||
global.ExtractedDOMRenderer = domModule.DOMRenderer;
|
||||
global.ExtractedFloatingMenu = domModule.FloatingMenu;
|
||||
global.ExtractedEditState = sectionModule.EditState;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted components: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support complete section creation workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test workflow: Create sections from markdown
|
||||
const testMarkdown = `# Main Heading
|
||||
This is the introduction content.
|
||||
|
||||
## Subheading One
|
||||
Content for first subsection.
|
||||
|
||||

|
||||
|
||||
## Subheading Two
|
||||
Content for second subsection.`;
|
||||
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
|
||||
// Verify sections were created
|
||||
// Expected: heading+paragraph, heading+paragraph, image, heading+paragraph = 4 sections
|
||||
runner.expect(sections.length).toBe(4);
|
||||
runner.expect(sections[0].type).toBe('heading');
|
||||
runner.expect(sections[2].type).toBe('image');
|
||||
|
||||
// Verify DOM rendering
|
||||
domRenderer.renderAllSections(sections);
|
||||
const renderedElements = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedElements.length).toBe(sections.length);
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support complete editing workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const EditState = global.ExtractedEditState;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nOriginal content here.';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
|
||||
// Test workflow: Start editing
|
||||
runner.expect(section.state).toBe(EditState.ORIGINAL);
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
|
||||
const content = sectionManager.startEditing(sectionId);
|
||||
runner.expect(content).toContain('Test Heading');
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
runner.expect(section.state).toBe(EditState.EDITING);
|
||||
|
||||
// Test workflow: Update content
|
||||
const newContent = '# Updated Heading\nModified content here.';
|
||||
sectionManager.updateContent(sectionId, newContent);
|
||||
runner.expect(section.editingMarkdown).toBe(newContent);
|
||||
|
||||
// Test workflow: Accept changes
|
||||
sectionManager.acceptChanges(sectionId);
|
||||
runner.expect(section.currentMarkdown).toBe(newContent);
|
||||
runner.expect(section.state).toBe(EditState.SAVED);
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support accept/cancel button functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nOriginal content here.';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
|
||||
// Start editing to trigger floating menu with buttons
|
||||
sectionManager.startEditing(sectionId);
|
||||
|
||||
// Check if floating menu exists
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
|
||||
runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
|
||||
|
||||
// Find buttons in the floating menu
|
||||
const menuElement = domRenderer.currentFloatingMenu.element;
|
||||
runner.expect(menuElement).toBeTruthy();
|
||||
|
||||
const buttons = menuElement.querySelectorAll('button');
|
||||
runner.expect(buttons.length >= 2).toBeTruthy(); // At least Accept and Cancel buttons
|
||||
|
||||
const acceptBtn = Array.from(buttons).find(btn => btn.textContent === 'Accept');
|
||||
const cancelBtn = Array.from(buttons).find(btn => btn.textContent === 'Cancel');
|
||||
|
||||
runner.expect(acceptBtn).toBeTruthy();
|
||||
runner.expect(cancelBtn).toBeTruthy();
|
||||
|
||||
// Test Accept button functionality
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Simulate updating content and clicking Accept
|
||||
const textarea = menuElement.querySelector('textarea');
|
||||
runner.expect(textarea).toBeTruthy();
|
||||
textarea.value = '# Updated Heading\nUpdated content via button.';
|
||||
|
||||
acceptBtn.click();
|
||||
|
||||
// After clicking Accept, section should be saved and menu hidden
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
runner.expect(section.currentMarkdown).toContain('Updated Heading');
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support cancel button functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Original Heading\nOriginal content here.';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
|
||||
// Start editing
|
||||
sectionManager.startEditing(sectionId);
|
||||
|
||||
// Find buttons in the floating menu
|
||||
const menuElement = domRenderer.currentFloatingMenu.element;
|
||||
const cancelBtn = Array.from(menuElement.querySelectorAll('button')).find(btn => btn.textContent === 'Cancel');
|
||||
|
||||
runner.expect(cancelBtn).toBeTruthy();
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Simulate changing content but then canceling
|
||||
const textarea = menuElement.querySelector('textarea');
|
||||
textarea.value = '# Changed Heading\nThis should be discarded.';
|
||||
|
||||
cancelBtn.click();
|
||||
|
||||
// After clicking Cancel, section should not be saved and menu hidden
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
runner.expect(section.currentMarkdown).toContain('Original Heading'); // Original content preserved
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support event-driven communication', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Track events
|
||||
let sectionsCreatedEvent = null;
|
||||
let editStartedEvent = null;
|
||||
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
sectionsCreatedEvent = data;
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
editStartedEvent = data;
|
||||
});
|
||||
|
||||
// Test event: sections-created
|
||||
const testMarkdown = '# Test\nContent';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
runner.expect(sectionsCreatedEvent).toBeTruthy();
|
||||
runner.expect(sectionsCreatedEvent.sections).toEqual(sections);
|
||||
runner.expect(sectionsCreatedEvent.count).toBe(1);
|
||||
|
||||
// Test event: edit-started
|
||||
const sectionId = sections[0].id;
|
||||
sectionManager.startEditing(sectionId);
|
||||
|
||||
runner.expect(editStartedEvent).toBeTruthy();
|
||||
runner.expect(editStartedEvent.sectionId).toBe(sectionId);
|
||||
runner.expect(editStartedEvent.content).toContain('Test');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support section type detection and rendering', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const Section = global.ExtractedSection;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test different section types
|
||||
const testMarkdown = `# Heading Section
|
||||
Regular paragraph content.
|
||||
|
||||

|
||||
|
||||
\`\`\`javascript
|
||||
// Code section
|
||||
console.log('test');
|
||||
\`\`\``;
|
||||
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
|
||||
// Verify type detection - adjusted for actual parsing behavior
|
||||
// Expected: heading+paragraph, image, code = 3 sections
|
||||
runner.expect(sections[0].type).toBe('heading'); // Combined heading+paragraph
|
||||
runner.expect(sections[1].type).toBe('image'); // Image section
|
||||
runner.expect(sections[2].type).toBe('code'); // Code section
|
||||
|
||||
// Verify image detection
|
||||
runner.expect(sections[1].isImage()).toBeTruthy(); // Image is now at index 1
|
||||
runner.expect(sections[0].isImage()).toBeFalsy();
|
||||
|
||||
// Verify rendering handles different types
|
||||
domRenderer.renderAllSections(sections);
|
||||
const renderedElements = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedElements.length).toBe(sections.length);
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support FloatingMenu integration', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const FloatingMenu = global.ExtractedFloatingMenu;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// Test showing editor (which uses FloatingMenu)
|
||||
domRenderer.showEditor(sectionId, 'test content');
|
||||
|
||||
// Verify floating menu state
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
|
||||
runner.expect(domRenderer.currentFloatingMenu.sectionId).toBe(sectionId);
|
||||
runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
|
||||
runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
|
||||
|
||||
// Test hiding editor
|
||||
domRenderer.hideCurrentEditor();
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
|
||||
runner.expect(domRenderer.editingSections.has(sectionId)).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support complete click-to-edit workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nTest content for editing';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = domRenderer.findSectionElement(sectionId);
|
||||
|
||||
// Simulate click event
|
||||
const clickEvent = new Event('click', { bubbles: true });
|
||||
Object.defineProperty(clickEvent, 'target', { value: element });
|
||||
|
||||
// Test complete workflow
|
||||
domRenderer.handleSectionClick(clickEvent);
|
||||
|
||||
// Verify editing state was triggered
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support document status tracking', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const container = document.createElement('div');
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test initial status
|
||||
let status = sectionManager.getDocumentStatus();
|
||||
runner.expect(status.totalSections).toBe(0);
|
||||
runner.expect(status.editingSections).toBe(0);
|
||||
|
||||
// Create sections
|
||||
const testMarkdown = '# Section 1\nContent 1\n\n# Section 2\nContent 2';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
status = sectionManager.getDocumentStatus();
|
||||
runner.expect(status.totalSections).toBe(2);
|
||||
runner.expect(status.editingSections).toBe(2); // Bug compatibility (isEditing property exists)
|
||||
|
||||
// Test getAllSections
|
||||
const allSections = sectionManager.getAllSections();
|
||||
runner.expect(allSections.length).toBe(2);
|
||||
runner.expect(allSections[0].currentMarkdown).toContain('Section 1');
|
||||
runner.expect(allSections[1].currentMarkdown).toContain('Section 2');
|
||||
});
|
||||
|
||||
runner.it('should support event tracking and analytics', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test event tracking
|
||||
domRenderer.trackEvent('test-event', { data: 'test' });
|
||||
domRenderer.trackEvent('section-click', { sectionId: 'test-123' });
|
||||
|
||||
const stats = domRenderer.getEventStats();
|
||||
runner.expect(stats.totalEvents).toBe(1); // Only section-click is tracked in stats
|
||||
runner.expect(stats.stats['section-click']).toBe(1);
|
||||
runner.expect(stats.recentEvents.length).toBe(2);
|
||||
runner.expect(stats.recentEvents[0].type).toBe('test-event');
|
||||
runner.expect(stats.recentEvents[1].type).toBe('section-click');
|
||||
});
|
||||
|
||||
// Integration stress test
|
||||
runner.it('should handle complex document with multiple operations', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Complex document
|
||||
const complexMarkdown = `# Document Title
|
||||
Introduction paragraph with some content.
|
||||
|
||||
## Section A
|
||||
Content for section A with details.
|
||||
|
||||

|
||||
|
||||
### Subsection A.1
|
||||
More detailed content here.
|
||||
|
||||
\`\`\`javascript
|
||||
function test() {
|
||||
console.log('code block');
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Section B
|
||||
Final section content.`;
|
||||
|
||||
// Create and render
|
||||
const sections = sectionManager.createSectionsFromMarkdown(complexMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
runner.expect(sections.length).toBe(6); // Adjusted based on actual parsing
|
||||
|
||||
// Test editing multiple sections
|
||||
const firstSection = sections[0];
|
||||
const imageSection = sections.find(s => s.isImage());
|
||||
const codeSection = sections.find(s => s.type === 'code');
|
||||
|
||||
// Edit first section
|
||||
sectionManager.startEditing(firstSection.id);
|
||||
sectionManager.updateContent(firstSection.id, '# Updated Title\nUpdated intro.');
|
||||
sectionManager.acceptChanges(firstSection.id);
|
||||
|
||||
// Edit image section
|
||||
sectionManager.startEditing(imageSection.id);
|
||||
sectionManager.updateContent(imageSection.id, '');
|
||||
sectionManager.acceptChanges(imageSection.id);
|
||||
|
||||
// Verify changes
|
||||
runner.expect(firstSection.currentMarkdown).toContain('Updated Title');
|
||||
runner.expect(imageSection.currentMarkdown).toContain('Updated Image');
|
||||
|
||||
// Verify document reconstruction
|
||||
const finalMarkdown = sectionManager.getDocumentMarkdown();
|
||||
runner.expect(finalMarkdown).toContain('Updated Title');
|
||||
runner.expect(finalMarkdown).toContain('Updated Image');
|
||||
runner.expect(finalMarkdown).toContain('Section B');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running Component Integration Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Component integration tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Debug Panel Component Extraction
|
||||
*
|
||||
* Tests the extraction of DebugPanel from the monolithic editor.js
|
||||
* DebugPanel handles debug message display and management.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// Define expected DebugPanel API
|
||||
const EXPECTED_DEBUGPANEL_API = [
|
||||
'constructor',
|
||||
'toggle',
|
||||
'update',
|
||||
'clear',
|
||||
'addMessage',
|
||||
'show',
|
||||
'hide',
|
||||
'getMessageCount',
|
||||
'getRecentMessages'
|
||||
];
|
||||
|
||||
runner.describe('DebugPanel Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
const expectedMethods = EXPECTED_DEBUGPANEL_API;
|
||||
runner.expect(expectedMethods.length).toBe(9);
|
||||
runner.expect(expectedMethods).toContain('toggle');
|
||||
runner.expect(expectedMethods).toContain('update');
|
||||
runner.expect(expectedMethods).toContain('addMessage');
|
||||
});
|
||||
|
||||
runner.it('should load extracted DebugPanel component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../components/debug-panel.js')];
|
||||
|
||||
try {
|
||||
const module = require('../components/debug-panel.js');
|
||||
runner.expect(module.DebugPanel).toBeTruthy();
|
||||
|
||||
// Set global for other tests
|
||||
global.ExtractedDebugPanel = module.DebugPanel;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted DebugPanel: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
runner.expect(debugPanel).toBeInstanceOf(DebugPanel);
|
||||
runner.expect(debugPanel.messages).toBeInstanceOf(Array);
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should preserve message handling functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test adding messages
|
||||
debugPanel.addMessage('Test message', 'INFO');
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(1);
|
||||
|
||||
const recentMessages = debugPanel.getRecentMessages(1);
|
||||
runner.expect(recentMessages.length).toBe(1);
|
||||
runner.expect(recentMessages[0].message).toBe('Test message');
|
||||
runner.expect(recentMessages[0].category).toBe('INFO');
|
||||
});
|
||||
|
||||
runner.it('should preserve toggle functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
// Create container element
|
||||
const container = document.createElement('div');
|
||||
container.id = 'debug-messages-container';
|
||||
container.style.display = 'none';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test toggle on
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
|
||||
// Test toggle off
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should preserve update functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.id = 'debug-messages-container';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
debugPanel.show();
|
||||
|
||||
debugPanel.addMessage('Test message 1', 'INFO');
|
||||
debugPanel.addMessage('Test message 2', 'ERROR');
|
||||
debugPanel.update();
|
||||
|
||||
runner.expect(container.innerHTML.length > 100).toBeTruthy();
|
||||
runner.expect(container.innerHTML).toContain('Test message 1');
|
||||
runner.expect(container.innerHTML).toContain('Test message 2');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should preserve clear functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
debugPanel.addMessage('Test message 1', 'INFO');
|
||||
debugPanel.addMessage('Test message 2', 'ERROR');
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2);
|
||||
|
||||
debugPanel.clear();
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(0);
|
||||
});
|
||||
|
||||
runner.it('should have core debug panel methods', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Should have core methods
|
||||
runner.expect(typeof debugPanel.toggle === 'function').toBeTruthy();
|
||||
runner.expect(typeof debugPanel.update === 'function').toBeTruthy();
|
||||
runner.expect(typeof debugPanel.addMessage === 'function').toBeTruthy();
|
||||
runner.expect(typeof debugPanel.clear === 'function').toBeTruthy();
|
||||
});
|
||||
|
||||
runner.it('should handle message categories properly', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test different message categories
|
||||
debugPanel.addMessage('Info message', 'INFO');
|
||||
debugPanel.addMessage('Warning message', 'WARNING');
|
||||
debugPanel.addMessage('Error message', 'ERROR');
|
||||
debugPanel.addMessage('Success message', 'SUCCESS');
|
||||
|
||||
const messages = debugPanel.getRecentMessages(4);
|
||||
runner.expect(messages.length).toBe(4);
|
||||
|
||||
const categories = messages.map(m => m.category);
|
||||
runner.expect(categories).toContain('INFO');
|
||||
runner.expect(categories).toContain('WARNING');
|
||||
runner.expect(categories).toContain('ERROR');
|
||||
runner.expect(categories).toContain('SUCCESS');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_DEBUGPANEL_API
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing DebugPanel Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DebugPanel extraction tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* DebugPanel Integration Test
|
||||
*
|
||||
* Tests that the extracted DebugPanel component integrates properly
|
||||
* with the existing SectionManager and DOMRenderer components.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('DebugPanel Integration Tests', () => {
|
||||
|
||||
runner.it('should load all extracted components including DebugPanel', () => {
|
||||
try {
|
||||
// Load extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
|
||||
runner.expect(sectionModule.SectionManager).toBeTruthy();
|
||||
runner.expect(domModule.DOMRenderer).toBeTruthy();
|
||||
runner.expect(debugModule.DebugPanel).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = sectionModule.SectionManager;
|
||||
global.ExtractedDOMRenderer = domModule.DOMRenderer;
|
||||
global.ExtractedDebugPanel = debugModule.DebugPanel;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted components: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support debug panel with section editing workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
// Setup DOM elements
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const debugContainer = document.createElement('div');
|
||||
debugContainer.id = 'debug-messages-container';
|
||||
debugContainer.style.display = 'none';
|
||||
document.body.appendChild(debugContainer);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
// Create components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test workflow: Create sections and debug them
|
||||
const testMarkdown = '# Test Heading\nTest content for debugging';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
// Add debug messages
|
||||
debugPanel.addMessage('Section created: ' + sections[0].id, 'INFO');
|
||||
debugPanel.addMessage('DOM rendered successfully', 'SUCCESS');
|
||||
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2);
|
||||
|
||||
// Test showing debug panel
|
||||
debugPanel.show();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
|
||||
// Test debug panel content
|
||||
const messages = debugPanel.getRecentMessages(2);
|
||||
runner.expect(messages[0].message).toContain('Section created');
|
||||
runner.expect(messages[1].message).toContain('DOM rendered');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(debugContainer);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should support debug panel clearing and message management', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Add multiple messages
|
||||
for (let i = 0; i < 10; i++) {
|
||||
debugPanel.addMessage(`Test message ${i}`, i % 2 === 0 ? 'INFO' : 'WARNING');
|
||||
}
|
||||
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(10);
|
||||
|
||||
// Test getting recent messages
|
||||
const recentFive = debugPanel.getRecentMessages(5);
|
||||
runner.expect(recentFive.length).toBe(5);
|
||||
runner.expect(recentFive[4].message).toContain('Test message 9');
|
||||
|
||||
// Test clearing
|
||||
debugPanel.clear();
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(0);
|
||||
});
|
||||
|
||||
runner.it('should handle debug panel DOM integration properly', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
// Setup DOM
|
||||
const debugContainer = document.createElement('div');
|
||||
debugContainer.id = 'debug-messages-container';
|
||||
debugContainer.style.display = 'none';
|
||||
document.body.appendChild(debugContainer);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
debugButton.style.background = '#6c757d';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test initial state
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
runner.expect(debugContainer.style.display).toBe('none');
|
||||
|
||||
// Test toggle on
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
runner.expect(debugContainer.style.display).toBe('block');
|
||||
runner.expect(debugButton.textContent).toContain('Debug (ON)');
|
||||
|
||||
// Test toggle off
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
runner.expect(debugContainer.style.display).toBe('none');
|
||||
runner.expect(debugButton.textContent).toBe('🔍 Debug');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(debugContainer);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should handle missing DOM elements gracefully', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Try to toggle without DOM elements (should not throw)
|
||||
try {
|
||||
debugPanel.toggle();
|
||||
debugPanel.show();
|
||||
debugPanel.hide();
|
||||
debugPanel.update();
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
|
||||
} catch (error) {
|
||||
throw new Error(`DebugPanel should handle missing DOM gracefully: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support event-driven debug message addition', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Listen to section manager events and add debug messages
|
||||
let eventCount = 0;
|
||||
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
debugPanel.addMessage(`Sections created: ${data.count} sections`, 'INFO');
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
// Create sections
|
||||
const testMarkdown = '# Test\nContent';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// Start editing
|
||||
sectionManager.startEditing(sections[0].id);
|
||||
|
||||
// Verify debug messages were added
|
||||
runner.expect(eventCount).toBe(2);
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2);
|
||||
|
||||
const messages = debugPanel.getRecentMessages(2);
|
||||
runner.expect(messages[0].message).toContain('Sections created');
|
||||
runner.expect(messages[1].message).toContain('Edit started');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running DebugPanel Integration Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DebugPanel integration tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocumentNavigator TDD Test Runner</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.test-header {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-output {
|
||||
background: #1a1a1a;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', monospace;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 1rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
.run-button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.run-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.run-button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.running {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.status.passed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.failed {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-header">
|
||||
<h1>📋 DocumentNavigator Widget TDD Test Suite</h1>
|
||||
<p>
|
||||
This test suite follows Test-Driven Development methodology to implement a Substack-style
|
||||
floating document navigation widget. The tests define the expected behavior before
|
||||
implementation begins.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<strong>Test Coverage:</strong>
|
||||
<ul>
|
||||
<li>✅ Widget class structure and inheritance</li>
|
||||
<li>✅ Configuration and initialization</li>
|
||||
<li>✅ DOM rendering and UI elements</li>
|
||||
<li>✅ Heading extraction and hierarchy building</li>
|
||||
<li>✅ Navigation functionality and smooth scrolling</li>
|
||||
<li>✅ Expand/collapse behavior</li>
|
||||
<li>✅ Scroll spy and active section detection</li>
|
||||
<li>✅ Responsive behavior and auto-hide</li>
|
||||
<li>✅ Keyboard navigation support</li>
|
||||
<li>✅ Event emission and user interaction</li>
|
||||
<li>✅ Edge cases and error handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button id="runTests" class="run-button">🧪 Run TDD Test Suite</button>
|
||||
|
||||
<div id="status" class="status" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div id="testOutput" class="test-output" style="display: none;"></div>
|
||||
|
||||
<script type="module">
|
||||
const runButton = document.getElementById('runTests');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const outputDiv = document.getElementById('testOutput');
|
||||
|
||||
// Capture console output
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
let capturedOutput = '';
|
||||
|
||||
function captureConsole() {
|
||||
capturedOutput = '';
|
||||
|
||||
console.log = (...args) => {
|
||||
capturedOutput += args.join(' ') + '\n';
|
||||
originalConsoleLog(...args);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
capturedOutput += 'ERROR: ' + args.join(' ') + '\n';
|
||||
originalConsoleError(...args);
|
||||
};
|
||||
}
|
||||
|
||||
function restoreConsole() {
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
}
|
||||
|
||||
function updateStatus(message, type) {
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.className = `status ${type}`;
|
||||
statusDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showOutput() {
|
||||
outputDiv.textContent = capturedOutput;
|
||||
outputDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
runButton.addEventListener('click', async () => {
|
||||
runButton.disabled = true;
|
||||
updateStatus('🧪 Running tests...', 'running');
|
||||
|
||||
captureConsole();
|
||||
|
||||
try {
|
||||
// Import and run tests
|
||||
const { runner } = await import('./test-document-navigator.js');
|
||||
|
||||
console.log('Starting DocumentNavigator TDD Test Suite...\n');
|
||||
console.log('Note: Tests are expected to FAIL initially (Red phase of TDD)');
|
||||
console.log('We will implement functionality to make them pass (Green phase).\n');
|
||||
|
||||
await runner.run();
|
||||
|
||||
if (runner.results.failed === 0) {
|
||||
updateStatus(`🎉 All ${runner.results.total} tests passed!`, 'passed');
|
||||
} else {
|
||||
updateStatus(`❌ ${runner.results.failed} of ${runner.results.total} tests failed (Expected in TDD Red phase)`, 'failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test execution failed:', error);
|
||||
updateStatus('💥 Test execution failed - this is expected in TDD Red phase', 'failed');
|
||||
} finally {
|
||||
restoreConsole();
|
||||
showOutput();
|
||||
runButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-run tests on page load for development
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DocumentNavigator TDD Test Runner loaded');
|
||||
console.log('Ready to run tests - click the button above');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Test content for heading extraction tests -->
|
||||
<div style="display: none;" id="test-content">
|
||||
<h1>Test Chapter 1</h1>
|
||||
<p>Sample content for testing heading extraction.</p>
|
||||
<h2>Section 1.1</h2>
|
||||
<h3>Subsection 1.1.1</h3>
|
||||
<p>More sample content.</p>
|
||||
<h2>Section 1.2</h2>
|
||||
<h1>Test Chapter 2</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,432 +0,0 @@
|
||||
/**
|
||||
* TDD Test Suite for DocumentNavigator Widget
|
||||
*
|
||||
* Tests the Substack-style floating navigation widget for document headings.
|
||||
* Following TDD methodology: write tests first, then implement functionality.
|
||||
*/
|
||||
|
||||
// Simple test runner for browser environment
|
||||
class DocumentNavigatorTestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.results = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
test(name, testFn) {
|
||||
this.tests.push({ name, testFn });
|
||||
}
|
||||
|
||||
expect(actual) {
|
||||
return {
|
||||
toBe: (expected) => {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`Expected ${actual} to be ${expected}`);
|
||||
}
|
||||
},
|
||||
toBeInstanceOf: (expectedClass) => {
|
||||
if (!(actual instanceof expectedClass)) {
|
||||
throw new Error(`Expected ${actual} to be instance of ${expectedClass.name}`);
|
||||
}
|
||||
},
|
||||
toBeTruthy: () => {
|
||||
if (!actual) {
|
||||
throw new Error(`Expected ${actual} to be truthy`);
|
||||
}
|
||||
},
|
||||
toBeFalsy: () => {
|
||||
if (actual) {
|
||||
throw new Error(`Expected ${actual} to be falsy`);
|
||||
}
|
||||
},
|
||||
toContain: (expected) => {
|
||||
if (typeof actual === 'string' && !actual.includes(expected)) {
|
||||
throw new Error(`Expected "${actual}" to contain "${expected}"`);
|
||||
}
|
||||
if (Array.isArray(actual) && !actual.includes(expected)) {
|
||||
throw new Error(`Expected array to contain ${expected}`);
|
||||
}
|
||||
},
|
||||
toHaveLength: (expected) => {
|
||||
if (actual.length !== expected) {
|
||||
throw new Error(`Expected length ${actual.length} to be ${expected}`);
|
||||
}
|
||||
},
|
||||
toBeGreaterThan: (expected) => {
|
||||
if (actual <= expected) {
|
||||
throw new Error(`Expected ${actual} to be greater than ${expected}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('🧪 Running DocumentNavigator TDD Test Suite...\n');
|
||||
|
||||
for (const { name, testFn } of this.tests) {
|
||||
this.results.total++;
|
||||
|
||||
try {
|
||||
await testFn.call(this);
|
||||
this.results.passed++;
|
||||
console.log(`✅ ${name}`);
|
||||
} catch (error) {
|
||||
this.results.failed++;
|
||||
console.log(`❌ ${name}`);
|
||||
console.log(` ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
this.printSummary();
|
||||
}
|
||||
|
||||
printSummary() {
|
||||
console.log(`\n📊 Test Results:`);
|
||||
console.log(` Passed: ${this.results.passed}`);
|
||||
console.log(` Failed: ${this.results.failed}`);
|
||||
console.log(` Total: ${this.results.total}`);
|
||||
|
||||
if (this.results.failed === 0) {
|
||||
console.log(`\n🎉 All tests passed!`);
|
||||
} else {
|
||||
console.log(`\n❌ ${this.results.failed} test(s) failed.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create test runner
|
||||
const runner = new DocumentNavigatorTestRunner();
|
||||
|
||||
// Test Suite: DocumentNavigator Widget
|
||||
runner.test('DocumentNavigator class should exist and be importable', async function() {
|
||||
// This test will fail initially - we haven't created the class yet
|
||||
try {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
this.expect(DocumentNavigator).toBeTruthy();
|
||||
this.expect(typeof DocumentNavigator).toBe('function');
|
||||
} catch (error) {
|
||||
throw new Error(`DocumentNavigator class not found: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should extend UIWidget', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
const { UIWidget } = await import('../widgets/base/UIWidget.js');
|
||||
|
||||
const navigator = new DocumentNavigator();
|
||||
this.expect(navigator).toBeInstanceOf(UIWidget);
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should initialize with default configuration', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const navigator = new DocumentNavigator();
|
||||
|
||||
// Test default configuration
|
||||
this.expect(navigator.config.position).toBe('left');
|
||||
this.expect(navigator.config.collapsed).toBe(true);
|
||||
this.expect(navigator.config.autoHide).toBe(true);
|
||||
this.expect(navigator.config.maxHeadingLevel).toBe(3);
|
||||
this.expect(navigator.config.enableScrollSpy).toBe(true);
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should accept custom configuration', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const customConfig = {
|
||||
position: 'right',
|
||||
collapsed: false,
|
||||
maxHeadingLevel: 4,
|
||||
theme: 'dark'
|
||||
};
|
||||
|
||||
const navigator = new DocumentNavigator(customConfig);
|
||||
|
||||
this.expect(navigator.config.position).toBe('right');
|
||||
this.expect(navigator.config.collapsed).toBe(false);
|
||||
this.expect(navigator.config.maxHeadingLevel).toBe(4);
|
||||
this.expect(navigator.config.theme).toBe('dark');
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should render floating panel element', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const navigator = new DocumentNavigator();
|
||||
await navigator.render();
|
||||
|
||||
this.expect(navigator.element).toBeInstanceOf(HTMLElement);
|
||||
this.expect(navigator.element.classList.contains('document-navigator')).toBeTruthy();
|
||||
this.expect(navigator.element.style.position).toBe('fixed');
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should have toggle button in collapsed state', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const navigator = new DocumentNavigator({ collapsed: true });
|
||||
await navigator.render();
|
||||
|
||||
const toggleButton = navigator.findElement('.navigator-toggle');
|
||||
this.expect(toggleButton).toBeInstanceOf(HTMLElement);
|
||||
this.expect(toggleButton.style.display).not.toBe('none');
|
||||
|
||||
const navList = navigator.findElement('.navigator-list');
|
||||
this.expect(navList.style.display).toBe('none');
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should extract headings from document', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
// Create test document with headings
|
||||
const testContainer = document.createElement('div');
|
||||
testContainer.innerHTML = `
|
||||
<h1 id="heading1">First Heading</h1>
|
||||
<p>Some content</p>
|
||||
<h2 id="heading2">Second Heading</h2>
|
||||
<h3 id="heading3">Third Heading</h3>
|
||||
<p>More content</p>
|
||||
<h2 id="heading4">Fourth Heading</h2>
|
||||
`;
|
||||
document.body.appendChild(testContainer);
|
||||
|
||||
const navigator = new DocumentNavigator({
|
||||
container: testContainer,
|
||||
maxHeadingLevel: 3
|
||||
});
|
||||
|
||||
const headings = navigator.extractHeadings();
|
||||
|
||||
this.expect(headings).toHaveLength(4);
|
||||
this.expect(headings[0].tagName).toBe('H1');
|
||||
this.expect(headings[0].textContent).toBe('First Heading');
|
||||
this.expect(headings[1].tagName).toBe('H2');
|
||||
this.expect(headings[2].tagName).toBe('H3');
|
||||
this.expect(headings[3].tagName).toBe('H2');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(testContainer);
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should build navigation hierarchy', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
// Create test document with nested headings
|
||||
const testContainer = document.createElement('div');
|
||||
testContainer.innerHTML = `
|
||||
<h1>Chapter 1</h1>
|
||||
<h2>Section 1.1</h2>
|
||||
<h3>Subsection 1.1.1</h3>
|
||||
<h3>Subsection 1.1.2</h3>
|
||||
<h2>Section 1.2</h2>
|
||||
<h1>Chapter 2</h1>
|
||||
`;
|
||||
document.body.appendChild(testContainer);
|
||||
|
||||
const navigator = new DocumentNavigator({ container: testContainer });
|
||||
await navigator.render();
|
||||
|
||||
const navItems = navigator.buildNavigationTree();
|
||||
|
||||
// Should have hierarchical structure
|
||||
this.expect(navItems).toHaveLength(2); // 2 H1 elements
|
||||
this.expect(navItems[0].children).toHaveLength(2); // 2 H2 under first H1
|
||||
this.expect(navItems[0].children[0].children).toHaveLength(2); // 2 H3 under first H2
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(testContainer);
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should handle click navigation', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
// Create test document
|
||||
const testContainer = document.createElement('div');
|
||||
testContainer.innerHTML = `
|
||||
<h1 id="target-heading">Target Heading</h1>
|
||||
<p style="height: 1000px;">Spacer content</p>
|
||||
`;
|
||||
document.body.appendChild(testContainer);
|
||||
|
||||
const navigator = new DocumentNavigator({ container: testContainer });
|
||||
await navigator.render();
|
||||
|
||||
// Simulate click on navigation item
|
||||
const navItem = navigator.findElement('[data-target="target-heading"]');
|
||||
this.expect(navItem).toBeTruthy();
|
||||
|
||||
// Mock scrollIntoView for testing
|
||||
const targetElement = document.getElementById('target-heading');
|
||||
let scrollCalled = false;
|
||||
targetElement.scrollIntoView = () => { scrollCalled = true; };
|
||||
|
||||
// Click navigation item
|
||||
navItem.click();
|
||||
|
||||
this.expect(scrollCalled).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(testContainer);
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should support expand/collapse functionality', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const navigator = new DocumentNavigator({ collapsed: true });
|
||||
await navigator.render();
|
||||
|
||||
// Should start collapsed
|
||||
this.expect(navigator.isCollapsed).toBeTruthy();
|
||||
|
||||
const toggleButton = navigator.findElement('.navigator-toggle');
|
||||
const navList = navigator.findElement('.navigator-list');
|
||||
|
||||
// Toggle to expanded
|
||||
await navigator.expand();
|
||||
this.expect(navigator.isCollapsed).toBeFalsy();
|
||||
this.expect(navList.style.display).not.toBe('none');
|
||||
|
||||
// Toggle back to collapsed
|
||||
await navigator.collapse();
|
||||
this.expect(navigator.isCollapsed).toBeTruthy();
|
||||
this.expect(navList.style.display).toBe('none');
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should implement scroll spy functionality', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
// Create test document with multiple sections
|
||||
const testContainer = document.createElement('div');
|
||||
testContainer.innerHTML = `
|
||||
<div style="height: 100px;"></div>
|
||||
<h1 id="section1">Section 1</h1>
|
||||
<div style="height: 400px;"></div>
|
||||
<h2 id="section2">Section 2</h2>
|
||||
<div style="height: 400px;"></div>
|
||||
<h2 id="section3">Section 3</h2>
|
||||
<div style="height: 400px;"></div>
|
||||
`;
|
||||
document.body.appendChild(testContainer);
|
||||
|
||||
const navigator = new DocumentNavigator({
|
||||
container: testContainer,
|
||||
enableScrollSpy: true
|
||||
});
|
||||
await navigator.render();
|
||||
|
||||
// Test current section detection
|
||||
const currentSection = navigator.getCurrentSection();
|
||||
this.expect(currentSection).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(testContainer);
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should handle responsive behavior', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const navigator = new DocumentNavigator({ autoHide: true });
|
||||
await navigator.render();
|
||||
|
||||
// Mock viewport resize
|
||||
const originalInnerWidth = window.innerWidth;
|
||||
|
||||
// Test mobile viewport
|
||||
Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true });
|
||||
navigator.handleResize();
|
||||
this.expect(navigator.element.style.display).toBe('none');
|
||||
|
||||
// Test desktop viewport
|
||||
Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true });
|
||||
navigator.handleResize();
|
||||
this.expect(navigator.element.style.display).not.toBe('none');
|
||||
|
||||
// Restore original
|
||||
Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true });
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should provide keyboard navigation support', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const navigator = new DocumentNavigator();
|
||||
await navigator.render();
|
||||
|
||||
// Test keyboard shortcuts
|
||||
let expandCalled = false;
|
||||
let collapseCalled = false;
|
||||
|
||||
navigator.expand = async () => { expandCalled = true; };
|
||||
navigator.collapse = async () => { collapseCalled = true; };
|
||||
|
||||
// Simulate keyboard events
|
||||
const element = navigator.element;
|
||||
|
||||
// Test Escape key (should collapse)
|
||||
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
|
||||
element.dispatchEvent(escapeEvent);
|
||||
this.expect(collapseCalled).toBeTruthy();
|
||||
|
||||
// Test Enter/Space key (should expand)
|
||||
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
|
||||
element.dispatchEvent(enterEvent);
|
||||
this.expect(expandCalled).toBeTruthy();
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should emit events for user interactions', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
const navigator = new DocumentNavigator();
|
||||
await navigator.render();
|
||||
|
||||
// Test event emission
|
||||
let navigationEvent = null;
|
||||
navigator.addEventListener('navigate', (e) => {
|
||||
navigationEvent = e;
|
||||
});
|
||||
|
||||
let toggleEvent = null;
|
||||
navigator.addEventListener('toggle', (e) => {
|
||||
toggleEvent = e;
|
||||
});
|
||||
|
||||
// Trigger navigation
|
||||
navigator.navigateToHeading('test-heading');
|
||||
this.expect(navigationEvent).toBeTruthy();
|
||||
this.expect(navigationEvent.detail.target).toBe('test-heading');
|
||||
|
||||
// Trigger toggle
|
||||
await navigator.toggle();
|
||||
this.expect(toggleEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
runner.test('DocumentNavigator should handle empty document gracefully', async function() {
|
||||
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
||||
|
||||
// Create empty container
|
||||
const emptyContainer = document.createElement('div');
|
||||
document.body.appendChild(emptyContainer);
|
||||
|
||||
const navigator = new DocumentNavigator({ container: emptyContainer });
|
||||
|
||||
const headings = navigator.extractHeadings();
|
||||
this.expect(headings).toHaveLength(0);
|
||||
|
||||
await navigator.render();
|
||||
const navList = navigator.findElement('.navigator-list');
|
||||
this.expect(navList.children).toHaveLength(0);
|
||||
|
||||
// Should show empty state message
|
||||
const emptyMessage = navigator.findElement('.navigator-empty');
|
||||
this.expect(emptyMessage).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(emptyContainer);
|
||||
});
|
||||
|
||||
// Export test runner for use in HTML
|
||||
window.runDocumentNavigatorTests = () => runner.run();
|
||||
|
||||
console.log('📋 DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()');
|
||||
|
||||
export { runner };
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Document Controls Component Extraction
|
||||
*
|
||||
* Tests the extraction of DocumentControls from the monolithic editor.js
|
||||
* DocumentControls handles the floating control panel and its actions.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// Define expected DocumentControls API
|
||||
const EXPECTED_DOCUMENTCONTROLS_API = [
|
||||
'constructor',
|
||||
'create',
|
||||
'destroy',
|
||||
'show',
|
||||
'hide',
|
||||
'addButton',
|
||||
'removeButton',
|
||||
'setEventHandlers',
|
||||
'updateStatus',
|
||||
'getControlPanel'
|
||||
];
|
||||
|
||||
runner.describe('DocumentControls Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
const expectedMethods = EXPECTED_DOCUMENTCONTROLS_API;
|
||||
runner.expect(expectedMethods.length).toBe(10);
|
||||
runner.expect(expectedMethods).toContain('create');
|
||||
runner.expect(expectedMethods).toContain('addButton');
|
||||
runner.expect(expectedMethods).toContain('setEventHandlers');
|
||||
});
|
||||
|
||||
runner.it('should load extracted DocumentControls component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../components/document-controls.js')];
|
||||
|
||||
try {
|
||||
const module = require('../components/document-controls.js');
|
||||
runner.expect(module.DocumentControls).toBeTruthy();
|
||||
|
||||
// Set global for other tests
|
||||
global.ExtractedDocumentControls = module.DocumentControls;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted DocumentControls: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
runner.expect(controls).toBeInstanceOf(DocumentControls);
|
||||
runner.expect(controls.controlPanel).toBeFalsy(); // Initially null
|
||||
runner.expect(controls.buttons).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
runner.it('should preserve control panel creation functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
const panel = controls.getControlPanel();
|
||||
runner.expect(panel).toBeTruthy();
|
||||
runner.expect(panel.id).toBe('markitect-global-controls');
|
||||
|
||||
// Check that panel is added to DOM
|
||||
const domPanel = document.getElementById('markitect-global-controls');
|
||||
runner.expect(domPanel).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should preserve button creation functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Default buttons should be created
|
||||
runner.expect(controls.buttons.has('save-document')).toBeTruthy();
|
||||
runner.expect(controls.buttons.has('reset-all')).toBeTruthy();
|
||||
runner.expect(controls.buttons.has('show-status')).toBeTruthy();
|
||||
runner.expect(controls.buttons.has('toggle-debug')).toBeTruthy();
|
||||
|
||||
// Check DOM elements exist
|
||||
runner.expect(document.getElementById('save-document')).toBeTruthy();
|
||||
runner.expect(document.getElementById('reset-all')).toBeTruthy();
|
||||
runner.expect(document.getElementById('show-status')).toBeTruthy();
|
||||
runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support custom button addition', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Add custom button
|
||||
const customButton = controls.addButton('custom-test', '🎯 Test', '#ff6600');
|
||||
runner.expect(customButton).toBeTruthy();
|
||||
runner.expect(customButton.id).toBe('custom-test');
|
||||
runner.expect(customButton.textContent).toBe('🎯 Test');
|
||||
|
||||
// Check button is in map and DOM
|
||||
runner.expect(controls.buttons.has('custom-test')).toBeTruthy();
|
||||
runner.expect(document.getElementById('custom-test')).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support event handler configuration', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
let saveClicked = false;
|
||||
let resetClicked = false;
|
||||
|
||||
const handlers = {
|
||||
'save-document': () => { saveClicked = true; },
|
||||
'reset-all': () => { resetClicked = true; }
|
||||
};
|
||||
|
||||
controls.setEventHandlers(handlers);
|
||||
|
||||
// Simulate button clicks
|
||||
const saveBtn = document.getElementById('save-document');
|
||||
const resetBtn = document.getElementById('reset-all');
|
||||
|
||||
saveBtn.click();
|
||||
resetBtn.click();
|
||||
|
||||
runner.expect(saveClicked).toBeTruthy();
|
||||
runner.expect(resetClicked).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support show/hide functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
const panel = controls.getControlPanel();
|
||||
|
||||
// Test hiding
|
||||
controls.hide();
|
||||
runner.expect(panel.style.display).toBe('none');
|
||||
|
||||
// Test showing
|
||||
controls.show();
|
||||
runner.expect(panel.style.display).toBe('block');
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should preserve destroy functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Verify panel exists
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
|
||||
|
||||
// Destroy
|
||||
controls.destroy();
|
||||
|
||||
// Verify panel is removed
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
|
||||
runner.expect(controls.controlPanel).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should support status updates', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Test status update
|
||||
controls.updateStatus({ totalSections: 5, editingSections: 2 });
|
||||
|
||||
// The status should be reflected in the panel (implementation specific)
|
||||
const panel = controls.getControlPanel();
|
||||
runner.expect(panel).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_DOCUMENTCONTROLS_API
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing DocumentControls Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DocumentControls extraction tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for DOMRenderer Component Extraction
|
||||
*
|
||||
* Tests the extraction of DOMRenderer from the monolithic editor.js
|
||||
* DOMRenderer handles all DOM interactions and UI rendering.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// Define expected DOMRenderer API
|
||||
const EXPECTED_DOMRENDERER_API = [
|
||||
'constructor',
|
||||
'renderAllSections',
|
||||
'renderSection',
|
||||
'showEditor',
|
||||
'hideCurrentEditor',
|
||||
'showImageEditor',
|
||||
'findSectionElement',
|
||||
'handleSectionClick',
|
||||
'setupSectionElement',
|
||||
'trackEvent',
|
||||
'getEventStats'
|
||||
// Note: addGlobalControls and debug methods are on MarkitectCleanEditor, not DOMRenderer
|
||||
];
|
||||
|
||||
runner.describe('DOMRenderer Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
const expectedMethods = EXPECTED_DOMRENDERER_API;
|
||||
runner.expect(expectedMethods.length).toBe(11);
|
||||
runner.expect(expectedMethods).toContain('renderAllSections');
|
||||
runner.expect(expectedMethods).toContain('showEditor');
|
||||
runner.expect(expectedMethods).toContain('handleSectionClick');
|
||||
});
|
||||
|
||||
runner.it('should extract from monolithic editor.js', () => {
|
||||
// Load the monolithic editor.js to extract DOMRenderer
|
||||
delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
|
||||
|
||||
try {
|
||||
const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
runner.expect(editorModule.DOMRenderer).toBeTruthy();
|
||||
// Set global for other tests
|
||||
global.DOMRenderer = editorModule.DOMRenderer;
|
||||
global.SectionManager = editorModule.SectionManager;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve DOMRenderer constructor functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
runner.expect(renderer).toBeInstanceOf(DOMRenderer);
|
||||
runner.expect(renderer.sectionManager).toBe(sectionManager);
|
||||
runner.expect(renderer.container).toBe(container);
|
||||
});
|
||||
|
||||
runner.it('should preserve section rendering functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// This should not throw an error
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
// Check that some content was rendered
|
||||
runner.expect(container.innerHTML.length).toBe(container.innerHTML.length); // Basic sanity check
|
||||
});
|
||||
|
||||
runner.it('should preserve findSectionElement functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = renderer.findSectionElement(sectionId);
|
||||
|
||||
// Should find an element or return null (not throw error)
|
||||
runner.expect(typeof element === 'object').toBeTruthy();
|
||||
});
|
||||
|
||||
runner.it('should preserve event tracking functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Should have trackEvent method
|
||||
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
|
||||
|
||||
// Should be able to track an event
|
||||
renderer.trackEvent('test-event', { data: 'test' });
|
||||
|
||||
// Should have getEventStats method
|
||||
runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
|
||||
|
||||
const stats = renderer.getEventStats();
|
||||
runner.expect(typeof stats === 'object').toBeTruthy();
|
||||
});
|
||||
|
||||
runner.it('should preserve editor showing functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// showEditor should not throw error
|
||||
try {
|
||||
renderer.showEditor(sectionId, 'test content');
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
|
||||
} catch (error) {
|
||||
// Some errors are expected if DOM structure isn't complete
|
||||
runner.expect(typeof error.message === 'string').toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should have core DOM rendering methods', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Should have core methods
|
||||
runner.expect(typeof renderer.renderAllSections === 'function').toBeTruthy();
|
||||
runner.expect(typeof renderer.showEditor === 'function').toBeTruthy();
|
||||
runner.expect(typeof renderer.findSectionElement === 'function').toBeTruthy();
|
||||
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// Export API tests for use during extraction
|
||||
const DOMRENDERER_API_TESTS = [
|
||||
(DOMRenderer, SectionManager) => {
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
if (!renderer.sectionManager) {
|
||||
throw new Error('sectionManager property missing');
|
||||
}
|
||||
},
|
||||
(DOMRenderer, SectionManager) => {
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
if (typeof renderer.renderAllSections !== 'function') {
|
||||
throw new Error('renderAllSections method missing');
|
||||
}
|
||||
},
|
||||
(DOMRenderer, SectionManager) => {
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
if (typeof renderer.showEditor !== 'function') {
|
||||
throw new Error('showEditor method missing');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_DOMRENDERER_API,
|
||||
DOMRENDERER_API_TESTS
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing DOMRenderer Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DOMRenderer extraction tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Extracted DOMRenderer Component
|
||||
*
|
||||
* Tests the extracted DOMRenderer component independently from the monolith.
|
||||
* Verifies that core functionality is preserved after extraction.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Extracted DOMRenderer Component', () => {
|
||||
|
||||
runner.it('should load extracted DOMRenderer component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../components/dom-renderer.js')];
|
||||
|
||||
try {
|
||||
const module = require('../components/dom-renderer.js');
|
||||
runner.expect(module.DOMRenderer).toBeTruthy();
|
||||
runner.expect(module.FloatingMenu).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedDOMRenderer = module.DOMRenderer;
|
||||
global.ExtractedFloatingMenu = module.FloatingMenu;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted DOMRenderer: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Load SectionManager from our extracted core
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
runner.expect(renderer).toBeInstanceOf(DOMRenderer);
|
||||
runner.expect(renderer.sectionManager).toBe(sectionManager);
|
||||
runner.expect(renderer.container).toBe(container);
|
||||
runner.expect(renderer.editingSections).toBeInstanceOf(Set);
|
||||
});
|
||||
|
||||
runner.it('should preserve section rendering functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// This should not throw an error
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
// Check that content was rendered
|
||||
runner.expect(container.innerHTML.length > 100).toBeTruthy();
|
||||
runner.expect(container.innerHTML).toContain('Test Heading');
|
||||
});
|
||||
|
||||
runner.it('should preserve findSectionElement functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = renderer.findSectionElement(sectionId);
|
||||
|
||||
runner.expect(element).toBeTruthy();
|
||||
runner.expect(element.getAttribute('data-section-id')).toBe(sectionId);
|
||||
});
|
||||
|
||||
runner.it('should preserve event tracking functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Should have trackEvent method
|
||||
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
|
||||
|
||||
// Should be able to track an event
|
||||
renderer.trackEvent('test-event', { data: 'test' });
|
||||
|
||||
// Should have getEventStats method
|
||||
runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
|
||||
|
||||
const stats = renderer.getEventStats();
|
||||
runner.expect(typeof stats === 'object').toBeTruthy();
|
||||
runner.expect(stats).toHaveProperty('stats');
|
||||
runner.expect(stats).toHaveProperty('totalEvents');
|
||||
runner.expect(stats).toHaveProperty('recentEvents');
|
||||
});
|
||||
|
||||
runner.it('should preserve editor showing functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// showEditor should not throw error
|
||||
try {
|
||||
renderer.showEditor(sectionId, 'test content');
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
|
||||
|
||||
// Check that editing state was set
|
||||
runner.expect(renderer.editingSections.has(sectionId)).toBeTruthy();
|
||||
} catch (error) {
|
||||
throw new Error(`showEditor failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve FloatingMenu functionality', () => {
|
||||
const FloatingMenu = global.ExtractedFloatingMenu;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const floatingMenu = new FloatingMenu(sectionId, 'text', renderer);
|
||||
|
||||
runner.expect(floatingMenu.sectionId).toBe(sectionId);
|
||||
runner.expect(floatingMenu.type).toBe('text');
|
||||
runner.expect(floatingMenu.renderer).toBe(renderer);
|
||||
runner.expect(floatingMenu.isVisible).toBeFalsy();
|
||||
|
||||
// Test show/hide functionality
|
||||
const content = document.createElement('div');
|
||||
content.textContent = 'Test content';
|
||||
|
||||
floatingMenu.show(content);
|
||||
runner.expect(floatingMenu.isVisible).toBeTruthy();
|
||||
|
||||
floatingMenu.hide();
|
||||
runner.expect(floatingMenu.isVisible).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should handle section click events', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = renderer.findSectionElement(sectionId);
|
||||
|
||||
// Simulate a click event
|
||||
const clickEvent = new Event('click', { bubbles: true });
|
||||
Object.defineProperty(clickEvent, 'target', { value: element });
|
||||
|
||||
// Should not throw error
|
||||
try {
|
||||
renderer.handleSectionClick(clickEvent);
|
||||
runner.expect(true).toBeTruthy();
|
||||
} catch (error) {
|
||||
throw new Error(`handleSectionClick failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Comparative test - verify extracted component behaves similarly to original
|
||||
runner.it('should behave similarly to original monolithic component', () => {
|
||||
// Load both components
|
||||
const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
const extractedModule = require('../components/dom-renderer.js');
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
|
||||
const originalSectionManager = new originalModule.SectionManager();
|
||||
const extractedSectionManager = new sectionModule.SectionManager();
|
||||
|
||||
const originalContainer = document.createElement('div');
|
||||
originalContainer.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const extractedContainer = document.createElement('div');
|
||||
extractedContainer.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const originalRenderer = new originalModule.DOMRenderer(originalSectionManager, originalContainer);
|
||||
const extractedRenderer = new extractedModule.DOMRenderer(extractedSectionManager, extractedContainer);
|
||||
|
||||
const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
|
||||
|
||||
// Create sections with both
|
||||
const originalSections = originalSectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
const extractedSections = extractedSectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// Render with both
|
||||
originalRenderer.renderAllSections(originalSections);
|
||||
extractedRenderer.renderAllSections(extractedSections);
|
||||
|
||||
// Should have rendered content
|
||||
runner.expect(originalContainer.innerHTML.length > 100).toBeTruthy();
|
||||
runner.expect(extractedContainer.innerHTML.length > 100).toBeTruthy();
|
||||
|
||||
// Should have same number of section elements
|
||||
const originalSectionElements = originalContainer.querySelectorAll('.ui-edit-section');
|
||||
const extractedSectionElements = extractedContainer.querySelectorAll('.ui-edit-section');
|
||||
|
||||
runner.expect(extractedSectionElements.length).toBe(originalSectionElements.length);
|
||||
|
||||
// Should have similar event stats structure
|
||||
const originalStats = originalRenderer.getEventStats();
|
||||
const extractedStats = extractedRenderer.getEventStats();
|
||||
|
||||
runner.expect(extractedStats).toHaveProperty('stats');
|
||||
runner.expect(extractedStats).toHaveProperty('totalEvents');
|
||||
runner.expect(extractedStats).toHaveProperty('recentEvents');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing Extracted DOMRenderer Component');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Extracted DOMRenderer tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Extracted SectionManager Component
|
||||
*
|
||||
* Tests the extracted SectionManager component independently from the monolith.
|
||||
* Verifies that all functionality is preserved after extraction.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Extracted SectionManager Component', () => {
|
||||
|
||||
runner.it('should load extracted SectionManager component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../core/section-manager.js')];
|
||||
|
||||
try {
|
||||
const module = require('../core/section-manager.js');
|
||||
runner.expect(module.SectionManager).toBeTruthy();
|
||||
runner.expect(module.Section).toBeTruthy();
|
||||
runner.expect(module.EditState).toBeTruthy();
|
||||
runner.expect(module.SectionType).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = module.SectionManager;
|
||||
global.ExtractedSection = module.Section;
|
||||
global.ExtractedEditState = module.EditState;
|
||||
global.ExtractedSectionType = module.SectionType;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted SectionManager: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
|
||||
const manager = new SectionManager();
|
||||
runner.expect(manager).toBeInstanceOf(SectionManager);
|
||||
runner.expect(manager.sections).toBeInstanceOf(Map);
|
||||
runner.expect(manager.listeners).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
runner.it('should preserve section creation functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
|
||||
const sections = manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
runner.expect(Array.isArray(sections)).toBeTruthy();
|
||||
runner.expect(sections.length).toBe(2);
|
||||
runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
|
||||
runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
|
||||
});
|
||||
|
||||
runner.it('should preserve section editing functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// Test start editing
|
||||
const content = manager.startEditing(sectionId);
|
||||
runner.expect(content).toContain('Test');
|
||||
|
||||
const section = manager.sections.get(sectionId);
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Test stop editing
|
||||
section.stopEditing();
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should preserve event system functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
let eventFired = false;
|
||||
let eventData = null;
|
||||
|
||||
manager.on('test-event', (data) => {
|
||||
eventFired = true;
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
manager.emit('test-event', { test: 'data' });
|
||||
|
||||
runner.expect(eventFired).toBeTruthy();
|
||||
runner.expect(eventData).toEqual({ test: 'data' });
|
||||
});
|
||||
|
||||
runner.it('should preserve document status functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const status = manager.getDocumentStatus();
|
||||
|
||||
runner.expect(status).toHaveProperty('totalSections');
|
||||
runner.expect(status).toHaveProperty('editingSections');
|
||||
runner.expect(status.totalSections).toBe(1);
|
||||
});
|
||||
|
||||
runner.it('should preserve getAllSections functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = '# One\nContent\n\n# Two\nMore content';
|
||||
manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
const allSections = manager.getAllSections();
|
||||
runner.expect(Array.isArray(allSections)).toBeTruthy();
|
||||
runner.expect(allSections.length).toBe(2);
|
||||
});
|
||||
|
||||
runner.it('should preserve section splitting functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Original\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
|
||||
const newSections = manager.handleSectionSplit(sectionId, newContent);
|
||||
|
||||
runner.expect(Array.isArray(newSections)).toBeTruthy();
|
||||
runner.expect(newSections.length).toBe(2);
|
||||
runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
|
||||
});
|
||||
|
||||
runner.it('should preserve Section class functionality', () => {
|
||||
const Section = global.ExtractedSection;
|
||||
const EditState = global.ExtractedEditState;
|
||||
|
||||
const section = new Section('test-id', '# Test Content', 'heading');
|
||||
|
||||
runner.expect(section.id).toBe('test-id');
|
||||
runner.expect(section.currentMarkdown).toBe('# Test Content');
|
||||
runner.expect(section.type).toBe('heading');
|
||||
runner.expect(section.state).toBe(EditState.ORIGINAL);
|
||||
});
|
||||
|
||||
runner.it('should preserve Section ID generation', () => {
|
||||
const Section = global.ExtractedSection;
|
||||
|
||||
const id1 = Section.generateId('# Test Heading', 0);
|
||||
const id2 = Section.generateId('# Different Heading', 1);
|
||||
|
||||
runner.expect(typeof id1 === 'string').toBeTruthy();
|
||||
runner.expect(typeof id2 === 'string').toBeTruthy();
|
||||
runner.expect(id1).toContain('section-');
|
||||
runner.expect(id2).toContain('section-');
|
||||
runner.expect(id1 !== id2).toBeTruthy(); // Should be unique
|
||||
});
|
||||
|
||||
runner.it('should preserve Section type detection', () => {
|
||||
const Section = global.ExtractedSection;
|
||||
const SectionType = global.ExtractedSectionType;
|
||||
|
||||
runner.expect(Section.detectType('# Heading')).toBe(SectionType.HEADING);
|
||||
runner.expect(Section.detectType('')).toBe(SectionType.IMAGE);
|
||||
runner.expect(Section.detectType('```code```')).toBe(SectionType.CODE);
|
||||
runner.expect(Section.detectType('Regular paragraph')).toBe(SectionType.PARAGRAPH);
|
||||
});
|
||||
|
||||
// Comparative test - verify extracted component behaves identically to original
|
||||
runner.it('should behave identically to original monolithic component', () => {
|
||||
// Load both components
|
||||
const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
const extractedModule = require('../core/section-manager.js');
|
||||
|
||||
const originalManager = new originalModule.SectionManager();
|
||||
const extractedManager = new extractedModule.SectionManager();
|
||||
|
||||
const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
|
||||
|
||||
// Debug: Check what each component produces
|
||||
console.log('Creating sections with original component...');
|
||||
const originalSections = originalManager.createSectionsFromMarkdown(testMarkdown);
|
||||
console.log(`Original produced ${originalSections.length} sections`);
|
||||
|
||||
console.log('Creating sections with extracted component...');
|
||||
const extractedSections = extractedManager.createSectionsFromMarkdown(testMarkdown);
|
||||
console.log(`Extracted produced ${extractedSections.length} sections`);
|
||||
|
||||
if (originalSections.length > 0) {
|
||||
console.log('Original first section:', originalSections[0].currentMarkdown);
|
||||
}
|
||||
if (extractedSections.length > 0) {
|
||||
console.log('Extracted first section:', extractedSections[0].currentMarkdown);
|
||||
}
|
||||
|
||||
// Should have same number of sections
|
||||
runner.expect(extractedSections.length).toBe(originalSections.length);
|
||||
|
||||
// Should have same content
|
||||
for (let i = 0; i < originalSections.length; i++) {
|
||||
runner.expect(extractedSections[i].currentMarkdown).toBe(originalSections[i].currentMarkdown);
|
||||
runner.expect(extractedSections[i].type).toBe(originalSections[i].type);
|
||||
}
|
||||
|
||||
// Should have same document status structure
|
||||
const originalStatus = originalManager.getDocumentStatus();
|
||||
const extractedStatus = extractedManager.getDocumentStatus();
|
||||
|
||||
console.log('Original status:', originalStatus);
|
||||
console.log('Extracted status:', extractedStatus);
|
||||
|
||||
runner.expect(extractedStatus.totalSections).toBe(originalStatus.totalSections);
|
||||
runner.expect(extractedStatus.editingSections).toBe(originalStatus.editingSections);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing Extracted SectionManager Component');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Extracted SectionManager tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Full Integration Test
|
||||
*
|
||||
* Tests that all extracted components (SectionManager, DOMRenderer,
|
||||
* DebugPanel, DocumentControls) work together as a complete system.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Full Component Integration Tests', () => {
|
||||
|
||||
runner.it('should load all extracted components', () => {
|
||||
try {
|
||||
// Load all extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
runner.expect(sectionModule.SectionManager).toBeTruthy();
|
||||
runner.expect(domModule.DOMRenderer).toBeTruthy();
|
||||
runner.expect(debugModule.DebugPanel).toBeTruthy();
|
||||
runner.expect(controlsModule.DocumentControls).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = sectionModule.SectionManager;
|
||||
global.ExtractedDOMRenderer = domModule.DOMRenderer;
|
||||
global.ExtractedDebugPanel = debugModule.DebugPanel;
|
||||
global.ExtractedDocumentControls = controlsModule.DocumentControls;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted components: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support complete document editing workflow with all components', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Setup DOM container
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Create all components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Setup document controls
|
||||
documentControls.create();
|
||||
|
||||
// Wire up event handlers for debugging
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
|
||||
});
|
||||
|
||||
// Test workflow: Create document
|
||||
const testMarkdown = `# Document Title
|
||||
Introduction paragraph with some content.
|
||||
|
||||
## Section A
|
||||
Content for section A with details.
|
||||
|
||||

|
||||
|
||||
### Subsection A.1
|
||||
More detailed content here.`;
|
||||
|
||||
// Create sections
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
runner.expect(sections.length).toBe(4);
|
||||
|
||||
// Render sections
|
||||
domRenderer.renderAllSections(sections);
|
||||
const renderedElements = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedElements.length).toBe(sections.length);
|
||||
|
||||
// Test editing workflow
|
||||
const firstSection = sections[0];
|
||||
sectionManager.startEditing(firstSection.id);
|
||||
runner.expect(firstSection.isEditing()).toBeTruthy();
|
||||
|
||||
// Check debug messages were created
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2); // sections-created + edit-started
|
||||
|
||||
// Test document controls functionality
|
||||
const controlPanel = documentControls.getControlPanel();
|
||||
runner.expect(controlPanel).toBeTruthy();
|
||||
runner.expect(document.getElementById('save-document')).toBeTruthy();
|
||||
runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support debug panel integration with document controls', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Create components
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Setup document controls
|
||||
documentControls.create();
|
||||
|
||||
// Setup debug panel toggle handler
|
||||
const handlers = {
|
||||
'toggle-debug': () => debugPanel.toggle()
|
||||
};
|
||||
documentControls.setEventHandlers(handlers);
|
||||
|
||||
// Test debug toggle functionality
|
||||
const debugButton = documentControls.getButton('toggle-debug');
|
||||
runner.expect(debugButton).toBeTruthy();
|
||||
|
||||
// Add some debug messages
|
||||
debugPanel.addMessage('Test message 1', 'INFO');
|
||||
debugPanel.addMessage('Test message 2', 'ERROR');
|
||||
|
||||
// Simulate button click to show debug panel
|
||||
debugButton.click();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
|
||||
// Simulate button click to hide debug panel
|
||||
debugButton.click();
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support event-driven communication between all components', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Setup container
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Create components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
// Setup comprehensive event handling
|
||||
let eventLog = [];
|
||||
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
eventLog.push(`sections-created: ${data.count} sections`);
|
||||
debugPanel.addMessage(`Sections created: ${data.count}`, 'INFO');
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
eventLog.push(`edit-started: ${data.sectionId}`);
|
||||
debugPanel.addMessage(`Edit started: ${data.sectionId}`, 'DEBUG');
|
||||
});
|
||||
|
||||
sectionManager.on('changes-accepted', (data) => {
|
||||
eventLog.push(`changes-accepted: ${data.sectionId}`);
|
||||
debugPanel.addMessage(`Changes accepted: ${data.sectionId}`, 'SUCCESS');
|
||||
});
|
||||
|
||||
// Test complete workflow
|
||||
const testMarkdown = '# Test\nContent for testing';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
// Start editing
|
||||
sectionManager.startEditing(sections[0].id);
|
||||
sectionManager.updateContent(sections[0].id, '# Updated Test\nUpdated content');
|
||||
sectionManager.acceptChanges(sections[0].id);
|
||||
|
||||
// Verify events were logged
|
||||
runner.expect(eventLog.length).toBe(3);
|
||||
runner.expect(eventLog[0]).toContain('sections-created');
|
||||
runner.expect(eventLog[1]).toContain('edit-started');
|
||||
runner.expect(eventLog[2]).toContain('changes-accepted');
|
||||
|
||||
// Verify debug messages were created
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(3);
|
||||
|
||||
// Test document controls status update
|
||||
const status = sectionManager.getDocumentStatus();
|
||||
documentControls.updateStatus(status);
|
||||
runner.expect(documentControls.lastStatus).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should handle error scenarios gracefully across components', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Test component creation without proper DOM setup
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// These should not throw errors
|
||||
try {
|
||||
debugPanel.toggle(); // No DOM elements
|
||||
debugPanel.update(); // No DOM elements
|
||||
documentControls.show(); // No control panel created yet
|
||||
documentControls.hide(); // No control panel created yet
|
||||
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
|
||||
} catch (error) {
|
||||
throw new Error(`Components should handle missing DOM gracefully: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test section manager with invalid input
|
||||
const sectionManager = new SectionManager();
|
||||
const sections = sectionManager.createSectionsFromMarkdown('');
|
||||
runner.expect(sections.length).toBe(0);
|
||||
|
||||
// Test DOM renderer with invalid container
|
||||
try {
|
||||
const invalidRenderer = new DOMRenderer(sectionManager, null);
|
||||
runner.expect(invalidRenderer.container).toBeFalsy();
|
||||
} catch (error) {
|
||||
// This is acceptable - constructor might validate input
|
||||
runner.expect(typeof error.message === 'string').toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support scalable architecture with component lifecycle', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Test multiple instances
|
||||
const sectionManager1 = new SectionManager();
|
||||
const sectionManager2 = new SectionManager();
|
||||
const debugPanel1 = new DebugPanel();
|
||||
const debugPanel2 = new DebugPanel();
|
||||
|
||||
// Each should be independent
|
||||
debugPanel1.addMessage('Message from panel 1', 'INFO');
|
||||
debugPanel2.addMessage('Message from panel 2', 'ERROR');
|
||||
|
||||
runner.expect(debugPanel1.getMessageCount()).toBe(1);
|
||||
runner.expect(debugPanel2.getMessageCount()).toBe(1);
|
||||
|
||||
// Test section managers are independent
|
||||
const sections1 = sectionManager1.createSectionsFromMarkdown('# Document 1');
|
||||
const sections2 = sectionManager2.createSectionsFromMarkdown('# Document 2');
|
||||
|
||||
runner.expect(sections1.length).toBe(1);
|
||||
runner.expect(sections2.length).toBe(1);
|
||||
runner.expect(sections1[0]).toBeTruthy();
|
||||
runner.expect(sections2[0]).toBeTruthy();
|
||||
|
||||
// IDs should be different (each section gets unique ID)
|
||||
const id1 = sections1[0].id;
|
||||
const id2 = sections2[0].id;
|
||||
runner.expect(id1 !== id2).toBeTruthy();
|
||||
|
||||
// Test document controls lifecycle
|
||||
const controls1 = new DocumentControls();
|
||||
const controls2 = new DocumentControls();
|
||||
|
||||
controls1.create();
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
|
||||
|
||||
controls2.create(); // Should replace the first one
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
|
||||
|
||||
controls2.destroy();
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running Full Component Integration Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Full integration tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocumentNavigator Live Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
scroll-margin-top: 100px; /* Account for navigator */
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #34495e;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #7f8c8d;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: #fff3cd;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #ffc107;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="demo-header">
|
||||
<h1>📋 DocumentNavigator Live Demo</h1>
|
||||
<p>This page demonstrates the Substack-style floating navigation widget in action.</p>
|
||||
<p><strong>Look for the hamburger menu (☰) on the left side!</strong></p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>Features to test:</strong><br>
|
||||
• Click the hamburger menu to expand navigation<br>
|
||||
• Click any heading in the navigator to jump to it<br>
|
||||
• Scroll and watch the current section highlight<br>
|
||||
• Try keyboard shortcuts (Enter/Space to toggle, Escape to close)<br>
|
||||
• Resize window to test responsive behavior
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="markdown-content" class="demo-content">
|
||||
<h1 id="introduction">1. Introduction to MarkiTect</h1>
|
||||
<div class="content-section">
|
||||
<p>MarkiTect is an advanced markdown processing engine that provides sophisticated document management capabilities. This demo showcases the DocumentNavigator widget, which provides Substack-style navigation for long-form documents.</p>
|
||||
|
||||
<p>The navigator automatically extracts headings from your content and builds a hierarchical table of contents that floats elegantly on the side of your document.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="features">1.1 Core Features</h2>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator widget includes numerous advanced features designed for optimal user experience:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Automatic Heading Detection</strong>: Scans document for H1, H2, H3 elements</li>
|
||||
<li><strong>Hierarchical Structure</strong>: Maintains proper heading hierarchy with indentation</li>
|
||||
<li><strong>Scroll Spy</strong>: Highlights current section as you scroll</li>
|
||||
<li><strong>Smooth Navigation</strong>: Animated scrolling to clicked sections</li>
|
||||
<li><strong>Responsive Design</strong>: Auto-hides on mobile devices</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 id="responsive">1.1.1 Responsive Behavior</h3>
|
||||
<div class="content-section">
|
||||
<p>The navigator intelligently adapts to different screen sizes. On desktop computers, it remains visible as a floating panel. On mobile devices, it automatically hides to preserve screen real estate for content.</p>
|
||||
|
||||
<p>Try resizing your browser window to see this behavior in action. The navigator will disappear when the viewport becomes narrow (under 768px wide).</p>
|
||||
</div>
|
||||
|
||||
<h3 id="accessibility">1.1.2 Accessibility Features</h3>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator is built with accessibility in mind:</p>
|
||||
|
||||
<ul>
|
||||
<li>Full keyboard navigation support</li>
|
||||
<li>ARIA labels and proper semantic markup</li>
|
||||
<li>Screen reader compatibility</li>
|
||||
<li>High contrast hover states</li>
|
||||
<li>Focus management</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="implementation">1.2 Implementation Details</h2>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator is implemented as a modular ES6 class that extends our base UIWidget class. This follows the planned plugin architecture for MarkiTect widgets.</p>
|
||||
|
||||
<p>Key implementation highlights include:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>extractHeadings()</code> - Scans DOM for heading elements</li>
|
||||
<li><code>buildNavigationTree()</code> - Creates hierarchical structure</li>
|
||||
<li><code>handleScroll()</code> - Manages scroll spy functionality</li>
|
||||
<li><code>navigateToHeading()</code> - Handles smooth scrolling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h1 id="architecture">2. Widget Architecture</h1>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator follows a clean architectural pattern that separates concerns and provides maximum flexibility for customization and extension.</p>
|
||||
|
||||
<p>The widget is designed as part of a larger plugin ecosystem that will allow developers to create custom UI components that can be loaded dynamically and configured independently.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="base-classes">2.1 Base Class Hierarchy</h2>
|
||||
<div class="content-section">
|
||||
<p>Our widget system is built on a foundation of base classes that provide common functionality:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Widget</strong>: Core functionality (events, state, lifecycle)</li>
|
||||
<li><strong>UIWidget</strong>: DOM manipulation and visual behavior</li>
|
||||
<li><strong>InteractiveWidget</strong>: Event handling and user interaction</li>
|
||||
</ul>
|
||||
|
||||
<p>DocumentNavigator extends UIWidget directly since it doesn't require complex interaction handling beyond basic click and keyboard events.</p>
|
||||
</div>
|
||||
|
||||
<h3 id="events">2.1.1 Event System</h3>
|
||||
<div class="content-section">
|
||||
<p>The widget uses a custom event system built on the native EventTarget API. This allows for clean separation of concerns and easy integration with other components.</p>
|
||||
|
||||
<p>Key events emitted by DocumentNavigator:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>rendered</code> - Widget has been rendered to DOM</li>
|
||||
<li><code>navigate</code> - User navigated to a heading</li>
|
||||
<li><code>toggle</code> - Widget was expanded or collapsed</li>
|
||||
<li><code>theme-changed</code> - Theme was changed</li>
|
||||
<li><code>destroyed</code> - Widget was destroyed</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 id="state">2.1.2 State Management</h3>
|
||||
<div class="content-section">
|
||||
<p>State management is handled through a simple Map-based system that provides reactive updates and event emission when state changes occur.</p>
|
||||
|
||||
<p>This approach is lightweight but powerful enough for most widget use cases while remaining debuggable and predictable.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="plugin-system">2.2 Plugin System Integration</h2>
|
||||
<div class="content-section">
|
||||
<p>While the current implementation works standalone, it's designed to integrate seamlessly with our planned plugin system. The plugin definition includes:</p>
|
||||
|
||||
<ul>
|
||||
<li>Metadata and versioning information</li>
|
||||
<li>Dependency declarations</li>
|
||||
<li>Default configuration options</li>
|
||||
<li>Lifecycle hooks</li>
|
||||
<li>Theme variants</li>
|
||||
<li>Development helpers</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h1 id="usage">3. Usage Examples</h1>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator can be used in several ways, from simple instantiation to advanced configuration with custom themes and behavior.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="basic-usage">3.1 Basic Usage</h2>
|
||||
<div class="content-section">
|
||||
<p>The simplest way to use DocumentNavigator is with default settings:</p>
|
||||
|
||||
<pre><code>const navigator = new DocumentNavigator();
|
||||
await navigator.initialize();
|
||||
await navigator.render();</code></pre>
|
||||
|
||||
<p>This creates a navigator with default settings that will scan the entire document for headings and display them in a collapsible panel on the left side.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="advanced-usage">3.2 Advanced Configuration</h2>
|
||||
<div class="content-section">
|
||||
<p>For more control, you can specify detailed configuration options:</p>
|
||||
|
||||
<pre><code>const navigator = new DocumentNavigator({
|
||||
position: 'right',
|
||||
collapsed: false,
|
||||
theme: 'dark',
|
||||
maxHeadingLevel: 4,
|
||||
enableScrollSpy: true,
|
||||
smoothScroll: true
|
||||
});</code></pre>
|
||||
|
||||
<p>This creates a navigator on the right side that starts expanded, includes H4 headings, and uses the dark theme.</p>
|
||||
</div>
|
||||
|
||||
<h3 id="theming">3.2.1 Custom Theming</h3>
|
||||
<div class="content-section">
|
||||
<p>The navigator supports multiple built-in themes and can be extended with custom themes. The theming system integrates with MarkiTect's document themes for consistent styling.</p>
|
||||
|
||||
<p>Available themes include <code>default</code>, <code>dark</code>, and <code>minimal</code>, each optimized for different use cases and aesthetics.</p>
|
||||
</div>
|
||||
|
||||
<h1 id="testing">4. Testing and Quality</h1>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator implementation follows Test-Driven Development (TDD) methodology with comprehensive test coverage ensuring reliability and maintainability.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="test-coverage">4.1 Test Coverage</h2>
|
||||
<div class="content-section">
|
||||
<p>Our test suite covers all major functionality:</p>
|
||||
|
||||
<ul>
|
||||
<li>Widget instantiation and configuration</li>
|
||||
<li>DOM rendering and element creation</li>
|
||||
<li>Heading extraction and hierarchy building</li>
|
||||
<li>Navigation and smooth scrolling</li>
|
||||
<li>Expand/collapse animations</li>
|
||||
<li>Scroll spy functionality</li>
|
||||
<li>Responsive behavior</li>
|
||||
<li>Keyboard navigation</li>
|
||||
<li>Event emission</li>
|
||||
<li>Edge cases and error handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="performance">4.2 Performance Considerations</h2>
|
||||
<div class="content-section">
|
||||
<p>The navigator is optimized for performance with several key strategies:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Throttled Scroll Events</strong>: Scroll spy updates are throttled to 100ms intervals</li>
|
||||
<li><strong>Efficient DOM Queries</strong>: Heading extraction is done once and cached</li>
|
||||
<li><strong>Conditional Rendering</strong>: Navigator only renders if minimum heading count is met</li>
|
||||
<li><strong>Memory Management</strong>: Proper cleanup prevents memory leaks</li>
|
||||
<li><strong>Responsive Loading</strong>: Navigator automatically hides on mobile to save resources</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h1 id="conclusion">5. Conclusion</h1>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator widget successfully brings Substack-style navigation to MarkiTect documents. It provides an intuitive, accessible, and performant way for users to navigate long-form content.</p>
|
||||
|
||||
<p>The implementation demonstrates the power of our widget architecture approach, with clean separation of concerns, comprehensive testing, and excellent extensibility for future enhancements.</p>
|
||||
|
||||
<p><strong>Scroll back to the top and try the navigation features!</strong> The hamburger menu should be visible on the left side of your screen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load widget classes -->
|
||||
<script type="module">
|
||||
// Import our widget classes
|
||||
import { Widget } from '../widgets/base/Widget.js';
|
||||
import { UIWidget } from '../widgets/base/UIWidget.js';
|
||||
import { DocumentNavigator } from '../widgets/navigation/DocumentNavigator.js';
|
||||
|
||||
// Make classes available globally for demo
|
||||
window.Widget = Widget;
|
||||
window.UIWidget = UIWidget;
|
||||
window.DocumentNavigator = DocumentNavigator;
|
||||
|
||||
// Initialize navigator on page load
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('🧭 Initializing DocumentNavigator demo...');
|
||||
|
||||
try {
|
||||
// Create navigator with demo settings
|
||||
const navigator = new DocumentNavigator({
|
||||
container: document.getElementById('markdown-content'),
|
||||
position: 'left',
|
||||
collapsed: true,
|
||||
theme: 'default',
|
||||
enableScrollSpy: true,
|
||||
autoHide: true,
|
||||
maxHeadingLevel: 3,
|
||||
minHeadings: 1 // Show navigator even with few headings for demo
|
||||
});
|
||||
|
||||
// Initialize and render
|
||||
await navigator.initialize();
|
||||
const element = await navigator.render();
|
||||
|
||||
if (element) {
|
||||
console.log('✅ DocumentNavigator initialized successfully!');
|
||||
console.log(` Found ${navigator.headings.length} headings`);
|
||||
console.log(' Click the hamburger menu (☰) to expand navigation');
|
||||
} else {
|
||||
console.log('ℹ️ DocumentNavigator not rendered (insufficient headings)');
|
||||
}
|
||||
|
||||
// Add some debugging helpers
|
||||
window.navigator = navigator;
|
||||
window.testNavigator = {
|
||||
expand: () => navigator.expand(),
|
||||
collapse: () => navigator.collapse(),
|
||||
toggle: () => navigator.toggle(),
|
||||
showHeadings: () => console.table(navigator.headings),
|
||||
showTree: () => console.log(navigator.navigationTree)
|
||||
};
|
||||
|
||||
console.log('🔧 Debugging helpers available:');
|
||||
console.log(' window.navigator - navigator instance');
|
||||
console.log(' window.testNavigator - helper functions');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DocumentNavigator initialization failed:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,285 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Real User Functionality Tests
|
||||
*
|
||||
* This test file validates the actual functionality that users experience,
|
||||
* not just internal API calls. It tests the complete user workflow.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Real User Functionality Tests', () => {
|
||||
|
||||
runner.it('should allow users to edit content and see changes in DOM', () => {
|
||||
// Load all extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DebugPanel } = debugModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
// Setup DOM container
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Create components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Setup document controls
|
||||
documentControls.create();
|
||||
|
||||
// Create sections from test markdown
|
||||
const testMarkdown = `# Original Title\nOriginal content that should be editable.`;
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const firstSection = sections[0];
|
||||
const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
|
||||
// Verify original content is rendered
|
||||
runner.expect(sectionElement.innerHTML).toContain('Original Title');
|
||||
|
||||
// Simulate user clicking on section
|
||||
const clickEvent = new Event('click', { bubbles: true });
|
||||
sectionElement.dispatchEvent(clickEvent);
|
||||
|
||||
// Verify editing state is active
|
||||
runner.expect(firstSection.isEditing()).toBeTruthy();
|
||||
|
||||
// Find the floating menu and edit controls
|
||||
const floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
runner.expect(floatingMenu).toBeTruthy();
|
||||
|
||||
const textarea = floatingMenu.querySelector('textarea');
|
||||
const acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
|
||||
|
||||
runner.expect(textarea).toBeTruthy();
|
||||
runner.expect(acceptButton).toBeTruthy();
|
||||
|
||||
// Simulate user editing content
|
||||
const newContent = '# Updated Title\nCompletely new content added by user.';
|
||||
textarea.value = newContent;
|
||||
|
||||
// Simulate user clicking accept
|
||||
acceptButton.click();
|
||||
|
||||
// Verify section is no longer editing
|
||||
runner.expect(firstSection.isEditing()).toBeFalsy();
|
||||
|
||||
// Verify floating menu is gone
|
||||
const menuAfterAccept = document.querySelector('.ui-edit-floating-menu');
|
||||
runner.expect(menuAfterAccept).toBeFalsy();
|
||||
|
||||
// CRITICAL TEST: Verify DOM was actually updated with new content
|
||||
const updatedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(updatedElement.innerHTML).toContain('Updated Title');
|
||||
runner.expect(updatedElement.innerHTML).toContain('Completely new content');
|
||||
runner.expect(updatedElement.innerHTML).not.toContain('Original Title');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should allow users to reset all changes', () => {
|
||||
// Setup similar to above
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
// Create and modify content
|
||||
const testMarkdown = `# Test Section\nOriginal content for reset test.`;
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const firstSection = sections[0];
|
||||
|
||||
// Make changes to the section
|
||||
sectionManager.startEditing(firstSection.id);
|
||||
sectionManager.updateContent(firstSection.id, '# Modified Title\nModified content.');
|
||||
sectionManager.acceptChanges(firstSection.id);
|
||||
|
||||
// Verify changes are applied
|
||||
let sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(sectionElement.innerHTML).toContain('Modified Title');
|
||||
runner.expect(firstSection.hasChanges()).toBeTruthy();
|
||||
|
||||
// Test reset functionality
|
||||
const resetButton = documentControls.getButton('reset-all');
|
||||
runner.expect(resetButton).toBeTruthy();
|
||||
|
||||
// Click reset button
|
||||
resetButton.click();
|
||||
|
||||
// Verify content is reset
|
||||
sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(sectionElement.innerHTML).toContain('Test Section');
|
||||
runner.expect(sectionElement.innerHTML).not.toContain('Modified Title');
|
||||
runner.expect(firstSection.hasChanges()).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should handle cancel operations correctly', () => {
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = `# Cancel Test\nContent that should remain unchanged.`;
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const firstSection = sections[0];
|
||||
const originalContent = firstSection.currentMarkdown;
|
||||
|
||||
// Start editing
|
||||
const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
sectionElement.click();
|
||||
|
||||
// Make changes but cancel them
|
||||
const floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
const textarea = floatingMenu.querySelector('textarea');
|
||||
const cancelButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Cancel'));
|
||||
|
||||
textarea.value = '# This should be cancelled\nThis content should not appear.';
|
||||
cancelButton.click();
|
||||
|
||||
// Verify content is unchanged
|
||||
const unchangedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(unchangedElement.innerHTML).toContain('Cancel Test');
|
||||
runner.expect(unchangedElement.innerHTML).not.toContain('This should be cancelled');
|
||||
runner.expect(firstSection.currentMarkdown).toBe(originalContent);
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should validate the complete editing workflow', () => {
|
||||
// This test validates the entire user experience end-to-end
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DebugPanel } = debugModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
// Multi-section document
|
||||
const testMarkdown = `# Document Title
|
||||
Introduction paragraph.
|
||||
|
||||
## Section A
|
||||
Content for section A.
|
||||
|
||||
## Section B
|
||||
Content for section B.`;
|
||||
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
// Verify all sections are rendered
|
||||
const renderedSections = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedSections.length).toBe(sections.length);
|
||||
|
||||
// Test editing multiple sections
|
||||
const firstSection = sections[0];
|
||||
const secondSection = sections[2]; // Section A
|
||||
|
||||
// Edit first section
|
||||
renderedSections[0].click();
|
||||
let floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
let textarea = floatingMenu.querySelector('textarea');
|
||||
let acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
|
||||
|
||||
textarea.value = '# Updated Document Title\nUpdated introduction.';
|
||||
acceptButton.click();
|
||||
|
||||
// Edit second section
|
||||
renderedSections[2].click();
|
||||
floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
textarea = floatingMenu.querySelector('textarea');
|
||||
acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
|
||||
|
||||
textarea.value = '## Updated Section A\nCompletely new content for section A.';
|
||||
acceptButton.click();
|
||||
|
||||
// Verify both sections were updated
|
||||
const updatedSections = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(updatedSections[0].innerHTML).toContain('Updated Document Title');
|
||||
runner.expect(updatedSections[2].innerHTML).toContain('Updated Section A');
|
||||
|
||||
// Test reset restores all sections
|
||||
const resetButton = documentControls.getButton('reset-all');
|
||||
resetButton.click();
|
||||
|
||||
const resetSections = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(resetSections[0].innerHTML).toContain('Document Title');
|
||||
runner.expect(resetSections[0].innerHTML).not.toContain('Updated Document Title');
|
||||
runner.expect(resetSections[2].innerHTML).toContain('Section A');
|
||||
runner.expect(resetSections[2].innerHTML).not.toContain('Updated Section A');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running Real User Functionality Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Real user functionality tests completed');
|
||||
console.log('These tests validate what users actually experience, not just internal APIs');
|
||||
});
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for SectionManager Component Extraction
|
||||
*
|
||||
* Tests the extraction of SectionManager from the monolithic editor.js
|
||||
* Ensures all functionality is preserved during refactoring.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// First, let's define what the SectionManager API should look like
|
||||
const EXPECTED_SECTION_MANAGER_API = [
|
||||
'constructor',
|
||||
'createSectionsFromMarkdown',
|
||||
'startEditing',
|
||||
'stopEditing',
|
||||
'getAllSections',
|
||||
'sections', // Map property, not method
|
||||
'getDocumentStatus',
|
||||
'getDocumentMarkdown',
|
||||
'on', // event system
|
||||
'emit', // event system
|
||||
'handleSectionSplit',
|
||||
'updateContent',
|
||||
'acceptChanges',
|
||||
'cancelChanges',
|
||||
'resetSection'
|
||||
];
|
||||
|
||||
runner.describe('SectionManager Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
// This test defines what we expect from the extracted SectionManager
|
||||
const expectedMethods = EXPECTED_SECTION_MANAGER_API;
|
||||
runner.expect(expectedMethods.length).toBe(15);
|
||||
runner.expect(expectedMethods).toContain('createSectionsFromMarkdown');
|
||||
runner.expect(expectedMethods).toContain('startEditing');
|
||||
runner.expect(expectedMethods).toContain('stopEditing');
|
||||
});
|
||||
|
||||
runner.it('should extract from monolithic editor.js', () => {
|
||||
// Load the monolithic editor.js to extract SectionManager
|
||||
delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
|
||||
|
||||
try {
|
||||
const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
runner.expect(editorModule.SectionManager).toBeTruthy();
|
||||
// Set global for other tests
|
||||
global.SectionManager = editorModule.SectionManager;
|
||||
global.Section = editorModule.Section;
|
||||
global.EditState = editorModule.EditState;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve SectionManager constructor functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const manager = new SectionManager();
|
||||
runner.expect(manager).toBeInstanceOf(SectionManager);
|
||||
runner.expect(manager.sections).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
runner.it('should preserve createSectionsFromMarkdown functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
|
||||
const sections = manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
runner.expect(Array.isArray(sections)).toBeTruthy();
|
||||
runner.expect(sections.length).toBe(2);
|
||||
runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
|
||||
runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
|
||||
});
|
||||
|
||||
runner.it('should preserve section editing state management', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// Test start editing
|
||||
runner.expect(manager.startEditing(sectionId)).toBeTruthy();
|
||||
const section = manager.sections.get(sectionId);
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Test stop editing
|
||||
section.stopEditing();
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should preserve event system functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
let eventFired = false;
|
||||
let eventData = null;
|
||||
|
||||
manager.on('test-event', (data) => {
|
||||
eventFired = true;
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
manager.emit('test-event', { test: 'data' });
|
||||
|
||||
runner.expect(eventFired).toBeTruthy();
|
||||
runner.expect(eventData).toEqual({ test: 'data' });
|
||||
});
|
||||
|
||||
runner.it('should preserve document status functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const status = manager.getDocumentStatus();
|
||||
|
||||
runner.expect(status).toHaveProperty('totalSections');
|
||||
runner.expect(status).toHaveProperty('editingSections');
|
||||
runner.expect(status.totalSections).toBe(1);
|
||||
});
|
||||
|
||||
runner.it('should preserve getAllSections functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = '# One\nContent\n\n# Two\nMore content';
|
||||
manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
const allSections = manager.getAllSections();
|
||||
runner.expect(Array.isArray(allSections)).toBeTruthy();
|
||||
runner.expect(allSections.length).toBe(2);
|
||||
});
|
||||
|
||||
runner.it('should preserve section splitting functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Original\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
|
||||
const newSections = manager.handleSectionSplit(sectionId, newContent);
|
||||
|
||||
runner.expect(Array.isArray(newSections)).toBeTruthy();
|
||||
runner.expect(newSections.length).toBe(2);
|
||||
runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
|
||||
});
|
||||
});
|
||||
|
||||
// Export API tests for use during extraction
|
||||
const SECTION_MANAGER_API_TESTS = [
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (!manager.sections || !(manager.sections instanceof Map)) {
|
||||
throw new Error('sections property missing or not a Map');
|
||||
}
|
||||
},
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (typeof manager.createSectionsFromMarkdown !== 'function') {
|
||||
throw new Error('createSectionsFromMarkdown method missing');
|
||||
}
|
||||
},
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (typeof manager.startEditing !== 'function') {
|
||||
throw new Error('startEditing method missing');
|
||||
}
|
||||
},
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (typeof manager.stopEditing !== 'function') {
|
||||
throw new Error('stopEditing method missing');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_SECTION_MANAGER_API,
|
||||
SECTION_MANAGER_API_TESTS
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing SectionManager Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ SectionManager extraction tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
# Test Document
|
||||
|
||||
This is a test document to check if UI controls appear in edit mode.
|
||||
|
||||
## Section 1
|
||||
Some content here.
|
||||
@@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="generator" content="Markitect Markitect v0.8.1.dev24+gdbde13e03.d20251111">
|
||||
<title>Test Document</title>
|
||||
|
||||
<!-- Base styling for document content -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Content styling */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #2c3e50;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5em; border-bottom: 3px solid #3498db; padding-bottom: 0.5rem; }
|
||||
h2 { font-size: 2em; border-bottom: 2px solid #3498db; padding-bottom: 0.3rem; }
|
||||
h3 { font-size: 1.5em; color: #34495e; }
|
||||
|
||||
p {
|
||||
margin-bottom: 1.2rem;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #3498db;
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.control-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Control system styles -->
|
||||
<link rel="stylesheet" href="markitect/static/css/controls.css">
|
||||
|
||||
<!-- External dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
||||
onerror="console.error('CDN library failed to load - network or firewall blocking marked.js'); window.markitectMarkedError = true;"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="markitect-content">
|
||||
<h1 id="test-document">Test Document</h1>
|
||||
<p>This is a test document to check if UI controls appear in edit mode.</p>
|
||||
<h2 id="section-1">Section 1</h2>
|
||||
<p>Some content here.</p>
|
||||
<hr />
|
||||
<p><em>-- html from markdown by <a href="https://coulomb.social/open/MarkiTect" target="_blank">MarkiTect</a> on 2025-11-11 23:42:23 by <a href="https://coulomb.social/open/worsch" target="_blank">worsch</a></em></p>
|
||||
</div>
|
||||
|
||||
<!-- Core JavaScript modules -->
|
||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||
|
||||
<!-- Control system -->
|
||||
<script src="../js/controls/control-base.js"></script>
|
||||
<script src="../js/controls/status-control.js"></script>
|
||||
|
||||
<!-- Main application -->
|
||||
<script src="markitect/static/js/main.js"></script>
|
||||
|
||||
<!-- Handle CDN loading errors -->
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
if (window.markitectMarkedError) {
|
||||
console.error("CDN library failed to load - network or firewall blocking marked.js");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,215 +0,0 @@
|
||||
/**
|
||||
* UI Widget Base Class
|
||||
*
|
||||
* Extends Widget with DOM manipulation and visual functionality.
|
||||
* Base for all widgets that render UI elements.
|
||||
*/
|
||||
import { Widget } from './Widget.js';
|
||||
|
||||
export class UIWidget extends Widget {
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
|
||||
// UI properties
|
||||
this.element = null;
|
||||
this.isVisible = false;
|
||||
this.isRendered = false;
|
||||
this.theme = options.theme || 'default';
|
||||
this.cssClasses = new Set(['markitect-widget']);
|
||||
|
||||
// Animation support
|
||||
this.animationDuration = options.animationDuration || 300;
|
||||
this.enableAnimations = options.enableAnimations !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the widget to DOM (abstract method)
|
||||
*/
|
||||
async render() {
|
||||
throw new Error('render() method must be implemented by subclass');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the widget
|
||||
*/
|
||||
async show(options = {}) {
|
||||
if (!this.isRendered) {
|
||||
await this.render();
|
||||
}
|
||||
|
||||
if (this.isVisible) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.isVisible = true;
|
||||
|
||||
if (this.element) {
|
||||
if (this.enableAnimations && !options.immediate) {
|
||||
await this.animateShow();
|
||||
} else {
|
||||
this.element.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('shown');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the widget
|
||||
*/
|
||||
async hide(options = {}) {
|
||||
if (!this.isVisible) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.isVisible = false;
|
||||
|
||||
if (this.element) {
|
||||
if (this.enableAnimations && !options.immediate) {
|
||||
await this.animateHide();
|
||||
} else {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('hidden');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility
|
||||
*/
|
||||
async toggle(options = {}) {
|
||||
return this.isVisible ? this.hide(options) : this.show(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show animation (override for custom animations)
|
||||
*/
|
||||
async animateShow() {
|
||||
if (!this.element) return;
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
|
||||
this.element.style.opacity = '0';
|
||||
this.element.style.display = '';
|
||||
|
||||
// Force reflow
|
||||
this.element.offsetHeight;
|
||||
|
||||
this.element.style.opacity = '1';
|
||||
|
||||
setTimeout(() => {
|
||||
this.element.style.transition = '';
|
||||
resolve();
|
||||
}, this.animationDuration);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide animation (override for custom animations)
|
||||
*/
|
||||
async animateHide() {
|
||||
if (!this.element) return;
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
|
||||
this.element.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
this.element.style.display = 'none';
|
||||
this.element.style.transition = '';
|
||||
this.element.style.opacity = '';
|
||||
resolve();
|
||||
}, this.animationDuration);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class management
|
||||
*/
|
||||
addClass(className) {
|
||||
this.cssClasses.add(className);
|
||||
if (this.element) {
|
||||
this.element.classList.add(className);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
removeClass(className) {
|
||||
this.cssClasses.delete(className);
|
||||
if (this.element) {
|
||||
this.element.classList.remove(className);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
hasClass(className) {
|
||||
return this.cssClasses.has(className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme styling
|
||||
*/
|
||||
applyTheme(themeName) {
|
||||
const oldTheme = this.theme;
|
||||
this.theme = themeName;
|
||||
|
||||
this.removeClass(`theme-${oldTheme}`);
|
||||
this.addClass(`theme-${themeName}`);
|
||||
|
||||
this.emit('theme-changed', { oldTheme, newTheme: themeName });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find child element by selector
|
||||
*/
|
||||
findElement(selector) {
|
||||
return this.element ? this.element.querySelector(selector) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all child elements by selector
|
||||
*/
|
||||
findElements(selector) {
|
||||
return this.element ? this.element.querySelectorAll(selector) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Override destroy to clean up DOM
|
||||
*/
|
||||
async destroy() {
|
||||
if (this.element && this.element.parentNode) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
|
||||
this.element = null;
|
||||
this.isRendered = false;
|
||||
this.isVisible = false;
|
||||
|
||||
await super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all CSS classes to element
|
||||
*/
|
||||
applyCSSClasses(element = this.element) {
|
||||
if (element) {
|
||||
element.className = Array.from(this.cssClasses).join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration for UI widgets
|
||||
*/
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
...super.getDefaultConfig(),
|
||||
theme: 'default',
|
||||
animationDuration: 300,
|
||||
enableAnimations: true
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* Base Widget Class
|
||||
*
|
||||
* Foundation class for all Markitect UI widgets following the plugin architecture.
|
||||
* Provides core functionality for event handling, state management, and lifecycle.
|
||||
*/
|
||||
export class Widget extends EventTarget {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
// Core properties
|
||||
this.id = options.id || `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.container = options.container || document.body;
|
||||
this.config = { ...this.getDefaultConfig(), ...options };
|
||||
|
||||
// State management
|
||||
this.state = new Map();
|
||||
this.isInitialized = false;
|
||||
this.isDestroyed = false;
|
||||
|
||||
// Mixin support
|
||||
this.mixins = [];
|
||||
|
||||
// Lifecycle hooks
|
||||
this.onInitialize = options.onInitialize || (() => {});
|
||||
this.onDestroy = options.onDestroy || (() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the widget
|
||||
*/
|
||||
async initialize() {
|
||||
if (this.isInitialized || this.isDestroyed) {
|
||||
return this;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.onInitialize(this);
|
||||
this.isInitialized = true;
|
||||
this.emit('initialized');
|
||||
return this;
|
||||
} catch (error) {
|
||||
this.emit('error', { phase: 'initialize', error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the widget and clean up resources
|
||||
*/
|
||||
async destroy() {
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.onDestroy(this);
|
||||
this.isDestroyed = true;
|
||||
this.emit('destroyed');
|
||||
} catch (error) {
|
||||
this.emit('error', { phase: 'destroy', error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State management
|
||||
*/
|
||||
setState(key, value) {
|
||||
const oldValue = this.state.get(key);
|
||||
this.state.set(key, value);
|
||||
this.emit('state-changed', { key, value, oldValue });
|
||||
}
|
||||
|
||||
getState(key, defaultValue = null) {
|
||||
return this.state.get(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emission wrapper
|
||||
*/
|
||||
emit(eventType, data = {}) {
|
||||
const event = new CustomEvent(eventType, {
|
||||
detail: { widget: this, ...data }
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mixin functionality
|
||||
*/
|
||||
applyMixin(mixin) {
|
||||
if (typeof mixin === 'object') {
|
||||
Object.assign(this, mixin);
|
||||
this.mixins.push(mixin);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration (override in subclasses)
|
||||
*/
|
||||
getDefaultConfig() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for creating DOM elements with styling
|
||||
*/
|
||||
createElement(tag, options = {}) {
|
||||
const element = document.createElement(tag);
|
||||
|
||||
if (options.className) {
|
||||
element.className = options.className;
|
||||
}
|
||||
|
||||
if (options.textContent) {
|
||||
element.textContent = options.textContent;
|
||||
}
|
||||
|
||||
if (options.innerHTML) {
|
||||
element.innerHTML = options.innerHTML;
|
||||
}
|
||||
|
||||
if (options.style) {
|
||||
if (typeof options.style === 'string') {
|
||||
element.style.cssText = options.style;
|
||||
} else {
|
||||
Object.assign(element.style, options.style);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.attributes) {
|
||||
Object.entries(options.attributes).forEach(([key, value]) => {
|
||||
element.setAttribute(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@@ -1,625 +0,0 @@
|
||||
/**
|
||||
* DocumentNavigator Widget
|
||||
*
|
||||
* Substack-style floating document navigation widget that displays a hierarchical
|
||||
* table of contents based on document headings. Supports smooth scrolling,
|
||||
* scroll spy, expand/collapse, and responsive behavior.
|
||||
*/
|
||||
import { UIWidget } from '../base/UIWidget.js';
|
||||
|
||||
export class DocumentNavigator extends UIWidget {
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
|
||||
// Navigation state
|
||||
this.isCollapsed = this.config.collapsed;
|
||||
this.currentSection = null;
|
||||
this.headings = [];
|
||||
this.navigationTree = [];
|
||||
|
||||
// Scroll spy state
|
||||
this.scrollSpyEnabled = this.config.enableScrollSpy;
|
||||
this.scrollThrottle = null;
|
||||
|
||||
// Event bindings
|
||||
this.boundScrollHandler = this.handleScroll.bind(this);
|
||||
this.boundResizeHandler = this.handleResize.bind(this);
|
||||
|
||||
// Initialize responsive behavior
|
||||
this.mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
}
|
||||
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
...super.getDefaultConfig(),
|
||||
position: 'left', // 'left' or 'right'
|
||||
collapsed: true, // Start collapsed
|
||||
autoHide: true, // Hide on mobile
|
||||
maxHeadingLevel: 3, // H1, H2, H3
|
||||
enableScrollSpy: true, // Highlight current section
|
||||
smoothScroll: true, // Smooth scroll behavior
|
||||
animationDuration: 300, // Animation timing
|
||||
minHeadings: 2, // Min headings to show navigator
|
||||
theme: 'default', // Theme support
|
||||
|
||||
// Styling options
|
||||
width: '280px',
|
||||
collapsedWidth: '40px',
|
||||
offset: { top: '80px', side: '20px' },
|
||||
|
||||
// Accessibility
|
||||
enableKeyboard: true,
|
||||
ariaLabel: 'Document Navigation'
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await super.initialize();
|
||||
|
||||
// Extract headings from container
|
||||
this.extractHeadings();
|
||||
this.buildNavigationTree();
|
||||
|
||||
// Set up event listeners
|
||||
if (this.scrollSpyEnabled) {
|
||||
window.addEventListener('scroll', this.boundScrollHandler, { passive: true });
|
||||
}
|
||||
|
||||
if (this.config.autoHide) {
|
||||
window.addEventListener('resize', this.boundResizeHandler);
|
||||
this.handleResize(); // Initial check
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async render() {
|
||||
if (this.isRendered) {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
// Check if we have enough headings
|
||||
if (this.headings.length < this.config.minHeadings) {
|
||||
this.isRendered = true;
|
||||
return null; // Don't render if too few headings
|
||||
}
|
||||
|
||||
// Create main container
|
||||
this.element = this.createElement('nav', {
|
||||
className: 'document-navigator markitect-widget',
|
||||
attributes: {
|
||||
'aria-label': this.config.ariaLabel,
|
||||
'role': 'navigation'
|
||||
},
|
||||
style: this.getNavigatorStyle()
|
||||
});
|
||||
|
||||
// Apply CSS classes
|
||||
this.applyCSSClasses();
|
||||
this.addClass('theme-' + this.theme);
|
||||
this.addClass('position-' + this.config.position);
|
||||
|
||||
// Create toggle button (always visible)
|
||||
this.createToggleButton();
|
||||
|
||||
// Create navigation list (hidden when collapsed)
|
||||
this.createNavigationList();
|
||||
|
||||
// Set initial visibility state
|
||||
if (this.isCollapsed) {
|
||||
await this.collapse({ immediate: true });
|
||||
} else {
|
||||
await this.expand({ immediate: true });
|
||||
}
|
||||
|
||||
// Append to container
|
||||
this.container.appendChild(this.element);
|
||||
|
||||
// Initialize scroll spy
|
||||
if (this.scrollSpyEnabled) {
|
||||
this.updateCurrentSection();
|
||||
}
|
||||
|
||||
this.isRendered = true;
|
||||
this.emit('rendered');
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
createToggleButton() {
|
||||
this.toggleButton = this.createElement('button', {
|
||||
className: 'navigator-toggle',
|
||||
attributes: {
|
||||
'type': 'button',
|
||||
'aria-label': this.isCollapsed ? 'Expand navigation' : 'Collapse navigation',
|
||||
'aria-expanded': !this.isCollapsed
|
||||
},
|
||||
innerHTML: this.getToggleIcon(),
|
||||
style: this.getToggleStyle()
|
||||
});
|
||||
|
||||
// Toggle on click
|
||||
this.toggleButton.addEventListener('click', async () => {
|
||||
await this.toggle();
|
||||
});
|
||||
|
||||
// Keyboard support
|
||||
if (this.config.enableKeyboard) {
|
||||
this.toggleButton.addEventListener('keydown', this.handleKeyboard.bind(this));
|
||||
}
|
||||
|
||||
this.element.appendChild(this.toggleButton);
|
||||
}
|
||||
|
||||
createNavigationList() {
|
||||
this.navigationList = this.createElement('div', {
|
||||
className: 'navigator-list',
|
||||
style: this.getListStyle()
|
||||
});
|
||||
|
||||
if (this.headings.length === 0) {
|
||||
this.createEmptyState();
|
||||
} else {
|
||||
this.populateNavigationList();
|
||||
}
|
||||
|
||||
this.element.appendChild(this.navigationList);
|
||||
}
|
||||
|
||||
createEmptyState() {
|
||||
const emptyMessage = this.createElement('div', {
|
||||
className: 'navigator-empty',
|
||||
textContent: 'No headings found',
|
||||
style: {
|
||||
padding: '1rem',
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
fontStyle: 'italic'
|
||||
}
|
||||
});
|
||||
|
||||
this.navigationList.appendChild(emptyMessage);
|
||||
}
|
||||
|
||||
populateNavigationList() {
|
||||
// Create header
|
||||
const header = this.createElement('div', {
|
||||
className: 'navigator-header',
|
||||
innerHTML: `
|
||||
<h3>Contents</h3>
|
||||
<button class="navigator-close" aria-label="Close navigation">✕</button>
|
||||
`,
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '1rem 1rem 0.5rem',
|
||||
borderBottom: '1px solid #eee',
|
||||
marginBottom: '0.5rem'
|
||||
}
|
||||
});
|
||||
|
||||
// Close button functionality
|
||||
const closeButton = header.querySelector('.navigator-close');
|
||||
closeButton.addEventListener('click', async () => {
|
||||
await this.collapse();
|
||||
});
|
||||
|
||||
this.navigationList.appendChild(header);
|
||||
|
||||
// Create navigation items
|
||||
const navContainer = this.createElement('div', {
|
||||
className: 'navigator-items',
|
||||
style: {
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'auto',
|
||||
padding: '0 0.5rem 1rem'
|
||||
}
|
||||
});
|
||||
|
||||
this.renderNavigationTree(navContainer, this.navigationTree);
|
||||
this.navigationList.appendChild(navContainer);
|
||||
}
|
||||
|
||||
renderNavigationTree(container, items, level = 0) {
|
||||
items.forEach(item => {
|
||||
const navItem = this.createElement('div', {
|
||||
className: `navigator-item level-${level}`,
|
||||
style: {
|
||||
marginLeft: `${level * 1}rem`,
|
||||
marginBottom: '0.25rem'
|
||||
}
|
||||
});
|
||||
|
||||
// Create clickable link
|
||||
const link = this.createElement('a', {
|
||||
className: 'navigator-link',
|
||||
textContent: item.text,
|
||||
attributes: {
|
||||
'href': `#${item.id}`,
|
||||
'data-target': item.id,
|
||||
'data-level': item.level,
|
||||
'role': 'button',
|
||||
'tabindex': '0'
|
||||
},
|
||||
style: {
|
||||
display: 'block',
|
||||
padding: '0.5rem 0.75rem',
|
||||
textDecoration: 'none',
|
||||
color: '#333',
|
||||
borderRadius: '4px',
|
||||
fontSize: level === 0 ? '0.9rem' : '0.8rem',
|
||||
fontWeight: level === 0 ? '600' : '400',
|
||||
transition: 'all 0.2s ease',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
});
|
||||
|
||||
// Hover effects
|
||||
link.addEventListener('mouseenter', () => {
|
||||
link.style.backgroundColor = '#f0f0f0';
|
||||
});
|
||||
|
||||
link.addEventListener('mouseleave', () => {
|
||||
if (!link.classList.contains('active')) {
|
||||
link.style.backgroundColor = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Click navigation
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.navigateToHeading(item.id);
|
||||
});
|
||||
|
||||
navItem.appendChild(link);
|
||||
|
||||
// Render children recursively
|
||||
if (item.children && item.children.length > 0) {
|
||||
this.renderNavigationTree(navItem, item.children, level + 1);
|
||||
}
|
||||
|
||||
container.appendChild(navItem);
|
||||
});
|
||||
}
|
||||
|
||||
extractHeadings() {
|
||||
const headingSelectors = [];
|
||||
for (let i = 1; i <= this.config.maxHeadingLevel; i++) {
|
||||
headingSelectors.push(`h${i}`);
|
||||
}
|
||||
|
||||
const headingElements = this.container.querySelectorAll(headingSelectors.join(', '));
|
||||
|
||||
this.headings = Array.from(headingElements).map((heading, index) => {
|
||||
// Ensure heading has an ID
|
||||
if (!heading.id) {
|
||||
heading.id = `heading-${index + 1}`;
|
||||
}
|
||||
|
||||
return {
|
||||
element: heading,
|
||||
id: heading.id,
|
||||
text: heading.textContent.trim(),
|
||||
level: parseInt(heading.tagName.substring(1)),
|
||||
offset: heading.offsetTop
|
||||
};
|
||||
});
|
||||
|
||||
return this.headings;
|
||||
}
|
||||
|
||||
buildNavigationTree() {
|
||||
this.navigationTree = [];
|
||||
const stack = [];
|
||||
|
||||
this.headings.forEach(heading => {
|
||||
const item = {
|
||||
...heading,
|
||||
children: []
|
||||
};
|
||||
|
||||
// Find correct parent based on heading level
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
// Top level item
|
||||
this.navigationTree.push(item);
|
||||
} else {
|
||||
// Child item
|
||||
stack[stack.length - 1].children.push(item);
|
||||
}
|
||||
|
||||
stack.push(item);
|
||||
});
|
||||
|
||||
return this.navigationTree;
|
||||
}
|
||||
|
||||
async toggle(options = {}) {
|
||||
return this.isCollapsed ? this.expand(options) : this.collapse(options);
|
||||
}
|
||||
|
||||
async expand(options = {}) {
|
||||
if (!this.isCollapsed) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.isCollapsed = false;
|
||||
|
||||
if (this.toggleButton) {
|
||||
this.toggleButton.setAttribute('aria-expanded', 'true');
|
||||
this.toggleButton.setAttribute('aria-label', 'Collapse navigation');
|
||||
this.toggleButton.innerHTML = this.getToggleIcon();
|
||||
}
|
||||
|
||||
if (this.navigationList) {
|
||||
if (this.enableAnimations && !options.immediate) {
|
||||
await this.animateExpand();
|
||||
} else {
|
||||
this.navigationList.style.display = '';
|
||||
this.element.style.width = this.config.width;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('toggle', { expanded: true });
|
||||
return this;
|
||||
}
|
||||
|
||||
async collapse(options = {}) {
|
||||
if (this.isCollapsed) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.isCollapsed = true;
|
||||
|
||||
if (this.toggleButton) {
|
||||
this.toggleButton.setAttribute('aria-expanded', 'false');
|
||||
this.toggleButton.setAttribute('aria-label', 'Expand navigation');
|
||||
this.toggleButton.innerHTML = this.getToggleIcon();
|
||||
}
|
||||
|
||||
if (this.navigationList) {
|
||||
if (this.enableAnimations && !options.immediate) {
|
||||
await this.animateCollapse();
|
||||
} else {
|
||||
this.navigationList.style.display = 'none';
|
||||
this.element.style.width = this.config.collapsedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('toggle', { expanded: false });
|
||||
return this;
|
||||
}
|
||||
|
||||
async animateExpand() {
|
||||
return new Promise(resolve => {
|
||||
this.navigationList.style.opacity = '0';
|
||||
this.navigationList.style.display = '';
|
||||
|
||||
// Animate width and opacity
|
||||
this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`;
|
||||
this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
|
||||
|
||||
// Force reflow
|
||||
this.element.offsetWidth;
|
||||
|
||||
this.element.style.width = this.config.width;
|
||||
this.navigationList.style.opacity = '1';
|
||||
|
||||
setTimeout(() => {
|
||||
this.element.style.transition = '';
|
||||
this.navigationList.style.transition = '';
|
||||
resolve();
|
||||
}, this.animationDuration);
|
||||
});
|
||||
}
|
||||
|
||||
async animateCollapse() {
|
||||
return new Promise(resolve => {
|
||||
this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`;
|
||||
this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
|
||||
|
||||
this.navigationList.style.opacity = '0';
|
||||
this.element.style.width = this.config.collapsedWidth;
|
||||
|
||||
setTimeout(() => {
|
||||
this.navigationList.style.display = 'none';
|
||||
this.element.style.transition = '';
|
||||
this.navigationList.style.transition = '';
|
||||
resolve();
|
||||
}, this.animationDuration);
|
||||
});
|
||||
}
|
||||
|
||||
navigateToHeading(headingId) {
|
||||
const targetElement = document.getElementById(headingId);
|
||||
if (!targetElement) {
|
||||
console.warn(`Heading with ID '${headingId}' not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update active navigation item
|
||||
this.setActiveItem(headingId);
|
||||
|
||||
// Scroll to target
|
||||
if (this.config.smoothScroll) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
inline: 'nearest'
|
||||
});
|
||||
} else {
|
||||
targetElement.scrollIntoView();
|
||||
}
|
||||
|
||||
// Emit navigation event
|
||||
this.emit('navigate', { target: headingId, element: targetElement });
|
||||
|
||||
// Optionally collapse after navigation on mobile
|
||||
if (this.mediaQuery.matches && this.config.autoHide) {
|
||||
setTimeout(() => this.collapse(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveItem(headingId) {
|
||||
// Remove previous active state
|
||||
const previousActive = this.findElement('.navigator-link.active');
|
||||
if (previousActive) {
|
||||
previousActive.classList.remove('active');
|
||||
previousActive.style.backgroundColor = '';
|
||||
}
|
||||
|
||||
// Set new active state
|
||||
const newActive = this.findElement(`[data-target="${headingId}"]`);
|
||||
if (newActive) {
|
||||
newActive.classList.add('active');
|
||||
newActive.style.backgroundColor = '#e3f2fd';
|
||||
newActive.style.color = '#1976d2';
|
||||
}
|
||||
|
||||
this.currentSection = headingId;
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
if (!this.scrollSpyEnabled || !this.isRendered) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Throttle scroll events
|
||||
if (this.scrollThrottle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollThrottle = setTimeout(() => {
|
||||
this.updateCurrentSection();
|
||||
this.scrollThrottle = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
updateCurrentSection() {
|
||||
const scrollPosition = window.pageYOffset + 100; // Offset for header
|
||||
let currentHeading = null;
|
||||
|
||||
// Find the current heading based on scroll position
|
||||
for (let i = this.headings.length - 1; i >= 0; i--) {
|
||||
const heading = this.headings[i];
|
||||
if (heading.element.offsetTop <= scrollPosition) {
|
||||
currentHeading = heading;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentHeading && currentHeading.id !== this.currentSection) {
|
||||
this.setActiveItem(currentHeading.id);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentSection() {
|
||||
return this.currentSection;
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
if (!this.config.autoHide) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mediaQuery.matches) {
|
||||
// Mobile: hide navigator
|
||||
if (this.element) {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Desktop: show navigator
|
||||
if (this.element) {
|
||||
this.element.style.display = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyboard(event) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
event.preventDefault();
|
||||
this.toggle();
|
||||
break;
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
this.collapse();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getNavigatorStyle() {
|
||||
const baseStyle = {
|
||||
position: 'fixed',
|
||||
top: this.config.offset.top,
|
||||
zIndex: '1000',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
border: '1px solid #e1e5e9',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
width: this.isCollapsed ? this.config.collapsedWidth : this.config.width,
|
||||
maxHeight: '80vh',
|
||||
overflow: 'hidden',
|
||||
transition: 'width 0.3s ease-in-out'
|
||||
};
|
||||
|
||||
// Position-specific styling
|
||||
if (this.config.position === 'left') {
|
||||
baseStyle.left = this.config.offset.side;
|
||||
} else {
|
||||
baseStyle.right = this.config.offset.side;
|
||||
}
|
||||
|
||||
return baseStyle;
|
||||
}
|
||||
|
||||
getToggleStyle() {
|
||||
return {
|
||||
width: '100%',
|
||||
height: this.config.collapsedWidth,
|
||||
border: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '16px',
|
||||
color: '#666',
|
||||
transition: 'color 0.2s ease'
|
||||
};
|
||||
}
|
||||
|
||||
getListStyle() {
|
||||
return {
|
||||
display: this.isCollapsed ? 'none' : '',
|
||||
opacity: this.isCollapsed ? '0' : '1'
|
||||
};
|
||||
}
|
||||
|
||||
getToggleIcon() {
|
||||
if (this.isCollapsed) {
|
||||
return this.config.position === 'left' ? '☰' : '☰';
|
||||
} else {
|
||||
return '✕';
|
||||
}
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
// Remove event listeners
|
||||
window.removeEventListener('scroll', this.boundScrollHandler);
|
||||
window.removeEventListener('resize', this.boundResizeHandler);
|
||||
|
||||
// Clear throttle
|
||||
if (this.scrollThrottle) {
|
||||
clearTimeout(this.scrollThrottle);
|
||||
}
|
||||
|
||||
await super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
<meta name="generator" content="Markitect {version}">
|
||||
<title>{title}</title>
|
||||
|
||||
{css_content}
|
||||
|
||||
<!-- Base styling for document content -->
|
||||
<style>
|
||||
body {
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
"""
|
||||
Test suite for the new clean architecture implementation
|
||||
Tests the JSON configuration interface and separation of concerns
|
||||
"""
|
||||
import pytest
|
||||
import tempfile
|
||||
import json
|
||||
from pathlib import Path
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
|
||||
class TestCleanArchitecture:
|
||||
"""Test suite for clean JavaScript-Python separation"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup for each test"""
|
||||
self.manager = CleanDocumentManager()
|
||||
|
||||
def test_clean_edit_mode_json_configuration(self):
|
||||
"""Test that edit mode uses clean JSON configuration interface"""
|
||||
test_markdown = '''# Test Document
|
||||
|
||||
## Section with Problematic Content
|
||||
```python
|
||||
script = f"""
|
||||
function test() {
|
||||
console.log("Hello {name}");
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
This content has quotes that previously broke JavaScript generation.
|
||||
'''
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
|
||||
# Read generated HTML
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Test 1: Check for clean template usage
|
||||
assert 'markitect-config' in html_content
|
||||
assert 'type="application/json"' in html_content
|
||||
|
||||
# Test 2: Extract and validate JSON configuration
|
||||
config_json = self.extract_config_json(html_content)
|
||||
assert config_json is not None, "Configuration JSON not found"
|
||||
|
||||
config = json.loads(config_json)
|
||||
|
||||
# Test 3: Validate configuration structure
|
||||
required_fields = ['markdownContent', 'mode', 'theme', 'originalFilename']
|
||||
for field in required_fields:
|
||||
assert field in config, f"Required field '{field}' missing from configuration"
|
||||
|
||||
# Test 4: Check that problematic content is properly escaped
|
||||
assert 'script = f"""' in config['markdownContent'] # Should be in JSON
|
||||
assert '"""' not in html_content.split('markitect-config')[1].split('</script>')[0], "Unescaped quotes in HTML"
|
||||
|
||||
def test_clean_architecture_no_python_js_mixing(self):
|
||||
"""Test that no Python code generates JavaScript strings"""
|
||||
test_markdown = "# Simple Test\n\nBasic content."
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Test 1: No direct JavaScript variable assignments from Python
|
||||
problematic_patterns = [
|
||||
'const markdownContent = "', # Old way
|
||||
'const markdownContentWithDogtag = "', # Old way
|
||||
'var markdownContent = "',
|
||||
'let markdownContent = "'
|
||||
]
|
||||
|
||||
for pattern in problematic_patterns:
|
||||
assert pattern not in html_content, f"Found problematic pattern: {pattern}"
|
||||
|
||||
# Test 2: Configuration should be in JSON script tag only
|
||||
config_sections = html_content.count('markitect-config')
|
||||
assert config_sections >= 2, f"Expected at least 2 config references (opening and closing), found {config_sections}"
|
||||
|
||||
# Test 3: JavaScript files should be embedded inline (no external src attributes)
|
||||
js_components = [
|
||||
'config-loader',
|
||||
'section-manager',
|
||||
'dom-renderer'
|
||||
]
|
||||
|
||||
for component in js_components:
|
||||
# Check that the component JavaScript is embedded, not referenced externally
|
||||
assert f'src="js/' not in html_content, "Found external JavaScript references - should be embedded"
|
||||
|
||||
# Check that components are embedded inline
|
||||
assert '{js_config_loader}' not in html_content, "Template placeholder not replaced"
|
||||
assert 'class MarkitectConfig' in html_content, "Config loader not embedded"
|
||||
assert 'class SectionManager' in html_content, "Section manager not embedded"
|
||||
|
||||
def test_configuration_interface_completeness(self):
|
||||
"""Test that all required data is passed through the configuration interface"""
|
||||
test_markdown = "# Config Test\n\nTesting configuration completeness."
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True,
|
||||
editor_theme='dark',
|
||||
keyboard_shortcuts=False
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
config_json = self.extract_config_json(html_content)
|
||||
config = json.loads(config_json)
|
||||
|
||||
# Test configuration completeness
|
||||
expected_config = {
|
||||
'markdownContent': test_markdown,
|
||||
'mode': 'edit',
|
||||
'theme': 'dark',
|
||||
'keyboardShortcuts': False,
|
||||
'autosave': False,
|
||||
'sections': True,
|
||||
'base64References': {}
|
||||
}
|
||||
|
||||
for key, expected_value in expected_config.items():
|
||||
assert key in config, f"Configuration missing key: {key}"
|
||||
if key == 'markdownContent':
|
||||
assert config[key] == expected_value, f"Configuration {key} value mismatch"
|
||||
|
||||
def test_insert_mode_configuration(self):
|
||||
"""Test insert mode specific configuration"""
|
||||
test_markdown = "# Insert Mode Test"
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
insert_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Check body class
|
||||
assert 'class="markitect-insert-mode"' in html_content
|
||||
|
||||
# Check configuration
|
||||
config_json = self.extract_config_json(html_content)
|
||||
config = json.loads(config_json)
|
||||
|
||||
assert config['mode'] == 'insert'
|
||||
assert 'restrictedHeadingLevels' in config
|
||||
assert config['restrictedHeadingLevels'] == [1, 2, 3]
|
||||
|
||||
def test_static_vs_edit_mode_separation(self):
|
||||
"""Test that static mode and edit mode use different templates"""
|
||||
test_markdown = "# Mode Test\n\nTesting template separation."
|
||||
|
||||
# Test static mode
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as static_file:
|
||||
static_result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=static_file.name,
|
||||
edit_mode=False
|
||||
)
|
||||
|
||||
static_content = Path(static_file.name).read_text()
|
||||
|
||||
# Static mode should NOT have configuration interface
|
||||
assert 'markitect-config' not in static_content
|
||||
assert 'application/json' not in static_content
|
||||
|
||||
# Test edit mode
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as edit_file:
|
||||
edit_result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=edit_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
edit_content = Path(edit_file.name).read_text()
|
||||
|
||||
# Edit mode should HAVE configuration interface
|
||||
assert 'markitect-config' in edit_content
|
||||
assert 'application/json' in edit_content
|
||||
|
||||
# Helper methods
|
||||
|
||||
def extract_config_json(self, html_content):
|
||||
"""Extract JSON configuration from HTML"""
|
||||
try:
|
||||
# Find the config script tag
|
||||
start_marker = 'id="markitect-config" type="application/json">'
|
||||
end_marker = '</script>'
|
||||
|
||||
start_pos = html_content.find(start_marker)
|
||||
if start_pos == -1:
|
||||
return None
|
||||
|
||||
start_pos += len(start_marker)
|
||||
end_pos = html_content.find(end_marker, start_pos)
|
||||
|
||||
if end_pos == -1:
|
||||
return None
|
||||
|
||||
config_json = html_content[start_pos:end_pos].strip()
|
||||
return config_json
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to extract config JSON: {e}")
|
||||
return None
|
||||
@@ -1,246 +0,0 @@
|
||||
"""
|
||||
Tests for Issue #132: Basic HTML Generation and Rendering
|
||||
|
||||
This module tests the core functionality of the md-render command for
|
||||
client-side markdown rendering with JavaScript.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
import json
|
||||
import re
|
||||
|
||||
# Add project root to path for imports
|
||||
import sys
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from markitect.plugins.builtin.markdown_commands import MarkdownCommandsPlugin
|
||||
|
||||
|
||||
class TestIssue132BasicRendering:
|
||||
"""Test basic HTML generation and markdown rendering functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment."""
|
||||
self.plugin = MarkdownCommandsPlugin()
|
||||
self.plugin.initialize()
|
||||
|
||||
# Create temporary directory for test outputs
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test environment."""
|
||||
# Clean up temporary files
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_md_render_command_exists(self):
|
||||
"""Test that md-render command is registered in plugin - Issue #132."""
|
||||
commands = self.plugin.get_commands()
|
||||
|
||||
# Should include md-render command
|
||||
assert 'md-render' in commands
|
||||
|
||||
# Command should be callable
|
||||
md_render_cmd = commands['md-render']
|
||||
assert callable(md_render_cmd)
|
||||
|
||||
def test_generate_basic_html_from_simple_markdown(self):
|
||||
"""Test generating HTML from simple markdown content - Issue #132."""
|
||||
# Create test markdown content
|
||||
markdown_content = """# Test Document
|
||||
|
||||
This is a **test** document with some *italic* text and a [link](https://example.com).
|
||||
|
||||
## Section 2
|
||||
|
||||
- List item 1
|
||||
- List item 2
|
||||
- List item 3
|
||||
"""
|
||||
|
||||
# Create temporary input file
|
||||
input_file = Path(self.temp_dir) / "test.md"
|
||||
input_file.write_text(markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "output.html"
|
||||
|
||||
# Test actual command execution
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
||||
|
||||
# Should execute successfully
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
# Should generate HTML file with content
|
||||
html_content = output_file.read_text()
|
||||
assert '<!DOCTYPE html>' in html_content
|
||||
assert '<title>Test Document</title>' in html_content
|
||||
|
||||
def test_html_contains_embedded_markdown_payload(self):
|
||||
"""Test that generated HTML contains markdown as JavaScript payload - Issue #132."""
|
||||
markdown_content = "# Simple Test\n\nThis is test content."
|
||||
|
||||
input_file = Path(self.temp_dir) / "simple.md"
|
||||
input_file.write_text(markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "simple.html"
|
||||
|
||||
# Test actual rendering
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should contain JavaScript with embedded markdown
|
||||
assert 'const markdownContent =' in html_content
|
||||
assert json.dumps(markdown_content) in html_content
|
||||
|
||||
# Should contain script tag for rendering
|
||||
assert '<script' in html_content
|
||||
assert 'marked' in html_content.lower()
|
||||
|
||||
def test_html_includes_javascript_markdown_parser(self):
|
||||
"""Test that generated HTML includes JavaScript markdown parser - Issue #132."""
|
||||
markdown_content = "# Parser Test\n\nTesting parser inclusion."
|
||||
|
||||
input_file = Path(self.temp_dir) / "parser_test.md"
|
||||
input_file.write_text(markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "parser_test.html"
|
||||
|
||||
# Test actual rendering
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include markdown parser (marked.js or similar)
|
||||
assert any(parser in html_content.lower() for parser in ['marked', 'markdown-it', 'showdown'])
|
||||
|
||||
# Should include rendering logic
|
||||
assert 'DOMContentLoaded' in html_content or 'window.onload' in html_content
|
||||
|
||||
def test_generated_html_is_valid_structure(self):
|
||||
"""Test that generated HTML has valid document structure - Issue #132."""
|
||||
markdown_content = "# Structure Test\n\nTesting HTML structure."
|
||||
|
||||
input_file = Path(self.temp_dir) / "structure.md"
|
||||
input_file.write_text(markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "structure.html"
|
||||
|
||||
# Test actual rendering
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Valid HTML5 document structure
|
||||
assert html_content.startswith('<!DOCTYPE html>')
|
||||
assert '<html' in html_content
|
||||
assert '<head>' in html_content
|
||||
assert '<body>' in html_content
|
||||
assert '</html>' in html_content
|
||||
|
||||
# Should have content div for rendering
|
||||
assert 'id="markdown-content"' in html_content
|
||||
|
||||
def test_handles_empty_markdown_file(self):
|
||||
"""Test behavior with empty markdown file - Issue #132."""
|
||||
# Create empty markdown file
|
||||
input_file = Path(self.temp_dir) / "empty.md"
|
||||
input_file.write_text("")
|
||||
|
||||
output_file = Path(self.temp_dir) / "empty.html"
|
||||
|
||||
# Test actual rendering
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
||||
|
||||
# Should handle empty file gracefully
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
# Should still generate valid HTML structure
|
||||
assert '<!DOCTYPE html>' in html_content
|
||||
assert 'const markdownContent = "";' in html_content
|
||||
|
||||
def test_handles_markdown_with_code_blocks(self):
|
||||
"""Test handling markdown with code blocks - Issue #132."""
|
||||
markdown_content = """# Code Test
|
||||
|
||||
Here's some Python code:
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello, World!")
|
||||
return True
|
||||
```
|
||||
|
||||
And some inline `code` too.
|
||||
"""
|
||||
|
||||
input_file = Path(self.temp_dir) / "code_test.md"
|
||||
input_file.write_text(markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "code_test.html"
|
||||
|
||||
# Test actual rendering with code blocks
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should properly escape code content in JavaScript
|
||||
assert 'def hello_world' in html_content
|
||||
# Should handle backticks and quotes properly
|
||||
assert json.dumps(markdown_content) in html_content
|
||||
|
||||
def test_cli_command_interface_exists(self):
|
||||
"""Test that md-render CLI command interface exists - Issue #132."""
|
||||
from markitect.cli import cli
|
||||
|
||||
# Should have md-render command registered
|
||||
assert 'md-render' in cli.commands
|
||||
|
||||
cmd = cli.commands['md-render']
|
||||
assert cmd.name == 'md-render'
|
||||
assert cmd.help is not None
|
||||
assert 'markdown' in cmd.help.lower()
|
||||
@@ -1,402 +0,0 @@
|
||||
"""
|
||||
Tests for Issue #132: Template System and CSS Injection
|
||||
|
||||
This module tests template selection and custom CSS injection functionality
|
||||
for client-side markdown rendering.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
import json
|
||||
|
||||
# Add project root to path for imports
|
||||
import sys
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
class TestIssue132TemplateSystem:
|
||||
"""Test template selection and CSS injection functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment."""
|
||||
# Create temporary directory for test outputs
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.markdown_content = """# Template Test
|
||||
|
||||
This is a test document for template system validation.
|
||||
|
||||
## Features
|
||||
- Multiple templates
|
||||
- Custom CSS support
|
||||
- Responsive design
|
||||
"""
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test environment."""
|
||||
# Clean up temporary files
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_default_template_generates_basic_html(self):
|
||||
"""Test that default template generates basic HTML structure - Issue #132."""
|
||||
input_file = Path(self.temp_dir) / "default.md"
|
||||
input_file.write_text(self.markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "default.html"
|
||||
|
||||
# Template system IS implemented - test actual functionality
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should contain basic HTML5 structure
|
||||
assert '<!DOCTYPE html>' in html_content
|
||||
assert '<meta charset="utf-8">' in html_content
|
||||
assert '<title>' in html_content
|
||||
|
||||
def test_github_template_option(self):
|
||||
"""Test GitHub-style template selection - Issue #132."""
|
||||
input_file = Path(self.temp_dir) / "github.md"
|
||||
input_file.write_text(self.markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "github.html"
|
||||
|
||||
# Template system IS implemented - test GitHub template
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--theme', 'github'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
assert 'border-bottom: 1px solid #d0d7de' in html_content # GitHub heading style
|
||||
|
||||
def test_template_loading_from_filesystem(self):
|
||||
"""Test template system uses embedded templates - Issue #132."""
|
||||
# Templates are embedded in code, not loaded from filesystem
|
||||
# Test that template system provides all expected templates
|
||||
from markitect.plugins.builtin.markdown_commands import TEMPLATE_STYLES
|
||||
|
||||
# Should have all expected templates available
|
||||
expected_templates = ['basic', 'github', 'academic', 'dark']
|
||||
|
||||
for template_name in expected_templates:
|
||||
assert template_name in TEMPLATE_STYLES
|
||||
template_config = TEMPLATE_STYLES[template_name]
|
||||
|
||||
# Each template should have required style properties
|
||||
assert 'body_color' in template_config
|
||||
assert 'font_family' in template_config
|
||||
assert 'max_width' in template_config
|
||||
|
||||
# Test that templates are properly formatted with variable placeholders
|
||||
from markitect.plugins.builtin.markdown_commands import generate_html_with_embedded_markdown
|
||||
test_html = generate_html_with_embedded_markdown("# Test", "Test Title", "basic", "", {})
|
||||
# HTML template should be properly formatted
|
||||
assert '<!DOCTYPE html>' in test_html
|
||||
assert 'Test Title' in test_html
|
||||
assert '# Test' in test_html
|
||||
|
||||
def test_template_variable_substitution(self):
|
||||
"""Test template variable substitution system - Issue #132."""
|
||||
input_file = Path(self.temp_dir) / "variables.md"
|
||||
input_file.write_text("# Variable Test\n\nTesting substitution.")
|
||||
|
||||
output_file = Path(self.temp_dir) / "variables.html"
|
||||
|
||||
# Template engine IS implemented - test actual functionality
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Variables should be substituted with actual values
|
||||
assert '{{ markdown_json }}' not in html_content # Should be replaced
|
||||
assert '{{ title }}' not in html_content # Should be replaced
|
||||
assert '{{ css_content }}' not in html_content # Should be replaced
|
||||
|
||||
# Should contain actual markdown content as JSON
|
||||
assert '# Variable Test' in html_content
|
||||
|
||||
def test_custom_css_injection(self):
|
||||
"""Test custom CSS injection into templates - Issue #132."""
|
||||
custom_css = """
|
||||
body {
|
||||
font-family: 'Comic Sans MS', cursive;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.markdown-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
"""
|
||||
|
||||
# Create CSS file
|
||||
css_file = Path(self.temp_dir) / "custom.css"
|
||||
css_file.write_text(custom_css)
|
||||
|
||||
input_file = Path(self.temp_dir) / "styled.md"
|
||||
input_file.write_text(self.markdown_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "styled.html"
|
||||
|
||||
# CSS injection IS implemented - test actual functionality
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--css', str(css_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
# Custom CSS should be injected
|
||||
assert 'Comic Sans MS' in html_content
|
||||
assert 'background-color: #f0f0f0' in html_content
|
||||
|
||||
def test_css_content_embedded_in_html(self):
|
||||
"""Test that CSS content is properly embedded in HTML - Issue #132."""
|
||||
custom_css = "body { color: red; }"
|
||||
css_file = Path(self.temp_dir) / "red.css"
|
||||
css_file.write_text(custom_css)
|
||||
|
||||
input_file = Path(self.temp_dir) / "red_test.md"
|
||||
input_file.write_text("# Red Test\n\nShould be red text.")
|
||||
|
||||
output_file = Path(self.temp_dir) / "red_test.html"
|
||||
|
||||
# CSS embedding IS implemented - test actual functionality
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--css', str(css_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# CSS should be embedded in <style> tags
|
||||
assert '<style>' in html_content
|
||||
assert 'body { color: red; }' in html_content
|
||||
assert '</style>' in html_content
|
||||
|
||||
def test_template_with_markdown_parser_integration(self):
|
||||
"""Test template integration with JavaScript markdown parser - Issue #132."""
|
||||
input_file = Path(self.temp_dir) / "integration.md"
|
||||
input_file.write_text("# Integration Test\n\nTesting parser integration.")
|
||||
|
||||
output_file = Path(self.temp_dir) / "integration.html"
|
||||
|
||||
# Integration IS implemented - test actual functionality
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should contain markdown parser script
|
||||
assert 'marked.min.js' in html_content
|
||||
assert 'marked.parse' in html_content
|
||||
assert 'Integration Test' in html_content
|
||||
|
||||
# Should contain rendering JavaScript
|
||||
assert 'DOMContentLoaded' in html_content
|
||||
assert 'getElementById' in html_content
|
||||
assert 'innerHTML' in html_content
|
||||
|
||||
def test_multiple_templates_available(self):
|
||||
"""Test that multiple template options are available - Issue #132."""
|
||||
# Test template availability
|
||||
theme_options = ['basic', 'github', 'academic', 'dark']
|
||||
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
# Create test markdown file
|
||||
input_file = Path(self.temp_dir) / "template_test.md"
|
||||
input_file.write_text("# Template Test\n\nTesting multiple templates.")
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
for theme in theme_options:
|
||||
output_file = Path(self.temp_dir) / f"{theme}_output.html"
|
||||
|
||||
result = runner.invoke(md_render_command, [
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--theme', theme
|
||||
])
|
||||
|
||||
# Should be able to specify different templates
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
# Verify template-specific styling
|
||||
html_content = output_file.read_text()
|
||||
assert '<title>Template Test</title>' in html_content
|
||||
|
||||
def test_dark_theme_template_specific_styling(self):
|
||||
"""Test that dark theme has appropriate dark styling - Issue #132."""
|
||||
input_file = Path(self.temp_dir) / "dark_test.md"
|
||||
input_file.write_text("# Dark Theme Test\n\n> Blockquote test\n\n```code block```")
|
||||
|
||||
output_file = Path(self.temp_dir) / "dark_test.html"
|
||||
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(md_render_command, [
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--theme', 'dark'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Verify dark theme specific colors
|
||||
assert 'background-color: #0d1117' in html_content # Dark background
|
||||
assert 'color: #e6edf3' in html_content # Light text (updated in modular theme)
|
||||
assert 'color: #58a6ff' in html_content # Blue headings
|
||||
assert 'background-color: #161b22' in html_content # Dark code blocks
|
||||
assert 'border-left: 4px solid #30363d' in html_content # Gray blockquote border (updated)
|
||||
|
||||
def test_invalid_template_handling(self):
|
||||
"""Test error handling for invalid template names - Issue #132."""
|
||||
input_file = Path(self.temp_dir) / "invalid.md"
|
||||
input_file.write_text("# Invalid Template Test")
|
||||
|
||||
# Error handling IS implemented - test invalid template
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--theme', 'nonexistent_template'
|
||||
])
|
||||
|
||||
# Should exit with error code for invalid template choice
|
||||
assert result.exit_code != 0
|
||||
assert ('invalid choice' in result.output.lower() or
|
||||
'not one of' in result.output.lower() or
|
||||
'unknown theme' in result.output.lower())
|
||||
|
||||
def test_template_title_extraction_from_markdown(self):
|
||||
"""Test title extraction from markdown for template variables - Issue #132."""
|
||||
markdown_with_title = """# Main Title
|
||||
|
||||
This document should use "Main Title" as the HTML title.
|
||||
"""
|
||||
|
||||
input_file = Path(self.temp_dir) / "title_test.md"
|
||||
input_file.write_text(markdown_with_title)
|
||||
|
||||
output_file = Path(self.temp_dir) / "title_test.html"
|
||||
|
||||
# Title extraction IS implemented - test actual functionality
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# HTML title should be extracted from first heading
|
||||
assert '<title>Main Title</title>' in html_content
|
||||
|
||||
def test_responsive_template_css(self):
|
||||
"""Test that default templates include responsive CSS - Issue #132."""
|
||||
input_file = Path(self.temp_dir) / "responsive.md"
|
||||
input_file.write_text("# Responsive Test\n\nTesting responsive design.")
|
||||
|
||||
output_file = Path(self.temp_dir) / "responsive.html"
|
||||
|
||||
# Responsive CSS IS implemented - test actual functionality
|
||||
from markitect.cli import cli
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include viewport meta tag
|
||||
assert '<meta name="viewport"' in html_content
|
||||
|
||||
# Should include responsive CSS patterns
|
||||
assert 'max-width' in html_content
|
||||
@@ -1,435 +0,0 @@
|
||||
"""
|
||||
Tests for Issue #133: CLI Integration with Instant Markdown Editing Support
|
||||
|
||||
This module tests the CLI command enhancement that adds editing capabilities
|
||||
to the existing md-render command through the --edit flag.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
from click.testing import CliRunner
|
||||
|
||||
# Add project root to path for imports
|
||||
import sys
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
class TestIssue133CLIIntegration:
|
||||
"""Test CLI integration for instant markdown editing support."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment."""
|
||||
self.runner = CliRunner()
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
# Sample markdown content for testing
|
||||
self.test_markdown = """# Editing Test Document
|
||||
|
||||
This is a test document for instant markdown editing functionality.
|
||||
|
||||
## Features
|
||||
- Click-to-edit sections
|
||||
- Live preview comparison
|
||||
- Change tracking
|
||||
- File saving
|
||||
|
||||
### Code Example
|
||||
```bash
|
||||
markitect md-render input.md --edit
|
||||
```
|
||||
|
||||
Content paragraph that should be editable.
|
||||
"""
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test environment."""
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_edit_flag_adds_editing_capabilities(self):
|
||||
"""Test that --edit flag enables editing mode - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "edit_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "edit_output.html"
|
||||
|
||||
# Edit flag functionality IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include editor library and edit mode flag
|
||||
assert 'SectionManager' in html_content
|
||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
||||
assert 'DOMRenderer' in html_content
|
||||
|
||||
def test_edit_flag_with_all_templates(self):
|
||||
"""Test --edit flag works with all template types - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "template_edit_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
templates = ['basic', 'github', 'academic', 'dark']
|
||||
|
||||
# Template editing IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
for template in templates:
|
||||
output_file = Path(self.temp_dir) / f"edit_{template}.html"
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--theme', template,
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
# Should work with template styles
|
||||
assert 'SectionManager' in html_content
|
||||
assert 'DOMRenderer' in html_content
|
||||
|
||||
def test_editor_library_loading_configuration(self):
|
||||
"""Test editor library loading and configuration options - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "config_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "config_output.html"
|
||||
|
||||
# Editor configuration IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit',
|
||||
'--editor-theme', 'dark'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include editor configuration with theme: 'dark'
|
||||
assert 'theme: \'dark\'' in html_content
|
||||
assert 'MARKITECT_EDITOR_CONFIG' in html_content
|
||||
|
||||
def test_backward_compatibility_without_edit_flag(self):
|
||||
"""Test that existing functionality remains unchanged without --edit - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "compatibility_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "compatibility_output.html"
|
||||
|
||||
# Existing functionality should continue to work
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--theme', 'github',
|
||||
'--nodogtag'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should NOT include editor library without --edit flag
|
||||
assert 'markitect-editor' not in html_content
|
||||
assert 'const MARKITECT_EDIT_MODE = true' not in html_content
|
||||
|
||||
# Should include existing functionality
|
||||
assert 'marked.min.js' in html_content
|
||||
assert 'Editing Test Document' in html_content
|
||||
|
||||
def test_help_text_includes_edit_options(self):
|
||||
"""Test that help text includes new editing options - Issue #133."""
|
||||
# Help text IS updated with edit options
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, ['md-render', '--help'])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '--edit' in result.output
|
||||
assert 'editing' in result.output.lower()
|
||||
assert 'instant' in result.output.lower() or 'edit' in result.output.lower()
|
||||
|
||||
def test_edit_flag_with_custom_css(self):
|
||||
"""Test --edit flag works with custom CSS injection - Issue #133."""
|
||||
# Create custom CSS file
|
||||
css_content = """
|
||||
.editor-section {
|
||||
border: 2px dashed #007acc;
|
||||
}
|
||||
.edit-mode textarea {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
"""
|
||||
css_file = Path(self.temp_dir) / "editor.css"
|
||||
css_file.write_text(css_content)
|
||||
|
||||
input_file = Path(self.temp_dir) / "css_edit_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "css_edit_output.html"
|
||||
|
||||
# CSS + editing integration IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--css', str(css_file),
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include both custom CSS and editor
|
||||
assert 'Courier New' in html_content
|
||||
assert 'SectionManager' in html_content
|
||||
assert 'DOMRenderer' in html_content
|
||||
|
||||
def test_large_document_editing_performance(self):
|
||||
"""Test editing flag with large markdown documents - Issue #133."""
|
||||
# Create large markdown document
|
||||
large_content = self.test_markdown * 50 # Repeat content 50 times
|
||||
|
||||
input_file = Path(self.temp_dir) / "large_edit_test.md"
|
||||
input_file.write_text(large_content)
|
||||
|
||||
output_file = Path(self.temp_dir) / "large_edit_output.html"
|
||||
|
||||
# Large document handling IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should handle large documents gracefully
|
||||
assert len(html_content) > 20000 # Should be substantial (adjusted from 50k)
|
||||
assert 'SectionManager' in html_content
|
||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
||||
|
||||
def test_front_matter_preservation_with_editing(self):
|
||||
"""Test YAML front matter preserved in editing mode - Issue #133."""
|
||||
markdown_with_frontmatter = """---
|
||||
title: "Editable Document"
|
||||
author: "Test Author"
|
||||
date: "2025-10-07"
|
||||
tags: [editing, test, markdown]
|
||||
---
|
||||
|
||||
# Editable Content
|
||||
|
||||
This content should be editable while preserving front matter.
|
||||
"""
|
||||
|
||||
input_file = Path(self.temp_dir) / "frontmatter_edit_test.md"
|
||||
input_file.write_text(markdown_with_frontmatter)
|
||||
|
||||
output_file = Path(self.temp_dir) / "frontmatter_edit_output.html"
|
||||
|
||||
# Front matter + editing IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should preserve front matter in JavaScript payload and include editing
|
||||
assert 'Test Author' in html_content or 'Editable Document' in html_content
|
||||
assert 'SectionManager' in html_content
|
||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
||||
|
||||
def test_error_handling_invalid_edit_options(self):
|
||||
"""Test error handling for invalid editing options - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "error_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
# Error handling IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
# Test invalid editor theme
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--edit',
|
||||
'--editor-theme', 'invalid_theme'
|
||||
])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'invalid' in result.output.lower() or 'not one of' in result.output.lower()
|
||||
|
||||
def test_editor_script_cdn_fallback(self):
|
||||
"""Test graceful handling when editor CDN fails - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "fallback_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "fallback_output.html"
|
||||
|
||||
# Editor functionality IS implemented with bundled JavaScript
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include bundled editor (not relying on CDN)
|
||||
assert 'SectionManager' in html_content
|
||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
||||
# The implementation uses bundled JavaScript, not CDN, so no fallback needed
|
||||
|
||||
def test_mobile_responsive_editing_meta_tags(self):
|
||||
"""Test that editing mode includes proper mobile meta tags - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "mobile_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "mobile_output.html"
|
||||
|
||||
# Mobile responsiveness IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include mobile-friendly meta tags
|
||||
assert 'viewport' in html_content
|
||||
assert 'width=device-width' in html_content
|
||||
assert 'SectionManager' in html_content
|
||||
|
||||
def test_keyboard_shortcuts_configuration(self):
|
||||
"""Test keyboard shortcuts can be configured for editing - Issue #133."""
|
||||
input_file = Path(self.temp_dir) / "shortcuts_test.md"
|
||||
input_file.write_text(self.test_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "shortcuts_output.html"
|
||||
|
||||
# Keyboard shortcuts ARE implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit',
|
||||
'--keyboard-shortcuts'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should include keyboard shortcut configuration
|
||||
assert 'MARKITECT_EDITOR_CONFIG' in html_content
|
||||
assert 'keyboardShortcuts' in html_content
|
||||
# TODO: Keyboard shortcut handlers not yet implemented in current architecture
|
||||
# assert 'keydown' in html_content # When keyboard shortcuts are implemented
|
||||
|
||||
def test_edit_mode_with_existing_command_patterns(self):
|
||||
"""Test that editing follows existing CLI command patterns - Issue #133."""
|
||||
# Command pattern consistency IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
# Should follow same patterns as other md-* commands
|
||||
md_commands = [name for name in cli.commands.keys() if name.startswith('md-')]
|
||||
|
||||
assert 'md-render' in md_commands
|
||||
|
||||
# md-render command should have consistent help format
|
||||
cmd = cli.commands['md-render']
|
||||
assert cmd.help is not None
|
||||
assert 'edit' in cmd.help.lower() or any('--edit' in str(param) for param in cmd.params)
|
||||
|
||||
def test_section_detection_configuration(self):
|
||||
"""Test section detection can be configured for different markdown structures - Issue #133."""
|
||||
complex_markdown = """# Main Title
|
||||
|
||||
## Section 1
|
||||
Content for section 1.
|
||||
|
||||
### Subsection 1.1
|
||||
- List item 1
|
||||
- List item 2
|
||||
|
||||
```python
|
||||
def example_function():
|
||||
return "editable code"
|
||||
```
|
||||
|
||||
## Section 2
|
||||
| Column 1 | Column 2 |
|
||||
|----------|----------|
|
||||
| Data 1 | Data 2 |
|
||||
|
||||
> This is a blockquote that should be editable.
|
||||
"""
|
||||
|
||||
input_file = Path(self.temp_dir) / "complex_test.md"
|
||||
input_file.write_text(complex_markdown)
|
||||
|
||||
output_file = Path(self.temp_dir) / "complex_output.html"
|
||||
|
||||
# Complex section detection IS implemented
|
||||
from markitect.cli import cli
|
||||
|
||||
result = self.runner.invoke(cli, [
|
||||
'md-render',
|
||||
str(input_file),
|
||||
'--output', str(output_file),
|
||||
'--edit'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
html_content = output_file.read_text()
|
||||
|
||||
# Should detect and mark various section types
|
||||
assert 'data-section' in html_content or 'markitect-section-editable' in html_content
|
||||
assert 'SectionManager' in html_content
|
||||
@@ -1,329 +0,0 @@
|
||||
"""
|
||||
Test suite for md-render --edit functionality to prevent regression.
|
||||
|
||||
This test suite specifically targets the critical JavaScript syntax errors
|
||||
that were causing edit mode to fail completely, ensuring they never happen again.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
class TestEditModeRegression:
|
||||
"""Tests to prevent regression of the md-render --edit functionality."""
|
||||
|
||||
def test_edit_mode_generates_valid_javascript(self):
|
||||
"""Test that edit mode generates syntactically valid JavaScript."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
# Test markdown content
|
||||
test_content = "# Test Header\n\nThis is a test paragraph.\n\n## Section 2\n\nAnother paragraph."
|
||||
|
||||
# Generate HTML with edit mode
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test Document",
|
||||
markdown_content=test_content,
|
||||
edit_mode=True,
|
||||
editor_theme='github',
|
||||
keyboard_shortcuts=True
|
||||
)
|
||||
|
||||
# Extract JavaScript from HTML
|
||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||
assert js_match, "No JavaScript found in edit mode HTML"
|
||||
|
||||
js_content = js_match.group(1)
|
||||
|
||||
# Write to temp file and validate syntax with Node.js
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
||||
f.write(js_content)
|
||||
temp_js_path = f.name
|
||||
|
||||
try:
|
||||
# Use Node.js to check JavaScript syntax
|
||||
result = subprocess.run(
|
||||
['node', '-c', temp_js_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"JavaScript syntax error: {result.stderr}"
|
||||
|
||||
finally:
|
||||
Path(temp_js_path).unlink()
|
||||
|
||||
def test_edit_mode_contains_required_functions(self):
|
||||
"""Test that edit mode HTML contains all required JavaScript functions."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Check for critical functions that must be present
|
||||
required_functions = [
|
||||
'SectionManager',
|
||||
'Section',
|
||||
'DOMRenderer',
|
||||
'DebugPanel',
|
||||
'DocumentControls'
|
||||
]
|
||||
|
||||
for func_name in required_functions:
|
||||
assert func_name in html_content, f"Required function '{func_name}' not found in edit mode HTML"
|
||||
|
||||
def test_edit_mode_no_broken_string_literals(self):
|
||||
"""Test that there are no broken string literals in the generated JavaScript."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Extract JavaScript
|
||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||
js_content = js_match.group(1)
|
||||
|
||||
# Check for broken string patterns that caused the original bug
|
||||
broken_patterns = [
|
||||
r"'\s*\n\s*'", # Broken string literal across lines
|
||||
r'"\s*\n\s*"', # Broken string literal across lines
|
||||
r'reconstructed \+= .*\'\n', # Unescaped newline in string
|
||||
]
|
||||
|
||||
for pattern in broken_patterns:
|
||||
matches = re.findall(pattern, js_content)
|
||||
assert not matches, f"Found broken string pattern: {pattern} - matches: {matches}"
|
||||
|
||||
def test_edit_mode_proper_brace_escaping(self):
|
||||
"""Test that braces are properly escaped in f-string templates."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Extract JavaScript
|
||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||
js_content = js_match.group(1)
|
||||
|
||||
# Check for inconsistent brace patterns
|
||||
inconsistent_patterns = [
|
||||
r'(?<!})} else if.*{{', # Single brace followed by double (incorrect)
|
||||
r'}} else if.*}(?!})', # Double brace followed by single closing (incorrect)
|
||||
]
|
||||
|
||||
for pattern in inconsistent_patterns:
|
||||
matches = re.findall(pattern, js_content)
|
||||
assert not matches, f"Found inconsistent brace pattern: {pattern}"
|
||||
|
||||
def test_edit_mode_template_literal_syntax(self):
|
||||
"""Test that template literals are properly escaped."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Extract JavaScript
|
||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||
js_content = js_match.group(1)
|
||||
|
||||
# Check for problematic template literal patterns
|
||||
# Should NOT find double-escaped template literals like ${{
|
||||
problematic_patterns = [
|
||||
r'\$\{\{.*?\}\}', # Double-escaped template literals
|
||||
]
|
||||
|
||||
for pattern in problematic_patterns:
|
||||
matches = re.findall(pattern, js_content)
|
||||
assert not matches, f"Found problematic template literal: {pattern}"
|
||||
|
||||
def test_edit_mode_contains_content_div(self):
|
||||
"""Test that edit mode HTML contains the markdown-content div."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test Content",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Should contain the content container
|
||||
assert 'id="markdown-content"' in html_content
|
||||
assert 'MARKITECT_EDIT_MODE = true' in html_content
|
||||
assert 'markitect-edit-mode' in html_content
|
||||
|
||||
def test_edit_mode_error_handling_elements(self):
|
||||
"""Test that edit mode includes proper error handling UI elements."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Should contain clean editor elements
|
||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
||||
assert 'class="markitect-edit-mode"' in html_content
|
||||
assert 'initializeCleanEditor' in html_content
|
||||
assert 'console.error' in html_content # Error handling
|
||||
|
||||
def test_edit_mode_vs_normal_mode_differences(self):
|
||||
"""Test that edit mode and normal mode generate different output appropriately."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
test_content = "# Test Header\n\nTest content."
|
||||
|
||||
# Generate both modes
|
||||
normal_html = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content=test_content,
|
||||
edit_mode=False
|
||||
)
|
||||
|
||||
edit_html = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content=test_content,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Edit mode should have additional elements
|
||||
assert len(edit_html) > len(normal_html)
|
||||
assert 'MARKITECT_EDIT_MODE = true' in edit_html
|
||||
assert 'MARKITECT_EDIT_MODE = true' not in normal_html
|
||||
assert 'markitect-edit-mode' in edit_html
|
||||
assert 'markitect-edit-mode' not in normal_html
|
||||
|
||||
def test_edit_mode_javascript_execution_flow(self):
|
||||
"""Test the logical flow of JavaScript execution in edit mode."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Extract JavaScript
|
||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||
js_content = js_match.group(1)
|
||||
|
||||
# Check for proper execution flow elements
|
||||
flow_elements = [
|
||||
'DOMContentLoaded', # Event listener setup
|
||||
'MARKITECT_EDIT_MODE', # Mode check
|
||||
'initializeCleanEditor', # Editor initialization
|
||||
'marked.parse', # Content rendering
|
||||
'SectionManager' # Section management class
|
||||
]
|
||||
|
||||
for element in flow_elements:
|
||||
assert element in js_content, f"Missing execution flow element: {element}"
|
||||
|
||||
def test_newline_escaping_in_javascript_strings(self):
|
||||
"""Test that newlines in JavaScript strings are properly escaped."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test\n\nMultiple\nLines",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Extract JavaScript
|
||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||
js_content = js_match.group(1)
|
||||
|
||||
# Look for the specific section that was broken
|
||||
# Should find properly escaped newlines like '\\n\\n' in the JavaScript
|
||||
assert '\\n\\n' in js_content, "Newlines not properly escaped in JavaScript strings"
|
||||
|
||||
# Should NOT find unescaped newlines in string contexts
|
||||
# This regex looks for string concatenation with actual newlines
|
||||
broken_newline_pattern = r"'\s*\+\s*text\s*\+\s*'\s*\n"
|
||||
matches = re.findall(broken_newline_pattern, js_content)
|
||||
assert not matches, f"Found unescaped newlines in string concatenation: {matches}"
|
||||
|
||||
|
||||
class TestEditModeIntegration:
|
||||
"""Integration tests for the complete edit mode functionality."""
|
||||
|
||||
def test_save_functionality_javascript_presence(self):
|
||||
"""Test that the save functionality JavaScript is properly included."""
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
# Create a CleanDocumentManager
|
||||
doc_manager = CleanDocumentManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Test",
|
||||
markdown_content="# Test Content",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
# Check for modular architecture components (current implementation)
|
||||
# TODO: Save functionality not yet implemented in modular architecture
|
||||
required_elements = [
|
||||
'SectionManager', # Core modular component
|
||||
'DOMRenderer', # Rendering component
|
||||
'DocumentControls', # Control component
|
||||
'MARKITECT_EDIT_MODE' # Edit mode flag
|
||||
]
|
||||
|
||||
for element in required_elements:
|
||||
assert element in html_content, f"Required modular component missing: {element}"
|
||||
|
||||
# Future save functionality elements (when implemented):
|
||||
# save_elements = [
|
||||
# '💾 Save Document',
|
||||
# 'generateSaveFilename',
|
||||
# 'getDocumentMarkdown',
|
||||
# 'Blob',
|
||||
# 'download'
|
||||
# ]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -20,7 +20,7 @@ from markitect.production.error_handler import (
|
||||
ResourceExhaustionError
|
||||
)
|
||||
try:
|
||||
from .test_utils import test_workspace
|
||||
from .test_utils import workspace_context
|
||||
except ImportError:
|
||||
# Fallback for missing test utilities
|
||||
import tempfile
|
||||
@@ -29,16 +29,13 @@ except ImportError:
|
||||
import shutil
|
||||
|
||||
@contextmanager
|
||||
def _test_workspace_fallback(name=None):
|
||||
def workspace_context(name=None):
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix=f"{name}_" if name else "test_"))
|
||||
try:
|
||||
yield temp_dir
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
# Assign to expected name
|
||||
test_workspace = _test_workspace_fallback
|
||||
|
||||
|
||||
class TestProductionErrorHandler:
|
||||
"""Test production error handling and recovery capabilities."""
|
||||
@@ -46,7 +43,7 @@ class TestProductionErrorHandler:
|
||||
@pytest.fixture
|
||||
def temp_workspace(self):
|
||||
"""Create temporary workspace for testing."""
|
||||
with test_workspace("error_handler") as temp_dir:
|
||||
with workspace_context("error_handler") as temp_dir:
|
||||
yield temp_dir
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
"""
|
||||
JavaScript Sanity Test Suite
|
||||
Tests for basic JavaScript functionality, syntax validation, and initialization
|
||||
"""
|
||||
import pytest
|
||||
import tempfile
|
||||
import re
|
||||
from pathlib import Path
|
||||
from markitect.clean_document_manager import CleanDocumentManager
|
||||
|
||||
|
||||
class TestJSSanity:
|
||||
"""Test suite for JavaScript sanity checks"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup for each test"""
|
||||
self.manager = CleanDocumentManager()
|
||||
|
||||
def test_basic_html_generation_no_edit_mode(self):
|
||||
"""Test that basic HTML generation works without edit mode"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write("# Test Document\n\nThis is a test.")
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=False
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
|
||||
# Read generated HTML
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Basic HTML structure checks
|
||||
assert '<!DOCTYPE html>' in html_content
|
||||
assert '<html' in html_content
|
||||
assert '</html>' in html_content
|
||||
assert '<body' in html_content
|
||||
assert '</body>' in html_content
|
||||
assert 'Test Document' in html_content
|
||||
|
||||
def test_edit_mode_javascript_syntax_validation(self):
|
||||
"""Test that edit mode generates syntactically valid JavaScript"""
|
||||
test_markdown = '''# Test Document
|
||||
|
||||
## Code Block Test
|
||||
```python
|
||||
script = f"""
|
||||
function test() {
|
||||
console.log("test");
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
This contains quotes that could break JavaScript.
|
||||
'''
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
|
||||
# Read generated HTML
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Extract JavaScript content
|
||||
js_content = self.extract_javascript_from_html(html_content)
|
||||
|
||||
# Test 1: Basic syntax validation
|
||||
syntax_errors = self.check_javascript_syntax(js_content)
|
||||
assert len(syntax_errors) == 0, f"JavaScript syntax errors found: {syntax_errors}"
|
||||
|
||||
# Test 2: Check for unescaped quotes
|
||||
quote_errors = self.check_for_quote_escaping_issues(js_content)
|
||||
assert len(quote_errors) == 0, f"Quote escaping issues found: {quote_errors}"
|
||||
|
||||
# Test 3: Check for required constants
|
||||
self.check_required_constants(js_content)
|
||||
|
||||
def test_edit_mode_component_loading(self):
|
||||
"""Test that all required JavaScript components are loaded"""
|
||||
test_markdown = "# Simple Test\n\nBasic content for component loading test."
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Check for required components
|
||||
required_components = [
|
||||
'js/core/debug-system.js',
|
||||
'js/core/section-manager.js',
|
||||
'js/components/dom-renderer.js',
|
||||
'js/controls/control-base.js',
|
||||
'js/main.js'
|
||||
]
|
||||
|
||||
for component in required_components:
|
||||
assert f"// === {component} ===" in html_content, f"Component {component} not loaded"
|
||||
|
||||
def test_edit_mode_class_definitions(self):
|
||||
"""Test that required JavaScript classes are defined"""
|
||||
test_markdown = "# Class Definition Test\n\nTesting class loading."
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Check for required class definitions
|
||||
required_classes = [
|
||||
'class Section',
|
||||
'class SectionManager',
|
||||
'class DOMRenderer',
|
||||
'class MarkitectDebugSystem',
|
||||
'const Control =',
|
||||
'class StatusControl',
|
||||
'class DebugControl',
|
||||
'class EditControl'
|
||||
]
|
||||
|
||||
for class_def in required_classes:
|
||||
assert class_def in html_content, f"Class definition '{class_def}' not found"
|
||||
|
||||
def test_edit_mode_initialization_functions(self):
|
||||
"""Test that required initialization functions are defined"""
|
||||
test_markdown = "# Initialization Test\n\nTesting function definitions."
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Check for required function definitions
|
||||
required_functions = [
|
||||
'function initializeCleanEditor',
|
||||
'function initializeScrollIndicators',
|
||||
'function debug'
|
||||
]
|
||||
|
||||
for func_def in required_functions:
|
||||
assert func_def in html_content, f"Function definition '{func_def}' not found"
|
||||
|
||||
def test_edit_mode_global_exports(self):
|
||||
"""Test that required globals are exported to window"""
|
||||
test_markdown = "# Global Exports Test\n\nTesting window exports."
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Check for required window exports
|
||||
required_exports = [
|
||||
'window.MarkitectDebugSystem = new MarkitectDebugSystem',
|
||||
'window.SectionManager = SectionManager',
|
||||
'window.Control = Control',
|
||||
'window.StatusControl = StatusControl'
|
||||
]
|
||||
|
||||
for export in required_exports:
|
||||
assert export in html_content, f"Window export '{export}' not found"
|
||||
|
||||
# Helper methods
|
||||
|
||||
def extract_javascript_from_html(self, html_content):
|
||||
"""Extract JavaScript content from HTML"""
|
||||
# Find all script tags and extract their content
|
||||
script_pattern = r'<script[^>]*>(.*?)</script>'
|
||||
scripts = re.findall(script_pattern, html_content, re.DOTALL)
|
||||
return '\n'.join(scripts)
|
||||
|
||||
def check_javascript_syntax(self, js_content):
|
||||
"""Basic JavaScript syntax validation"""
|
||||
errors = []
|
||||
|
||||
# Check for common syntax errors
|
||||
|
||||
# 1. Unmatched quotes
|
||||
single_quotes = js_content.count("'") - js_content.count("\\'")
|
||||
double_quotes = js_content.count('"') - js_content.count('\\"')
|
||||
|
||||
if single_quotes % 2 != 0:
|
||||
errors.append("Unmatched single quotes detected")
|
||||
if double_quotes % 2 != 0:
|
||||
errors.append("Unmatched double quotes detected")
|
||||
|
||||
# 2. Unmatched braces
|
||||
open_braces = js_content.count('{')
|
||||
close_braces = js_content.count('}')
|
||||
if open_braces != close_braces:
|
||||
errors.append(f"Unmatched braces: {open_braces} open, {close_braces} close")
|
||||
|
||||
# 3. Unmatched parentheses
|
||||
open_parens = js_content.count('(')
|
||||
close_parens = js_content.count(')')
|
||||
if open_parens != close_parens:
|
||||
errors.append(f"Unmatched parentheses: {open_parens} open, {close_parens} close")
|
||||
|
||||
# 4. Check for unterminated string literals
|
||||
# Look for patterns that suggest unterminated strings
|
||||
unterminated_patterns = [
|
||||
r'[^\\]"[^"]*$', # Double quote not followed by closing quote at line end
|
||||
r'[^\\]\'[^\']*$' # Single quote not followed by closing quote at line end
|
||||
]
|
||||
|
||||
for pattern in unterminated_patterns:
|
||||
matches = re.findall(pattern, js_content, re.MULTILINE)
|
||||
if matches:
|
||||
errors.append(f"Potential unterminated string literals: {len(matches)} found")
|
||||
|
||||
return errors
|
||||
|
||||
def check_for_quote_escaping_issues(self, js_content):
|
||||
"""Check for common quote escaping problems"""
|
||||
errors = []
|
||||
|
||||
# Look for problematic patterns
|
||||
|
||||
# 1. Triple quotes in JSON strings (common Python -> JS issue)
|
||||
if '"""' in js_content and 'const markdownContent' in js_content:
|
||||
errors.append("Triple quotes found in markdownContent - likely escaping issue")
|
||||
|
||||
# 2. Unescaped newlines in strings
|
||||
problem_patterns = [
|
||||
r'"[^"]*\n[^"]*"', # Newline in double-quoted string
|
||||
r"'[^']*\n[^']*'" # Newline in single-quoted string
|
||||
]
|
||||
|
||||
for pattern in problem_patterns:
|
||||
matches = re.findall(pattern, js_content)
|
||||
if matches:
|
||||
errors.append(f"Unescaped newlines in strings: {len(matches)} found")
|
||||
|
||||
return errors
|
||||
|
||||
def check_required_constants(self, js_content):
|
||||
"""Check that required constants are defined"""
|
||||
required_constants = [
|
||||
'const markdownContent =',
|
||||
'const MARKITECT_EDIT_MODE =',
|
||||
'const MARKITECT_EDITOR_CONFIG =',
|
||||
'const EditState =',
|
||||
'const SectionType ='
|
||||
]
|
||||
|
||||
for constant in required_constants:
|
||||
assert constant in js_content, f"Required constant '{constant}' not found"
|
||||
|
||||
def check_for_infinite_retry_loop(self, js_content):
|
||||
"""Check for patterns that indicate infinite retry loops"""
|
||||
errors = []
|
||||
|
||||
# Pattern 1: Retry logic that can loop infinitely
|
||||
if "setTimeout(() => this.initialize(), 50)" in js_content:
|
||||
# Check if there's a proper termination condition
|
||||
if "maxWait" not in js_content and "startTime" not in js_content:
|
||||
errors.append("Found retry setTimeout without timeout protection")
|
||||
|
||||
# Pattern 2: Configuration loading that retries indefinitely
|
||||
retry_patterns = [
|
||||
r"setTimeout\([^)]*initialize[^)]*\)", # setTimeout calling initialize
|
||||
r"if\s*\(\s*!.*\.loaded\s*\)\s*{[^}]*setTimeout" # if not loaded, setTimeout
|
||||
]
|
||||
|
||||
import re
|
||||
for pattern in retry_patterns:
|
||||
matches = re.findall(pattern, js_content)
|
||||
if matches:
|
||||
# Check if there are proper safeguards
|
||||
if "maxWait" not in js_content or "timeout" not in js_content.lower():
|
||||
errors.append(f"Found retry pattern without timeout protection: {pattern}")
|
||||
|
||||
# Pattern 3: Check for MarkitectMain.initialize calling itself recursively
|
||||
if js_content.count("MarkitectMain.initialize") > 2: # Once for definition, once for call
|
||||
if "this.initialized" not in js_content:
|
||||
errors.append("MarkitectMain.initialize may call itself recursively without proper guard")
|
||||
|
||||
return errors
|
||||
|
||||
def check_configuration_loading_logic(self, js_content):
|
||||
"""Check for proper configuration loading setup"""
|
||||
errors = []
|
||||
|
||||
# Check 1: Configuration should be loaded via JSON element
|
||||
if 'markitect-config' not in js_content:
|
||||
errors.append("No markitect-config element found - configuration loading will fail")
|
||||
|
||||
# Check 2: Configuration loader should wait for DOM
|
||||
if 'DOMContentLoaded' not in js_content and 'document.readyState' not in js_content:
|
||||
errors.append("Configuration loading doesn't wait for DOM ready")
|
||||
|
||||
# Check 3: Should have proper error handling for missing config element
|
||||
if "getElementById('markitect-config')" in js_content:
|
||||
if "throw new Error" not in js_content and "console.error" not in js_content:
|
||||
errors.append("No error handling for missing configuration element")
|
||||
|
||||
# Check 4: Check for proper retry logic with timeout
|
||||
if "setTimeout" in js_content and "initialize" in js_content:
|
||||
if "maxWait" not in js_content and "startTime" not in js_content:
|
||||
errors.append("Retry logic present but no timeout mechanism found")
|
||||
|
||||
return errors
|
||||
|
||||
def test_comprehensive_edit_mode_validation(self):
|
||||
"""Comprehensive test that validates the complete edit mode functionality"""
|
||||
# Use the actual GUARDRAILS.md that was causing issues
|
||||
test_markdown = '''# Development Guardrails
|
||||
|
||||
## JavaScript Code Principles
|
||||
|
||||
### 1. No Inline JavaScript in Python
|
||||
**NEVER write JavaScript code directly from Python code**
|
||||
|
||||
❌ **Wrong:**
|
||||
```python
|
||||
script = f"""
|
||||
function myFunction() {{
|
||||
console.log("Hello {name}");
|
||||
}}
|
||||
"""
|
||||
```
|
||||
|
||||
✅ **Correct:**
|
||||
```python
|
||||
# Load from external files only
|
||||
components = [
|
||||
'js/core/section-manager.js',
|
||||
'js/components/debug-panel.js'
|
||||
]
|
||||
```
|
||||
|
||||
This is the content that was breaking the JavaScript generation.
|
||||
'''
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
# This should not raise an exception
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
|
||||
# Read and validate the generated HTML
|
||||
html_content = Path(html_file.name).read_text()
|
||||
js_content = self.extract_javascript_from_html(html_content)
|
||||
|
||||
# Comprehensive validation
|
||||
syntax_errors = self.check_javascript_syntax(js_content)
|
||||
quote_errors = self.check_for_quote_escaping_issues(js_content)
|
||||
|
||||
# If these fail, we have the exact same problem as reported
|
||||
assert len(syntax_errors) == 0, f"SYNTAX ERRORS: {syntax_errors}"
|
||||
assert len(quote_errors) == 0, f"QUOTE ESCAPING ERRORS: {quote_errors}"
|
||||
|
||||
# Verify all required components loaded
|
||||
self.check_required_constants(js_content)
|
||||
|
||||
# CRITICAL: Test for infinite retry loop
|
||||
retry_errors = self.check_for_infinite_retry_loop(js_content)
|
||||
assert len(retry_errors) == 0, f"INFINITE RETRY LOOP DETECTED: {retry_errors}"
|
||||
|
||||
def test_configuration_loading_not_stuck_in_loop(self):
|
||||
"""Test specifically for infinite configuration loading retry loops"""
|
||||
test_markdown = "# Simple Test\n\nBasic content for testing configuration loading."
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
||||
md_file.write(test_markdown)
|
||||
md_file.flush()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
||||
result = self.manager.render_file(
|
||||
input_file=md_file.name,
|
||||
output_file=html_file.name,
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
html_content = Path(html_file.name).read_text()
|
||||
|
||||
# Test for infinite retry patterns
|
||||
retry_issues = self.check_for_infinite_retry_loop(html_content)
|
||||
assert len(retry_issues) == 0, f"INFINITE RETRY LOOP ISSUES: {retry_issues}"
|
||||
|
||||
# Test for proper configuration loading setup
|
||||
config_issues = self.check_configuration_loading_logic(html_content)
|
||||
assert len(config_issues) == 0, f"CONFIGURATION LOADING ISSUES: {config_issues}"
|
||||
@@ -1,512 +0,0 @@
|
||||
"""
|
||||
Comprehensive tests for the Gitea facade/integration layer.
|
||||
|
||||
This test suite covers all Gitea API operations through the facade pattern,
|
||||
ensuring the gitea.client module provides reliable, well-tested functionality
|
||||
for the rest of the application.
|
||||
|
||||
NOTE: This test suite needs to be updated for the new capability-based architecture
|
||||
where Gitea functionality has been moved to capabilities/release-management.
|
||||
Skipping for now until the test can be restructured or moved to the appropriate capability.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
# Skip all tests in this file until gitea tests are moved to release-management capability
|
||||
pytestmark = pytest.mark.skip(reason="Gitea functionality moved to release-management capability - tests need restructuring")
|
||||
|
||||
|
||||
class TestGiteaConfig:
|
||||
"""Test GiteaConfig functionality."""
|
||||
|
||||
def test_config_creation(self):
|
||||
"""Test basic config creation."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo",
|
||||
auth_token="test_token"
|
||||
)
|
||||
|
||||
assert config.gitea_url == "https://gitea.example.com"
|
||||
assert config.repo_owner == "test_owner"
|
||||
assert config.repo_name == "test_repo"
|
||||
assert config.auth_token == "test_token"
|
||||
|
||||
def test_api_url_properties(self):
|
||||
"""Test API URL property generation."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo"
|
||||
)
|
||||
|
||||
assert config.base_api_url == "https://gitea.example.com/api/v1"
|
||||
assert config.repo_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo"
|
||||
assert config.issues_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo/issues"
|
||||
|
||||
@patch('gitea.config.subprocess.run')
|
||||
def test_from_git_repository(self, mock_run):
|
||||
"""Test config creation from git repository."""
|
||||
mock_run.return_value = Mock(
|
||||
stdout="https://gitea.example.com/owner/repo.git",
|
||||
returncode=0
|
||||
)
|
||||
|
||||
config = GiteaConfig.from_git_repository()
|
||||
|
||||
assert config.gitea_url == "https://gitea.example.com"
|
||||
assert config.repo_owner == "owner"
|
||||
assert config.repo_name == "repo"
|
||||
|
||||
def test_config_validation(self):
|
||||
"""Test config validation."""
|
||||
# Valid config should not raise
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="owner",
|
||||
repo_name="repo"
|
||||
)
|
||||
config.validate() # Should not raise
|
||||
|
||||
# Invalid URL should raise
|
||||
invalid_config = GiteaConfig(
|
||||
gitea_url="invalid-url",
|
||||
repo_owner="owner",
|
||||
repo_name="repo"
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
invalid_config.validate()
|
||||
|
||||
|
||||
class TestIssuesClient:
|
||||
"""Test IssuesClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = IssuesClient(self.mock_api)
|
||||
|
||||
# Mock issue for responses
|
||||
self.mock_issue = Mock(spec=Issue)
|
||||
self.mock_issue.number = 1
|
||||
self.mock_issue.title = "Test Issue"
|
||||
self.mock_issue.body = "Test body"
|
||||
self.mock_issue.state = "open"
|
||||
self.mock_issue.html_url = "https://gitea.example.com/owner/repo/issues/1"
|
||||
self.mock_issue.created_at = datetime(2023, 1, 1, 12, 0, 0)
|
||||
self.mock_issue.updated_at = datetime(2023, 1, 1, 12, 0, 0)
|
||||
self.mock_issue.assignee = None
|
||||
self.mock_issue.labels = []
|
||||
self.mock_issue.milestone = None
|
||||
|
||||
def test_get_issue(self):
|
||||
"""Test getting a single issue."""
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.get(1)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
|
||||
def test_list_issues(self):
|
||||
"""Test listing issues."""
|
||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_issue]
|
||||
self.mock_api.list_issues.assert_called_once_with("all", 1, 50)
|
||||
|
||||
def test_list_issues_with_filters(self):
|
||||
"""Test listing issues with filters."""
|
||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
||||
|
||||
result = self.client.list(state="open", page=2, per_page=25)
|
||||
|
||||
assert result == [self.mock_issue]
|
||||
self.mock_api.list_issues.assert_called_once_with("open", 2, 25)
|
||||
|
||||
def test_create_issue(self):
|
||||
"""Test creating an issue."""
|
||||
self.mock_api.create_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.create("Test Title", "Test Body")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.create_issue.assert_called_once()
|
||||
|
||||
def test_create_issue_with_options(self):
|
||||
"""Test creating an issue with optional fields."""
|
||||
self.mock_api.create_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.create(
|
||||
"Test Title",
|
||||
"Test Body",
|
||||
assignees=["user1"],
|
||||
milestone=1,
|
||||
labels=["bug", "priority:high"]
|
||||
)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.create_issue.assert_called_once()
|
||||
|
||||
def test_update_issue(self):
|
||||
"""Test updating an issue."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update(1, title="New Title")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_close_issue(self):
|
||||
"""Test closing an issue."""
|
||||
closed_issue = Mock(spec=Issue)
|
||||
closed_issue.state = "closed"
|
||||
self.mock_api.update_issue.return_value = closed_issue
|
||||
|
||||
result = self.client.close(1)
|
||||
|
||||
assert result.state == "closed"
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_reopen_issue(self):
|
||||
"""Test reopening an issue."""
|
||||
opened_issue = Mock(spec=Issue)
|
||||
opened_issue.state = "open"
|
||||
self.mock_api.update_issue.return_value = opened_issue
|
||||
|
||||
result = self.client.reopen(1)
|
||||
|
||||
assert result.state == "open"
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_add_labels(self):
|
||||
"""Test adding labels to an issue."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="existing")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
# Mock update result
|
||||
updated_issue = Mock(spec=Issue)
|
||||
updated_issue.labels = [Mock(name="existing"), Mock(name="new")]
|
||||
self.mock_api.update_issue.return_value = updated_issue
|
||||
|
||||
result = self.client.add_labels(1, ["new"])
|
||||
|
||||
assert len(result.labels) == 2
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_remove_labels(self):
|
||||
"""Test removing labels from an issue."""
|
||||
# Mock getting current issue
|
||||
label1 = Mock(name="keep")
|
||||
label2 = Mock(name="remove")
|
||||
self.mock_issue.labels = [label1, label2]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
# Mock update result
|
||||
updated_issue = Mock(spec=Issue)
|
||||
updated_issue.labels = [label1]
|
||||
self.mock_api.update_issue.return_value = updated_issue
|
||||
|
||||
result = self.client.remove_labels(1, ["remove"])
|
||||
|
||||
assert len(result.labels) == 1
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_assign_to_milestone(self):
|
||||
"""Test assigning issue to milestone."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.assign_to_milestone(1, 5)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_remove_from_milestone(self):
|
||||
"""Test removing issue from milestone."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.remove_from_milestone(1)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_labels(self):
|
||||
"""Test replacing all labels on an issue."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_labels(1, ["bug", "priority:high"])
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_update_title(self):
|
||||
"""Test updating only issue title."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update_title(1, "New Title")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_update_body(self):
|
||||
"""Test updating only issue body."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update_body(1, "New Body")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_priority(self):
|
||||
"""Test setting issue priority."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="bug")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_priority(1, Priority.HIGH)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_status(self):
|
||||
"""Test setting issue status."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="bug")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_status(1, ProjectState.ACTIVE)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test converting issue to dictionary."""
|
||||
result = self.client.to_dict(self.mock_issue)
|
||||
|
||||
expected_keys = ['number', 'title', 'body', 'state', 'html_url',
|
||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
||||
|
||||
assert all(key in result for key in expected_keys)
|
||||
assert result['number'] == 1
|
||||
assert result['title'] == "Test Issue"
|
||||
assert result['state'] == "open"
|
||||
|
||||
|
||||
class TestMilestonesClient:
|
||||
"""Test MilestonesClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = MilestonesClient(self.mock_api)
|
||||
|
||||
self.mock_milestone = Mock(spec=Milestone)
|
||||
self.mock_milestone.id = 1
|
||||
self.mock_milestone.title = "Test Milestone"
|
||||
|
||||
def test_list_milestones(self):
|
||||
"""Test listing milestones."""
|
||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_milestone]
|
||||
self.mock_api.list_milestones.assert_called_once_with("all")
|
||||
|
||||
def test_list_open_milestones(self):
|
||||
"""Test listing open milestones."""
|
||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
||||
|
||||
result = self.client.list_open()
|
||||
|
||||
assert result == [self.mock_milestone]
|
||||
self.mock_api.list_milestones.assert_called_once_with("open")
|
||||
|
||||
def test_create_milestone(self):
|
||||
"""Test creating a milestone."""
|
||||
self.mock_api.create_milestone.return_value = self.mock_milestone
|
||||
|
||||
result = self.client.create("Test Milestone", "Description")
|
||||
|
||||
assert result == self.mock_milestone
|
||||
self.mock_api.create_milestone.assert_called_once()
|
||||
|
||||
|
||||
class TestLabelsClient:
|
||||
"""Test LabelsClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = LabelsClient(self.mock_api)
|
||||
|
||||
self.mock_label = Mock(spec=Label)
|
||||
self.mock_label.id = 1
|
||||
self.mock_label.name = "bug"
|
||||
|
||||
def test_list_labels(self):
|
||||
"""Test listing labels."""
|
||||
self.mock_api.list_labels.return_value = [self.mock_label]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_label]
|
||||
self.mock_api.list_labels.assert_called_once()
|
||||
|
||||
def test_create_label(self):
|
||||
"""Test creating a label."""
|
||||
self.mock_api.create_label.return_value = self.mock_label
|
||||
|
||||
result = self.client.create("bug", "red", "Bug reports")
|
||||
|
||||
assert result == self.mock_label
|
||||
self.mock_api.create_label.assert_called_once()
|
||||
|
||||
|
||||
class TestGiteaClient:
|
||||
"""Test the main GiteaClient facade."""
|
||||
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_client_initialization(self, mock_api_client):
|
||||
"""Test GiteaClient initialization."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo"
|
||||
)
|
||||
|
||||
client = GiteaClient(config)
|
||||
|
||||
assert isinstance(client.issues, IssuesClient)
|
||||
assert isinstance(client.milestones, MilestonesClient)
|
||||
assert isinstance(client.labels, LabelsClient)
|
||||
mock_api_client.assert_called_once_with(config)
|
||||
|
||||
@patch('gitea.client.GiteaConfig.from_git_repository')
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_client_auto_config(self, mock_api_client, mock_from_git):
|
||||
"""Test GiteaClient with auto-detected config."""
|
||||
mock_config = Mock()
|
||||
mock_from_git.return_value = mock_config
|
||||
|
||||
client = GiteaClient()
|
||||
|
||||
mock_from_git.assert_called_once()
|
||||
mock_api_client.assert_called_once_with(mock_config)
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling throughout the facade."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = IssuesClient(self.mock_api)
|
||||
|
||||
def test_gitea_error_propagation(self):
|
||||
"""Test that GiteaError is properly propagated."""
|
||||
self.mock_api.get_issue.side_effect = GiteaError("API Error")
|
||||
|
||||
with pytest.raises(GiteaError):
|
||||
self.client.get(1)
|
||||
|
||||
def test_not_found_error_propagation(self):
|
||||
"""Test that GiteaNotFoundError is properly propagated."""
|
||||
self.mock_api.get_issue.side_effect = GiteaNotFoundError("Issue not found")
|
||||
|
||||
with pytest.raises(GiteaNotFoundError):
|
||||
self.client.get(999)
|
||||
|
||||
def test_auth_error_propagation(self):
|
||||
"""Test that GiteaAuthError is properly propagated."""
|
||||
self.mock_api.create_issue.side_effect = GiteaAuthError("Unauthorized")
|
||||
|
||||
with pytest.raises(GiteaAuthError):
|
||||
self.client.create("Title", "Body")
|
||||
|
||||
|
||||
class TestIntegrationPatterns:
|
||||
"""Test integration patterns and best practices."""
|
||||
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_consistent_interface(self, mock_api_client):
|
||||
"""Test that the facade provides consistent interfaces."""
|
||||
config = GiteaConfig(gitea_url="https://gitea.example.com",
|
||||
repo_owner="owner", repo_name="repo")
|
||||
client = GiteaClient(config)
|
||||
|
||||
# All sub-clients should be available
|
||||
assert hasattr(client, 'issues')
|
||||
assert hasattr(client, 'milestones')
|
||||
assert hasattr(client, 'labels')
|
||||
|
||||
# All should have consistent method patterns
|
||||
assert hasattr(client.issues, 'list')
|
||||
assert hasattr(client.issues, 'get')
|
||||
assert hasattr(client.issues, 'create')
|
||||
assert hasattr(client.issues, 'update')
|
||||
|
||||
assert hasattr(client.milestones, 'list')
|
||||
assert hasattr(client.milestones, 'create')
|
||||
|
||||
assert hasattr(client.labels, 'list')
|
||||
assert hasattr(client.labels, 'create')
|
||||
|
||||
def test_backward_compatibility_dict_conversion(self):
|
||||
"""Test that to_dict provides backward compatibility."""
|
||||
mock_api = Mock()
|
||||
client = IssuesClient(mock_api)
|
||||
|
||||
# Create a mock issue with all expected attributes
|
||||
mock_issue = Mock(spec=Issue)
|
||||
mock_issue.number = 1
|
||||
mock_issue.title = "Test"
|
||||
mock_issue.body = "Body"
|
||||
mock_issue.state = "open"
|
||||
mock_issue.html_url = "https://example.com"
|
||||
mock_issue.created_at = datetime(2023, 1, 1)
|
||||
mock_issue.updated_at = datetime(2023, 1, 1)
|
||||
mock_issue.assignee = None
|
||||
mock_issue.labels = []
|
||||
mock_issue.milestone = None
|
||||
|
||||
result = client.to_dict(mock_issue)
|
||||
|
||||
# Should contain all expected fields for backward compatibility
|
||||
required_fields = ['number', 'title', 'body', 'state', 'html_url',
|
||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
||||
|
||||
for field in required_fields:
|
||||
assert field in result, f"Missing required field: {field}"
|
||||
|
||||
def test_label_operations_consistency(self):
|
||||
"""Test that label operations work consistently."""
|
||||
mock_api = Mock()
|
||||
client = IssuesClient(mock_api)
|
||||
|
||||
# Mock issue with labels
|
||||
mock_issue = Mock()
|
||||
mock_issue.labels = [Mock(name="bug"), Mock(name="priority:high")]
|
||||
mock_api.get_issue.return_value = mock_issue
|
||||
mock_api.update_issue.return_value = mock_issue
|
||||
|
||||
# Test all label operations
|
||||
client.add_labels(1, ["new-label"])
|
||||
client.remove_labels(1, ["old-label"])
|
||||
client.set_labels(1, ["label1", "label2"])
|
||||
|
||||
# Should have made appropriate API calls
|
||||
assert mock_api.get_issue.call_count == 2 # add_labels and remove_labels
|
||||
assert mock_api.update_issue.call_count == 3 # all three operations
|
||||
@@ -38,7 +38,7 @@ def create_test_workspace(prefix: str = "test") -> Path:
|
||||
|
||||
|
||||
@contextmanager
|
||||
def test_workspace(prefix: str = "test"):
|
||||
def workspace_context(prefix: str = "test"):
|
||||
"""Context manager for test workspace that auto-cleans up.
|
||||
|
||||
Args:
|
||||
|
||||
Reference in New Issue
Block a user