This commit is contained in:
parent
f7ad69f9ec
commit
c0c366e9f4
48
.gitea/workflows/ci.yml
Normal file
48
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
name: Python CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.x' # or specify version like '3.9', '3.10', etc.
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
# Install package in development mode
|
||||||
|
pip install -e .
|
||||||
|
# Install test dependencies if they exist
|
||||||
|
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
|
||||||
|
if [ -f requirements/test.txt ]; then pip install -r requirements/test.txt; fi
|
||||||
|
# Install common test packages
|
||||||
|
pip install pytest pytest-cov flake8
|
||||||
|
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
# Stop the build if there are Python syntax errors or undefined names
|
||||||
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
# Exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
|
||||||
|
- name: Run tests with pytest
|
||||||
|
run: |
|
||||||
|
# Run tests with coverage
|
||||||
|
python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing
|
||||||
|
|
||||||
|
- name: Test package installation
|
||||||
|
run: |
|
||||||
|
# Test that the package can be imported
|
||||||
|
python -c "import pyWebLayout; print('Package imported successfully')"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from .block import Block, BlockType, Parapgraph, Heading, HeadingLevel, Quote, CodeBlock
|
from .block import Block, BlockType, Paragraph, Heading, HeadingLevel, Quote, CodeBlock
|
||||||
from .block import HList, ListItem, ListStyle, Table, TableRow, TableCell
|
from .block import HList, ListItem, ListStyle, Table, TableRow, TableCell
|
||||||
from .block import HorizontalRule, LineBreak, Image
|
from .block import HorizontalRule, LineBreak, Image
|
||||||
from .inline import Word, FormattedSpan
|
from .inline import Word, FormattedSpan
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import List, Dict, Optional, Tuple, Union, Any
|
from typing import List, Dict, Optional, Tuple, Union, Any
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from .block import Block, BlockType, Heading, HeadingLevel, Parapgraph
|
from .block import Block, BlockType, Heading, HeadingLevel, Paragraph
|
||||||
from .functional import Link, Button, Form
|
from .functional import Link, Button, Form
|
||||||
from .inline import Word, FormattedSpan
|
from .inline import Word, FormattedSpan
|
||||||
|
|
||||||
@ -27,13 +27,14 @@ class Document:
|
|||||||
This class manages the logical structure of the document without rendering concerns.
|
This class manages the logical structure of the document without rendering concerns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title: Optional[str] = None, language: str = "en-US"):
|
def __init__(self, title: Optional[str] = None, language: str = "en-US", default_style=None):
|
||||||
"""
|
"""
|
||||||
Initialize a new document.
|
Initialize a new document.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title: The document title
|
title: The document title
|
||||||
language: The document language code
|
language: The document language code
|
||||||
|
default_style: Optional default style for child blocks
|
||||||
"""
|
"""
|
||||||
self._blocks: List[Block] = []
|
self._blocks: List[Block] = []
|
||||||
self._metadata: Dict[MetadataType, Any] = {}
|
self._metadata: Dict[MetadataType, Any] = {}
|
||||||
@ -41,6 +42,7 @@ class Document:
|
|||||||
self._resources: Dict[str, Any] = {} # External resources like images
|
self._resources: Dict[str, Any] = {} # External resources like images
|
||||||
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
|
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
|
||||||
self._scripts: List[str] = [] # JavaScript code
|
self._scripts: List[str] = [] # JavaScript code
|
||||||
|
self._default_style = default_style
|
||||||
|
|
||||||
# Set basic metadata
|
# Set basic metadata
|
||||||
if title:
|
if title:
|
||||||
@ -52,6 +54,16 @@ class Document:
|
|||||||
"""Get the top-level blocks in this document"""
|
"""Get the top-level blocks in this document"""
|
||||||
return self._blocks
|
return self._blocks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_style(self):
|
||||||
|
"""Get the default style for this document"""
|
||||||
|
return self._default_style
|
||||||
|
|
||||||
|
@default_style.setter
|
||||||
|
def default_style(self, style):
|
||||||
|
"""Set the default style for this document"""
|
||||||
|
self._default_style = style
|
||||||
|
|
||||||
def add_block(self, block: Block):
|
def add_block(self, block: Block):
|
||||||
"""
|
"""
|
||||||
Add a block to this document.
|
Add a block to this document.
|
||||||
@ -61,6 +73,55 @@ class Document:
|
|||||||
"""
|
"""
|
||||||
self._blocks.append(block)
|
self._blocks.append(block)
|
||||||
|
|
||||||
|
def create_paragraph(self, style=None) -> Paragraph:
|
||||||
|
"""
|
||||||
|
Create a new paragraph and add it to this document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
style: Optional style override. If None, inherits from document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Paragraph object
|
||||||
|
"""
|
||||||
|
if style is None:
|
||||||
|
style = self._default_style
|
||||||
|
paragraph = Paragraph(style)
|
||||||
|
self.add_block(paragraph)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
|
||||||
|
"""
|
||||||
|
Create a new heading and add it to this document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: The heading level
|
||||||
|
style: Optional style override. If None, inherits from document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Heading object
|
||||||
|
"""
|
||||||
|
if style is None:
|
||||||
|
style = self._default_style
|
||||||
|
heading = Heading(level, style)
|
||||||
|
self.add_block(heading)
|
||||||
|
return heading
|
||||||
|
|
||||||
|
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> 'Chapter':
|
||||||
|
"""
|
||||||
|
Create a new chapter with inherited style.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: The chapter title
|
||||||
|
level: The chapter level
|
||||||
|
style: Optional style override. If None, inherits from document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Chapter object
|
||||||
|
"""
|
||||||
|
if style is None:
|
||||||
|
style = self._default_style
|
||||||
|
return Chapter(title, level, style)
|
||||||
|
|
||||||
def set_metadata(self, meta_type: MetadataType, value: Any):
|
def set_metadata(self, meta_type: MetadataType, value: Any):
|
||||||
"""
|
"""
|
||||||
Set a metadata value.
|
Set a metadata value.
|
||||||
@ -229,18 +290,20 @@ class Chapter:
|
|||||||
A chapter contains a sequence of blocks and has metadata.
|
A chapter contains a sequence of blocks and has metadata.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title: Optional[str] = None, level: int = 1):
|
def __init__(self, title: Optional[str] = None, level: int = 1, style=None):
|
||||||
"""
|
"""
|
||||||
Initialize a new chapter.
|
Initialize a new chapter.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title: The chapter title
|
title: The chapter title
|
||||||
level: The chapter level (1 = top level, 2 = subsection, etc.)
|
level: The chapter level (1 = top level, 2 = subsection, etc.)
|
||||||
|
style: Optional default style for child blocks
|
||||||
"""
|
"""
|
||||||
self._title = title
|
self._title = title
|
||||||
self._level = level
|
self._level = level
|
||||||
self._blocks: List[Block] = []
|
self._blocks: List[Block] = []
|
||||||
self._metadata: Dict[str, Any] = {}
|
self._metadata: Dict[str, Any] = {}
|
||||||
|
self._style = style
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> Optional[str]:
|
def title(self) -> Optional[str]:
|
||||||
@ -262,6 +325,16 @@ class Chapter:
|
|||||||
"""Get the blocks in this chapter"""
|
"""Get the blocks in this chapter"""
|
||||||
return self._blocks
|
return self._blocks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style(self):
|
||||||
|
"""Get the default style for this chapter"""
|
||||||
|
return self._style
|
||||||
|
|
||||||
|
@style.setter
|
||||||
|
def style(self, style):
|
||||||
|
"""Set the default style for this chapter"""
|
||||||
|
self._style = style
|
||||||
|
|
||||||
def add_block(self, block: Block):
|
def add_block(self, block: Block):
|
||||||
"""
|
"""
|
||||||
Add a block to this chapter.
|
Add a block to this chapter.
|
||||||
@ -271,6 +344,39 @@ class Chapter:
|
|||||||
"""
|
"""
|
||||||
self._blocks.append(block)
|
self._blocks.append(block)
|
||||||
|
|
||||||
|
def create_paragraph(self, style=None) -> Paragraph:
|
||||||
|
"""
|
||||||
|
Create a new paragraph and add it to this chapter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
style: Optional style override. If None, inherits from chapter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Paragraph object
|
||||||
|
"""
|
||||||
|
if style is None:
|
||||||
|
style = self._style
|
||||||
|
paragraph = Paragraph(style)
|
||||||
|
self.add_block(paragraph)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
|
||||||
|
"""
|
||||||
|
Create a new heading and add it to this chapter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: The heading level
|
||||||
|
style: Optional style override. If None, inherits from chapter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Heading object
|
||||||
|
"""
|
||||||
|
if style is None:
|
||||||
|
style = self._style
|
||||||
|
heading = Heading(level, style)
|
||||||
|
self.add_block(heading)
|
||||||
|
return heading
|
||||||
|
|
||||||
def set_metadata(self, key: str, value: Any):
|
def set_metadata(self, key: str, value: Any):
|
||||||
"""
|
"""
|
||||||
Set a metadata value.
|
Set a metadata value.
|
||||||
@ -300,7 +406,8 @@ class Book(Document):
|
|||||||
A book is a document that contains chapters.
|
A book is a document that contains chapters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title: Optional[str] = None, author: Optional[str] = None, language: str = "en-US"):
|
def __init__(self, title: Optional[str] = None, author: Optional[str] = None,
|
||||||
|
language: str = "en-US", default_style=None):
|
||||||
"""
|
"""
|
||||||
Initialize a new book.
|
Initialize a new book.
|
||||||
|
|
||||||
@ -308,8 +415,9 @@ class Book(Document):
|
|||||||
title: The book title
|
title: The book title
|
||||||
author: The book author
|
author: The book author
|
||||||
language: The book language code
|
language: The book language code
|
||||||
|
default_style: Optional default style for child chapters and blocks
|
||||||
"""
|
"""
|
||||||
super().__init__(title, language)
|
super().__init__(title, language, default_style)
|
||||||
self._chapters: List[Chapter] = []
|
self._chapters: List[Chapter] = []
|
||||||
|
|
||||||
if author:
|
if author:
|
||||||
@ -329,18 +437,21 @@ class Book(Document):
|
|||||||
"""
|
"""
|
||||||
self._chapters.append(chapter)
|
self._chapters.append(chapter)
|
||||||
|
|
||||||
def create_chapter(self, title: Optional[str] = None, level: int = 1) -> Chapter:
|
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> Chapter:
|
||||||
"""
|
"""
|
||||||
Create and add a new chapter.
|
Create and add a new chapter with inherited style.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title: The chapter title
|
title: The chapter title
|
||||||
level: The chapter level
|
level: The chapter level
|
||||||
|
style: Optional style override. If None, inherits from book
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The new chapter
|
The new chapter
|
||||||
"""
|
"""
|
||||||
chapter = Chapter(title, level)
|
if style is None:
|
||||||
|
style = self._default_style
|
||||||
|
chapter = Chapter(title, level, style)
|
||||||
self.add_chapter(chapter)
|
self.add_chapter(chapter)
|
||||||
return chapter
|
return chapter
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,91 @@ class Word:
|
|||||||
self._previous = previous
|
self._previous = previous
|
||||||
self._next = None
|
self._next = None
|
||||||
self._hyphenated_parts = None # Will store hyphenated parts if word is hyphenated
|
self._hyphenated_parts = None # Will store hyphenated parts if word is hyphenated
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_and_add_to(cls, text: str, container, style: Optional[Font] = None,
|
||||||
|
background=None) -> 'Word':
|
||||||
|
"""
|
||||||
|
Create a new Word and add it to a container, inheriting style and language
|
||||||
|
from the container if not explicitly provided.
|
||||||
|
|
||||||
|
This method provides a convenient way to create words that automatically
|
||||||
|
inherit styling from their container (Paragraph, FormattedSpan, etc.)
|
||||||
|
without copying string values - using object references instead.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The text content of the word
|
||||||
|
container: The container to add the word to (must have add_word method and style property)
|
||||||
|
style: Optional Font style override. If None, inherits from container
|
||||||
|
background: Optional background color override. If None, inherits from container
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Word object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: If the container doesn't have the required add_word method or style property
|
||||||
|
"""
|
||||||
|
# Inherit style from container if not provided
|
||||||
|
if style is None:
|
||||||
|
if hasattr(container, 'style'):
|
||||||
|
style = container.style
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Container {type(container).__name__} must have a 'style' property")
|
||||||
|
|
||||||
|
# Inherit background from container if not provided
|
||||||
|
if background is None and hasattr(container, 'background'):
|
||||||
|
background = container.background
|
||||||
|
|
||||||
|
# Determine the previous word for proper linking
|
||||||
|
previous = None
|
||||||
|
if hasattr(container, '_words') and container._words:
|
||||||
|
# Container has a _words list (like FormattedSpan)
|
||||||
|
previous = container._words[-1]
|
||||||
|
elif hasattr(container, 'words'):
|
||||||
|
# Container has a words() method (like Paragraph)
|
||||||
|
try:
|
||||||
|
# Get the last word from the iterator
|
||||||
|
for _, word in container.words():
|
||||||
|
previous = word
|
||||||
|
except (StopIteration, TypeError):
|
||||||
|
previous = None
|
||||||
|
|
||||||
|
# Create the new word
|
||||||
|
word = cls(text, style, background, previous)
|
||||||
|
|
||||||
|
# Link the previous word to this new one
|
||||||
|
if previous:
|
||||||
|
previous.add_next(word)
|
||||||
|
|
||||||
|
# Add the word to the container
|
||||||
|
if hasattr(container, 'add_word'):
|
||||||
|
# Check if add_word expects a Word object or text string
|
||||||
|
import inspect
|
||||||
|
sig = inspect.signature(container.add_word)
|
||||||
|
params = list(sig.parameters.keys())
|
||||||
|
|
||||||
|
if len(params) > 0:
|
||||||
|
# Peek at the parameter name to guess the expected type
|
||||||
|
param_name = params[0]
|
||||||
|
if param_name in ['word', 'word_obj', 'word_object']:
|
||||||
|
# Expects a Word object
|
||||||
|
container.add_word(word)
|
||||||
|
else:
|
||||||
|
# Might expect text string (like FormattedSpan.add_word)
|
||||||
|
# In this case, we can't use the container's add_word as it would create
|
||||||
|
# a duplicate Word. We need to add directly to the container's word list.
|
||||||
|
if hasattr(container, '_words'):
|
||||||
|
container._words.append(word)
|
||||||
|
else:
|
||||||
|
# Fallback: try calling with the Word object anyway
|
||||||
|
container.add_word(word)
|
||||||
|
else:
|
||||||
|
# No parameters, shouldn't happen with add_word methods
|
||||||
|
container.add_word(word)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Container {type(container).__name__} must have an 'add_word' method")
|
||||||
|
|
||||||
|
return word
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self) -> str:
|
def text(self) -> str:
|
||||||
@ -167,6 +252,45 @@ class FormattedSpan:
|
|||||||
self._background = background if background else style.background
|
self._background = background if background else style.background
|
||||||
self._words: List[Word] = []
|
self._words: List[Word] = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_and_add_to(cls, container, style: Optional[Font] = None, background=None) -> 'FormattedSpan':
|
||||||
|
"""
|
||||||
|
Create a new FormattedSpan and add it to a container, inheriting style from
|
||||||
|
the container if not explicitly provided.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container: The container to add the span to (must have add_span method and style property)
|
||||||
|
style: Optional Font style override. If None, inherits from container
|
||||||
|
background: Optional background color override
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created FormattedSpan object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: If the container doesn't have the required add_span method or style property
|
||||||
|
"""
|
||||||
|
# Inherit style from container if not provided
|
||||||
|
if style is None:
|
||||||
|
if hasattr(container, 'style'):
|
||||||
|
style = container.style
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Container {type(container).__name__} must have a 'style' property")
|
||||||
|
|
||||||
|
# Inherit background from container if not provided
|
||||||
|
if background is None and hasattr(container, 'background'):
|
||||||
|
background = container.background
|
||||||
|
|
||||||
|
# Create the new span
|
||||||
|
span = cls(style, background)
|
||||||
|
|
||||||
|
# Add the span to the container
|
||||||
|
if hasattr(container, 'add_span'):
|
||||||
|
container.add_span(span)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Container {type(container).__name__} must have an 'add_span' method")
|
||||||
|
|
||||||
|
return span
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def style(self) -> Font:
|
def style(self) -> Font:
|
||||||
"""Get the font style of this span"""
|
"""Get the font style of this span"""
|
||||||
|
|||||||
@ -27,7 +27,7 @@ def main():
|
|||||||
parser.add_argument('epub_file', help='Path to EPUB file')
|
parser.add_argument('epub_file', help='Path to EPUB file')
|
||||||
parser.add_argument('--output-dir', '-o', default='output', help='Output directory for rendered pages')
|
parser.add_argument('--output-dir', '-o', default='output', help='Output directory for rendered pages')
|
||||||
parser.add_argument('--width', '-w', type=int, default=800, help='Page width')
|
parser.add_argument('--width', '-w', type=int, default=800, help='Page width')
|
||||||
parser.add_argument('--height', '-h', type=int, default=1000, help='Page height')
|
parser.add_argument('--height', '-y', type=int, default=1000, help='Page height')
|
||||||
parser.add_argument('--margin', '-m', type=int, default=50, help='Page margin')
|
parser.add_argument('--margin', '-m', type=int, default=50, help='Page margin')
|
||||||
parser.add_argument('--max-pages', '-p', type=int, default=10, help='Maximum number of pages to render')
|
parser.add_argument('--max-pages', '-p', type=int, default=10, help='Maximum number of pages to render')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
86
pyWebLayout/examples/simple_epub_test.py
Normal file
86
pyWebLayout/examples/simple_epub_test.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple EPUB test script to isolate the issue.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the parent directory to the path to import pyWebLayout
|
||||||
|
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
|
def test_epub_basic():
|
||||||
|
"""Test basic EPUB functionality without full HTML parsing."""
|
||||||
|
print("Testing basic EPUB components...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test basic document classes
|
||||||
|
from pyWebLayout.abstract.document import Document, Book, Chapter, MetadataType
|
||||||
|
print("✓ Document classes imported")
|
||||||
|
|
||||||
|
# Test creating a simple book
|
||||||
|
book = Book("Test Book", "Test Author")
|
||||||
|
chapter = book.create_chapter("Test Chapter")
|
||||||
|
print("✓ Book and chapter created")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Basic test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_epub_file():
|
||||||
|
"""Test opening the EPUB file without full parsing."""
|
||||||
|
print("Testing EPUB file access...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zipfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
epub_path = "pg174-images-3.epub"
|
||||||
|
if not os.path.exists(epub_path):
|
||||||
|
print(f"✗ EPUB file not found: {epub_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with zipfile.ZipFile(epub_path, 'r') as zip_ref:
|
||||||
|
file_list = zip_ref.namelist()
|
||||||
|
print(f"✓ EPUB file opened, contains {len(file_list)} files")
|
||||||
|
|
||||||
|
# Look for key files
|
||||||
|
has_container = any('container.xml' in f for f in file_list)
|
||||||
|
has_opf = any('.opf' in f for f in file_list)
|
||||||
|
|
||||||
|
print(f"✓ Container file: {'found' if has_container else 'not found'}")
|
||||||
|
print(f"✓ Package file: {'found' if has_opf else 'not found'}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ EPUB file test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Simple EPUB Test")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Test basic functionality
|
||||||
|
if not test_epub_basic():
|
||||||
|
return False
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test EPUB file access
|
||||||
|
if not test_epub_file():
|
||||||
|
return False
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("All basic tests passed!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@ -370,7 +370,7 @@ class EPUBReader:
|
|||||||
|
|
||||||
# Parse HTML and add blocks to chapter
|
# Parse HTML and add blocks to chapter
|
||||||
base_url = os.path.dirname(path)
|
base_url = os.path.dirname(path)
|
||||||
document = parse_html(html, base_url)
|
document = parse_html(html, base_url=base_url)
|
||||||
|
|
||||||
# Copy blocks to the chapter
|
# Copy blocks to the chapter
|
||||||
for block in document.blocks:
|
for block in document.blocks:
|
||||||
@ -381,8 +381,11 @@ class EPUBReader:
|
|||||||
# Add an error message block
|
# Add an error message block
|
||||||
from pyWebLayout.abstract.block import Parapgraph
|
from pyWebLayout.abstract.block import Parapgraph
|
||||||
from pyWebLayout.abstract.inline import Word
|
from pyWebLayout.abstract.inline import Word
|
||||||
|
from pyWebLayout.style import Font
|
||||||
error_para = Parapgraph()
|
error_para = Parapgraph()
|
||||||
error_para.add_word(Word(f"Error loading chapter: {str(e)}"))
|
# Create a default font style for the error message
|
||||||
|
default_font = Font()
|
||||||
|
error_para.add_word(Word(f"Error loading chapter: {str(e)}", default_font))
|
||||||
chapter.add_block(error_para)
|
chapter.add_block(error_para)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
468
tests/test_abstract_document.py
Normal file
468
tests/test_abstract_document.py
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for abstract document elements.
|
||||||
|
|
||||||
|
Tests the Document, Chapter, Book, and MetadataType classes that handle
|
||||||
|
document structure and metadata management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from pyWebLayout.abstract.document import Document, Chapter, Book, MetadataType
|
||||||
|
from pyWebLayout.abstract.block import Parapgraph, Heading, HeadingLevel, BlockType
|
||||||
|
from pyWebLayout.abstract.inline import Word, FormattedSpan
|
||||||
|
from pyWebLayout.style import Font
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetadataType(unittest.TestCase):
|
||||||
|
"""Test cases for MetadataType enum."""
|
||||||
|
|
||||||
|
def test_metadata_types(self):
|
||||||
|
"""Test that all expected metadata types exist."""
|
||||||
|
expected_types = [
|
||||||
|
'TITLE', 'AUTHOR', 'DESCRIPTION', 'KEYWORDS', 'LANGUAGE',
|
||||||
|
'PUBLICATION_DATE', 'MODIFIED_DATE', 'PUBLISHER', 'IDENTIFIER',
|
||||||
|
'COVER_IMAGE', 'CUSTOM'
|
||||||
|
]
|
||||||
|
|
||||||
|
for type_name in expected_types:
|
||||||
|
self.assertTrue(hasattr(MetadataType, type_name))
|
||||||
|
|
||||||
|
# Test custom type has expected value
|
||||||
|
self.assertEqual(MetadataType.CUSTOM.value, 100)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDocument(unittest.TestCase):
|
||||||
|
"""Test cases for Document class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.doc = Document("Test Document", "en-US")
|
||||||
|
self.font = Font()
|
||||||
|
|
||||||
|
def test_document_creation(self):
|
||||||
|
"""Test document creation with basic parameters."""
|
||||||
|
self.assertEqual(self.doc.get_title(), "Test Document")
|
||||||
|
self.assertEqual(self.doc.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||||
|
self.assertEqual(len(self.doc.blocks), 0)
|
||||||
|
|
||||||
|
def test_document_creation_minimal(self):
|
||||||
|
"""Test document creation with minimal parameters."""
|
||||||
|
doc = Document()
|
||||||
|
self.assertIsNone(doc.get_title())
|
||||||
|
self.assertEqual(doc.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||||
|
|
||||||
|
def test_metadata_management(self):
|
||||||
|
"""Test setting and getting metadata."""
|
||||||
|
# Set various metadata types
|
||||||
|
self.doc.set_metadata(MetadataType.AUTHOR, "John Doe")
|
||||||
|
self.doc.set_metadata(MetadataType.DESCRIPTION, "A test document")
|
||||||
|
self.doc.set_metadata(MetadataType.KEYWORDS, ["test", "document"])
|
||||||
|
|
||||||
|
# Test retrieval
|
||||||
|
self.assertEqual(self.doc.get_metadata(MetadataType.AUTHOR), "John Doe")
|
||||||
|
self.assertEqual(self.doc.get_metadata(MetadataType.DESCRIPTION), "A test document")
|
||||||
|
self.assertEqual(self.doc.get_metadata(MetadataType.KEYWORDS), ["test", "document"])
|
||||||
|
|
||||||
|
# Test non-existent metadata
|
||||||
|
self.assertIsNone(self.doc.get_metadata(MetadataType.PUBLISHER))
|
||||||
|
|
||||||
|
def test_title_convenience_methods(self):
|
||||||
|
"""Test title getter and setter convenience methods."""
|
||||||
|
# Test setting title
|
||||||
|
self.doc.set_title("New Title")
|
||||||
|
self.assertEqual(self.doc.get_title(), "New Title")
|
||||||
|
|
||||||
|
# Test that it's also in metadata
|
||||||
|
self.assertEqual(self.doc.get_metadata(MetadataType.TITLE), "New Title")
|
||||||
|
|
||||||
|
def test_block_management(self):
|
||||||
|
"""Test adding and managing blocks."""
|
||||||
|
# Create some blocks
|
||||||
|
para1 = Parapgraph()
|
||||||
|
para2 = Parapgraph()
|
||||||
|
heading = Heading(HeadingLevel.H1)
|
||||||
|
|
||||||
|
# Add blocks
|
||||||
|
self.doc.add_block(para1)
|
||||||
|
self.doc.add_block(heading)
|
||||||
|
self.doc.add_block(para2)
|
||||||
|
|
||||||
|
# Test blocks list
|
||||||
|
self.assertEqual(len(self.doc.blocks), 3)
|
||||||
|
self.assertEqual(self.doc.blocks[0], para1)
|
||||||
|
self.assertEqual(self.doc.blocks[1], heading)
|
||||||
|
self.assertEqual(self.doc.blocks[2], para2)
|
||||||
|
|
||||||
|
def test_anchor_management(self):
|
||||||
|
"""Test named anchor functionality."""
|
||||||
|
heading = Heading(HeadingLevel.H1)
|
||||||
|
para = Parapgraph()
|
||||||
|
|
||||||
|
# Add anchors
|
||||||
|
self.doc.add_anchor("intro", heading)
|
||||||
|
self.doc.add_anchor("content", para)
|
||||||
|
|
||||||
|
# Test retrieval
|
||||||
|
self.assertEqual(self.doc.get_anchor("intro"), heading)
|
||||||
|
self.assertEqual(self.doc.get_anchor("content"), para)
|
||||||
|
self.assertIsNone(self.doc.get_anchor("nonexistent"))
|
||||||
|
|
||||||
|
def test_resource_management(self):
|
||||||
|
"""Test document resource management."""
|
||||||
|
# Add various resources
|
||||||
|
self.doc.add_resource("image1", {"type": "image", "path": "test.jpg"})
|
||||||
|
self.doc.add_resource("style1", {"type": "css", "content": "body {}"})
|
||||||
|
|
||||||
|
# Test retrieval
|
||||||
|
image = self.doc.get_resource("image1")
|
||||||
|
self.assertEqual(image["type"], "image")
|
||||||
|
self.assertEqual(image["path"], "test.jpg")
|
||||||
|
|
||||||
|
style = self.doc.get_resource("style1")
|
||||||
|
self.assertEqual(style["type"], "css")
|
||||||
|
|
||||||
|
# Test non-existent resource
|
||||||
|
self.assertIsNone(self.doc.get_resource("nonexistent"))
|
||||||
|
|
||||||
|
def test_stylesheet_management(self):
|
||||||
|
"""Test stylesheet addition."""
|
||||||
|
# Add stylesheets
|
||||||
|
css1 = {"href": "style.css", "type": "text/css"}
|
||||||
|
css2 = {"href": "theme.css", "type": "text/css"}
|
||||||
|
|
||||||
|
self.doc.add_stylesheet(css1)
|
||||||
|
self.doc.add_stylesheet(css2)
|
||||||
|
|
||||||
|
# Test that stylesheets are stored
|
||||||
|
self.assertEqual(len(self.doc._stylesheets), 2)
|
||||||
|
self.assertEqual(self.doc._stylesheets[0], css1)
|
||||||
|
self.assertEqual(self.doc._stylesheets[1], css2)
|
||||||
|
|
||||||
|
def test_script_management(self):
|
||||||
|
"""Test script addition."""
|
||||||
|
# Add scripts
|
||||||
|
script1 = "console.log('Hello');"
|
||||||
|
script2 = "document.ready(function(){});"
|
||||||
|
|
||||||
|
self.doc.add_script(script1)
|
||||||
|
self.doc.add_script(script2)
|
||||||
|
|
||||||
|
# Test that scripts are stored
|
||||||
|
self.assertEqual(len(self.doc._scripts), 2)
|
||||||
|
self.assertEqual(self.doc._scripts[0], script1)
|
||||||
|
self.assertEqual(self.doc._scripts[1], script2)
|
||||||
|
|
||||||
|
def test_find_blocks_by_type(self):
|
||||||
|
"""Test finding blocks by type."""
|
||||||
|
# Create blocks of different types
|
||||||
|
para1 = Parapgraph()
|
||||||
|
para2 = Parapgraph()
|
||||||
|
heading1 = Heading(HeadingLevel.H1)
|
||||||
|
heading2 = Heading(HeadingLevel.H2)
|
||||||
|
|
||||||
|
# Add blocks to document
|
||||||
|
self.doc.add_block(para1)
|
||||||
|
self.doc.add_block(heading1)
|
||||||
|
self.doc.add_block(para2)
|
||||||
|
self.doc.add_block(heading2)
|
||||||
|
|
||||||
|
# Test finding paragraphs
|
||||||
|
paragraphs = self.doc.find_blocks_by_type(BlockType.PARAGRAPH)
|
||||||
|
self.assertEqual(len(paragraphs), 2)
|
||||||
|
self.assertIn(para1, paragraphs)
|
||||||
|
self.assertIn(para2, paragraphs)
|
||||||
|
|
||||||
|
# Test finding headings
|
||||||
|
headings = self.doc.find_blocks_by_type(BlockType.HEADING)
|
||||||
|
self.assertEqual(len(headings), 2)
|
||||||
|
self.assertIn(heading1, headings)
|
||||||
|
self.assertIn(heading2, headings)
|
||||||
|
|
||||||
|
def test_find_headings(self):
|
||||||
|
"""Test finding heading blocks specifically."""
|
||||||
|
# Create mixed blocks
|
||||||
|
para = Parapgraph()
|
||||||
|
h1 = Heading(HeadingLevel.H1)
|
||||||
|
h2 = Heading(HeadingLevel.H2)
|
||||||
|
|
||||||
|
# Add words to headings for title extraction
|
||||||
|
word1 = Word("Chapter", self.font)
|
||||||
|
word2 = Word("One", self.font)
|
||||||
|
h1.add_word(word1)
|
||||||
|
h1.add_word(word2)
|
||||||
|
|
||||||
|
word3 = Word("Section", self.font)
|
||||||
|
h2.add_word(word3)
|
||||||
|
|
||||||
|
self.doc.add_block(para)
|
||||||
|
self.doc.add_block(h1)
|
||||||
|
self.doc.add_block(h2)
|
||||||
|
|
||||||
|
# Test finding headings
|
||||||
|
headings = self.doc.find_headings()
|
||||||
|
self.assertEqual(len(headings), 2)
|
||||||
|
self.assertIn(h1, headings)
|
||||||
|
self.assertIn(h2, headings)
|
||||||
|
self.assertNotIn(para, headings)
|
||||||
|
|
||||||
|
def test_generate_table_of_contents(self):
|
||||||
|
"""Test table of contents generation."""
|
||||||
|
# Create headings with content
|
||||||
|
h1 = Heading(HeadingLevel.H1)
|
||||||
|
h2 = Heading(HeadingLevel.H2)
|
||||||
|
h3 = Heading(HeadingLevel.H3)
|
||||||
|
|
||||||
|
# Add words to headings
|
||||||
|
h1.add_word(Word("Introduction", self.font))
|
||||||
|
h2.add_word(Word("Getting", self.font))
|
||||||
|
h2.add_word(Word("Started", self.font))
|
||||||
|
h3.add_word(Word("Installation", self.font))
|
||||||
|
|
||||||
|
self.doc.add_block(h1)
|
||||||
|
self.doc.add_block(h2)
|
||||||
|
self.doc.add_block(h3)
|
||||||
|
|
||||||
|
# Generate TOC
|
||||||
|
toc = self.doc.generate_table_of_contents()
|
||||||
|
|
||||||
|
# Test TOC structure
|
||||||
|
self.assertEqual(len(toc), 3)
|
||||||
|
|
||||||
|
# Test first entry
|
||||||
|
level, title, block = toc[0]
|
||||||
|
self.assertEqual(level, 1) # H1
|
||||||
|
self.assertEqual(title, "Introduction")
|
||||||
|
self.assertEqual(block, h1)
|
||||||
|
|
||||||
|
# Test second entry
|
||||||
|
level, title, block = toc[1]
|
||||||
|
self.assertEqual(level, 2) # H2
|
||||||
|
self.assertEqual(title, "Getting Started")
|
||||||
|
self.assertEqual(block, h2)
|
||||||
|
|
||||||
|
# Test third entry
|
||||||
|
level, title, block = toc[2]
|
||||||
|
self.assertEqual(level, 3) # H3
|
||||||
|
self.assertEqual(title, "Installation")
|
||||||
|
self.assertEqual(block, h3)
|
||||||
|
|
||||||
|
|
||||||
|
class TestChapter(unittest.TestCase):
|
||||||
|
"""Test cases for Chapter class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.chapter = Chapter("Test Chapter", 1)
|
||||||
|
|
||||||
|
def test_chapter_creation(self):
|
||||||
|
"""Test chapter creation."""
|
||||||
|
self.assertEqual(self.chapter.title, "Test Chapter")
|
||||||
|
self.assertEqual(self.chapter.level, 1)
|
||||||
|
self.assertEqual(len(self.chapter.blocks), 0)
|
||||||
|
|
||||||
|
def test_chapter_creation_minimal(self):
|
||||||
|
"""Test chapter creation with minimal parameters."""
|
||||||
|
chapter = Chapter()
|
||||||
|
self.assertIsNone(chapter.title)
|
||||||
|
self.assertEqual(chapter.level, 1)
|
||||||
|
|
||||||
|
def test_title_property(self):
|
||||||
|
"""Test title property getter and setter."""
|
||||||
|
# Test setter
|
||||||
|
self.chapter.title = "New Chapter Title"
|
||||||
|
self.assertEqual(self.chapter.title, "New Chapter Title")
|
||||||
|
|
||||||
|
# Test setting to None
|
||||||
|
self.chapter.title = None
|
||||||
|
self.assertIsNone(self.chapter.title)
|
||||||
|
|
||||||
|
def test_level_property(self):
|
||||||
|
"""Test level property."""
|
||||||
|
self.assertEqual(self.chapter.level, 1)
|
||||||
|
|
||||||
|
# Level should be read-only (no setter test)
|
||||||
|
# This is by design based on the class definition
|
||||||
|
|
||||||
|
def test_block_management(self):
|
||||||
|
"""Test adding blocks to chapter."""
|
||||||
|
para1 = Parapgraph()
|
||||||
|
para2 = Parapgraph()
|
||||||
|
heading = Heading(HeadingLevel.H2)
|
||||||
|
|
||||||
|
# Add blocks
|
||||||
|
self.chapter.add_block(para1)
|
||||||
|
self.chapter.add_block(heading)
|
||||||
|
self.chapter.add_block(para2)
|
||||||
|
|
||||||
|
# Test blocks list
|
||||||
|
self.assertEqual(len(self.chapter.blocks), 3)
|
||||||
|
self.assertEqual(self.chapter.blocks[0], para1)
|
||||||
|
self.assertEqual(self.chapter.blocks[1], heading)
|
||||||
|
self.assertEqual(self.chapter.blocks[2], para2)
|
||||||
|
|
||||||
|
def test_metadata_management(self):
|
||||||
|
"""Test chapter metadata."""
|
||||||
|
# Set metadata
|
||||||
|
self.chapter.set_metadata("author", "Jane Doe")
|
||||||
|
self.chapter.set_metadata("word_count", 1500)
|
||||||
|
self.chapter.set_metadata("tags", ["intro", "basics"])
|
||||||
|
|
||||||
|
# Test retrieval
|
||||||
|
self.assertEqual(self.chapter.get_metadata("author"), "Jane Doe")
|
||||||
|
self.assertEqual(self.chapter.get_metadata("word_count"), 1500)
|
||||||
|
self.assertEqual(self.chapter.get_metadata("tags"), ["intro", "basics"])
|
||||||
|
|
||||||
|
# Test non-existent metadata
|
||||||
|
self.assertIsNone(self.chapter.get_metadata("nonexistent"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestBook(unittest.TestCase):
|
||||||
|
"""Test cases for Book class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.book = Book("Test Book", "Author Name", "en-US")
|
||||||
|
|
||||||
|
def test_book_creation(self):
|
||||||
|
"""Test book creation with all parameters."""
|
||||||
|
self.assertEqual(self.book.get_title(), "Test Book")
|
||||||
|
self.assertEqual(self.book.get_author(), "Author Name")
|
||||||
|
self.assertEqual(self.book.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||||
|
self.assertEqual(len(self.book.chapters), 0)
|
||||||
|
|
||||||
|
def test_book_creation_minimal(self):
|
||||||
|
"""Test book creation with minimal parameters."""
|
||||||
|
book = Book()
|
||||||
|
self.assertIsNone(book.get_title())
|
||||||
|
self.assertIsNone(book.get_author())
|
||||||
|
self.assertEqual(book.get_metadata(MetadataType.LANGUAGE), "en-US")
|
||||||
|
|
||||||
|
def test_book_creation_partial(self):
|
||||||
|
"""Test book creation with partial parameters."""
|
||||||
|
book = Book(title="Just Title")
|
||||||
|
self.assertEqual(book.get_title(), "Just Title")
|
||||||
|
self.assertIsNone(book.get_author())
|
||||||
|
|
||||||
|
def test_author_convenience_methods(self):
|
||||||
|
"""Test author getter and setter convenience methods."""
|
||||||
|
# Test setting author
|
||||||
|
self.book.set_author("New Author")
|
||||||
|
self.assertEqual(self.book.get_author(), "New Author")
|
||||||
|
|
||||||
|
# Test that it's also in metadata
|
||||||
|
self.assertEqual(self.book.get_metadata(MetadataType.AUTHOR), "New Author")
|
||||||
|
|
||||||
|
def test_chapter_management(self):
|
||||||
|
"""Test adding and managing chapters."""
|
||||||
|
# Create chapters
|
||||||
|
ch1 = Chapter("Introduction", 1)
|
||||||
|
ch2 = Chapter("Getting Started", 1)
|
||||||
|
ch3 = Chapter("Advanced Topics", 1)
|
||||||
|
|
||||||
|
# Add chapters
|
||||||
|
self.book.add_chapter(ch1)
|
||||||
|
self.book.add_chapter(ch2)
|
||||||
|
self.book.add_chapter(ch3)
|
||||||
|
|
||||||
|
# Test chapters list
|
||||||
|
self.assertEqual(len(self.book.chapters), 3)
|
||||||
|
self.assertEqual(self.book.chapters[0], ch1)
|
||||||
|
self.assertEqual(self.book.chapters[1], ch2)
|
||||||
|
self.assertEqual(self.book.chapters[2], ch3)
|
||||||
|
|
||||||
|
def test_create_chapter(self):
|
||||||
|
"""Test creating chapters through the book."""
|
||||||
|
# Create chapter with title and level
|
||||||
|
ch1 = self.book.create_chapter("Chapter 1", 1)
|
||||||
|
self.assertEqual(ch1.title, "Chapter 1")
|
||||||
|
self.assertEqual(ch1.level, 1)
|
||||||
|
self.assertEqual(len(self.book.chapters), 1)
|
||||||
|
self.assertEqual(self.book.chapters[0], ch1)
|
||||||
|
|
||||||
|
# Create chapter with minimal parameters
|
||||||
|
ch2 = self.book.create_chapter()
|
||||||
|
self.assertIsNone(ch2.title)
|
||||||
|
self.assertEqual(ch2.level, 1)
|
||||||
|
self.assertEqual(len(self.book.chapters), 2)
|
||||||
|
|
||||||
|
def test_generate_book_toc(self):
|
||||||
|
"""Test table of contents generation for book."""
|
||||||
|
# Create chapters with different levels
|
||||||
|
ch1 = Chapter("Introduction", 1)
|
||||||
|
ch2 = Chapter("Getting Started", 1)
|
||||||
|
ch3 = Chapter("Basic Concepts", 2)
|
||||||
|
ch4 = Chapter("Advanced Topics", 1)
|
||||||
|
ch5 = Chapter("Best Practices", 2)
|
||||||
|
|
||||||
|
# Add chapters to book
|
||||||
|
self.book.add_chapter(ch1)
|
||||||
|
self.book.add_chapter(ch2)
|
||||||
|
self.book.add_chapter(ch3)
|
||||||
|
self.book.add_chapter(ch4)
|
||||||
|
self.book.add_chapter(ch5)
|
||||||
|
|
||||||
|
# Generate TOC
|
||||||
|
toc = self.book.generate_table_of_contents()
|
||||||
|
|
||||||
|
# Test TOC structure
|
||||||
|
self.assertEqual(len(toc), 5)
|
||||||
|
|
||||||
|
# Test entries
|
||||||
|
expected = [
|
||||||
|
(1, "Introduction", ch1),
|
||||||
|
(1, "Getting Started", ch2),
|
||||||
|
(2, "Basic Concepts", ch3),
|
||||||
|
(1, "Advanced Topics", ch4),
|
||||||
|
(2, "Best Practices", ch5)
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (exp_level, exp_title, exp_chapter) in enumerate(expected):
|
||||||
|
level, title, chapter = toc[i]
|
||||||
|
self.assertEqual(level, exp_level)
|
||||||
|
self.assertEqual(title, exp_title)
|
||||||
|
self.assertEqual(chapter, exp_chapter)
|
||||||
|
|
||||||
|
def test_generate_book_toc_with_untitled_chapters(self):
|
||||||
|
"""Test TOC generation with chapters that have no title."""
|
||||||
|
# Create chapters, some without titles
|
||||||
|
ch1 = Chapter("Introduction", 1)
|
||||||
|
ch2 = Chapter(None, 1) # No title
|
||||||
|
ch3 = Chapter("Conclusion", 1)
|
||||||
|
|
||||||
|
self.book.add_chapter(ch1)
|
||||||
|
self.book.add_chapter(ch2)
|
||||||
|
self.book.add_chapter(ch3)
|
||||||
|
|
||||||
|
# Generate TOC
|
||||||
|
toc = self.book.generate_table_of_contents()
|
||||||
|
|
||||||
|
# Should only include chapters with titles
|
||||||
|
self.assertEqual(len(toc), 2)
|
||||||
|
|
||||||
|
level, title, chapter = toc[0]
|
||||||
|
self.assertEqual(title, "Introduction")
|
||||||
|
self.assertEqual(chapter, ch1)
|
||||||
|
|
||||||
|
level, title, chapter = toc[1]
|
||||||
|
self.assertEqual(title, "Conclusion")
|
||||||
|
self.assertEqual(chapter, ch3)
|
||||||
|
|
||||||
|
def test_book_inherits_document_features(self):
|
||||||
|
"""Test that Book inherits all Document functionality."""
|
||||||
|
# Test that book can use all document methods
|
||||||
|
# Add blocks directly to book
|
||||||
|
para = Parapgraph()
|
||||||
|
self.book.add_block(para)
|
||||||
|
self.assertEqual(len(self.book.blocks), 1)
|
||||||
|
|
||||||
|
# Test metadata
|
||||||
|
self.book.set_metadata(MetadataType.PUBLISHER, "Test Publisher")
|
||||||
|
self.assertEqual(self.book.get_metadata(MetadataType.PUBLISHER), "Test Publisher")
|
||||||
|
|
||||||
|
# Test anchors
|
||||||
|
heading = Heading(HeadingLevel.H1)
|
||||||
|
self.book.add_anchor("preface", heading)
|
||||||
|
self.assertEqual(self.book.get_anchor("preface"), heading)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
529
tests/test_abstract_functional.py
Normal file
529
tests/test_abstract_functional.py
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for abstract functional elements.
|
||||||
|
|
||||||
|
Tests the Link, Button, Form, FormField, and related classes that handle
|
||||||
|
interactive functionality and user interface elements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
from pyWebLayout.abstract.functional import (
|
||||||
|
Link, LinkType, Button, Form, FormField, FormFieldType
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLinkType(unittest.TestCase):
|
||||||
|
"""Test cases for LinkType enum."""
|
||||||
|
|
||||||
|
def test_link_types(self):
|
||||||
|
"""Test that all expected link types exist."""
|
||||||
|
expected_types = ['INTERNAL', 'EXTERNAL', 'API', 'FUNCTION']
|
||||||
|
|
||||||
|
for type_name in expected_types:
|
||||||
|
self.assertTrue(hasattr(LinkType, type_name))
|
||||||
|
|
||||||
|
# Test specific values
|
||||||
|
self.assertEqual(LinkType.INTERNAL.value, 1)
|
||||||
|
self.assertEqual(LinkType.EXTERNAL.value, 2)
|
||||||
|
self.assertEqual(LinkType.API.value, 3)
|
||||||
|
self.assertEqual(LinkType.FUNCTION.value, 4)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLink(unittest.TestCase):
|
||||||
|
"""Test cases for Link class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_callback = Mock(return_value="callback_result")
|
||||||
|
|
||||||
|
def test_link_creation_minimal(self):
|
||||||
|
"""Test link creation with minimal parameters."""
|
||||||
|
link = Link("test-location")
|
||||||
|
|
||||||
|
self.assertEqual(link.location, "test-location")
|
||||||
|
self.assertEqual(link.link_type, LinkType.INTERNAL) # Default
|
||||||
|
self.assertEqual(link.params, {})
|
||||||
|
self.assertIsNone(link.title)
|
||||||
|
self.assertIsNone(link._callback)
|
||||||
|
|
||||||
|
def test_link_creation_full(self):
|
||||||
|
"""Test link creation with all parameters."""
|
||||||
|
params = {"param1": "value1", "param2": "value2"}
|
||||||
|
link = Link(
|
||||||
|
location="https://example.com",
|
||||||
|
link_type=LinkType.EXTERNAL,
|
||||||
|
callback=self.mock_callback,
|
||||||
|
params=params,
|
||||||
|
title="Example Link"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(link.location, "https://example.com")
|
||||||
|
self.assertEqual(link.link_type, LinkType.EXTERNAL)
|
||||||
|
self.assertEqual(link.params, params)
|
||||||
|
self.assertEqual(link.title, "Example Link")
|
||||||
|
self.assertEqual(link._callback, self.mock_callback)
|
||||||
|
|
||||||
|
def test_internal_link_execution(self):
|
||||||
|
"""Test executing internal links."""
|
||||||
|
link = Link("#section1", LinkType.INTERNAL)
|
||||||
|
result = link.execute()
|
||||||
|
|
||||||
|
# Internal links should return the location
|
||||||
|
self.assertEqual(result, "#section1")
|
||||||
|
|
||||||
|
def test_external_link_execution(self):
|
||||||
|
"""Test executing external links."""
|
||||||
|
link = Link("https://example.com", LinkType.EXTERNAL)
|
||||||
|
result = link.execute()
|
||||||
|
|
||||||
|
# External links should return the location
|
||||||
|
self.assertEqual(result, "https://example.com")
|
||||||
|
|
||||||
|
def test_api_link_execution(self):
|
||||||
|
"""Test executing API links with callback."""
|
||||||
|
params = {"action": "save", "id": 123}
|
||||||
|
link = Link(
|
||||||
|
location="/api/save",
|
||||||
|
link_type=LinkType.API,
|
||||||
|
callback=self.mock_callback,
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
|
||||||
|
result = link.execute()
|
||||||
|
|
||||||
|
# Should call callback with location and params
|
||||||
|
self.mock_callback.assert_called_once_with("/api/save", action="save", id=123)
|
||||||
|
self.assertEqual(result, "callback_result")
|
||||||
|
|
||||||
|
def test_function_link_execution(self):
|
||||||
|
"""Test executing function links with callback."""
|
||||||
|
params = {"data": "test"}
|
||||||
|
link = Link(
|
||||||
|
location="save_document",
|
||||||
|
link_type=LinkType.FUNCTION,
|
||||||
|
callback=self.mock_callback,
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
|
||||||
|
result = link.execute()
|
||||||
|
|
||||||
|
# Should call callback with location and params
|
||||||
|
self.mock_callback.assert_called_once_with("save_document", data="test")
|
||||||
|
self.assertEqual(result, "callback_result")
|
||||||
|
|
||||||
|
def test_api_link_without_callback(self):
|
||||||
|
"""Test API link without callback returns location."""
|
||||||
|
link = Link("/api/endpoint", LinkType.API)
|
||||||
|
result = link.execute()
|
||||||
|
|
||||||
|
# Without callback, should return location
|
||||||
|
self.assertEqual(result, "/api/endpoint")
|
||||||
|
|
||||||
|
def test_function_link_without_callback(self):
|
||||||
|
"""Test function link without callback returns location."""
|
||||||
|
link = Link("function_name", LinkType.FUNCTION)
|
||||||
|
result = link.execute()
|
||||||
|
|
||||||
|
# Without callback, should return location
|
||||||
|
self.assertEqual(result, "function_name")
|
||||||
|
|
||||||
|
def test_link_properties(self):
|
||||||
|
"""Test link property access."""
|
||||||
|
params = {"key": "value"}
|
||||||
|
link = Link(
|
||||||
|
location="test",
|
||||||
|
link_type=LinkType.API,
|
||||||
|
params=params,
|
||||||
|
title="Test Title"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test all property getters
|
||||||
|
self.assertEqual(link.location, "test")
|
||||||
|
self.assertEqual(link.link_type, LinkType.API)
|
||||||
|
self.assertEqual(link.params, params)
|
||||||
|
self.assertEqual(link.title, "Test Title")
|
||||||
|
|
||||||
|
|
||||||
|
class TestButton(unittest.TestCase):
|
||||||
|
"""Test cases for Button class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_callback = Mock(return_value="button_clicked")
|
||||||
|
|
||||||
|
def test_button_creation_minimal(self):
|
||||||
|
"""Test button creation with minimal parameters."""
|
||||||
|
button = Button("Click Me", self.mock_callback)
|
||||||
|
|
||||||
|
self.assertEqual(button.label, "Click Me")
|
||||||
|
self.assertEqual(button._callback, self.mock_callback)
|
||||||
|
self.assertEqual(button.params, {})
|
||||||
|
self.assertTrue(button.enabled)
|
||||||
|
|
||||||
|
def test_button_creation_full(self):
|
||||||
|
"""Test button creation with all parameters."""
|
||||||
|
params = {"action": "submit", "form_id": "test_form"}
|
||||||
|
button = Button(
|
||||||
|
label="Submit",
|
||||||
|
callback=self.mock_callback,
|
||||||
|
params=params,
|
||||||
|
enabled=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(button.label, "Submit")
|
||||||
|
self.assertEqual(button._callback, self.mock_callback)
|
||||||
|
self.assertEqual(button.params, params)
|
||||||
|
self.assertFalse(button.enabled)
|
||||||
|
|
||||||
|
def test_button_label_property(self):
|
||||||
|
"""Test button label getter and setter."""
|
||||||
|
button = Button("Original", self.mock_callback)
|
||||||
|
|
||||||
|
# Test getter
|
||||||
|
self.assertEqual(button.label, "Original")
|
||||||
|
|
||||||
|
# Test setter
|
||||||
|
button.label = "New Label"
|
||||||
|
self.assertEqual(button.label, "New Label")
|
||||||
|
|
||||||
|
def test_button_enabled_property(self):
|
||||||
|
"""Test button enabled getter and setter."""
|
||||||
|
button = Button("Test", self.mock_callback, enabled=True)
|
||||||
|
|
||||||
|
# Test initial state
|
||||||
|
self.assertTrue(button.enabled)
|
||||||
|
|
||||||
|
# Test setter
|
||||||
|
button.enabled = False
|
||||||
|
self.assertFalse(button.enabled)
|
||||||
|
|
||||||
|
button.enabled = True
|
||||||
|
self.assertTrue(button.enabled)
|
||||||
|
|
||||||
|
def test_button_execute_enabled(self):
|
||||||
|
"""Test executing enabled button."""
|
||||||
|
params = {"data": "test_data"}
|
||||||
|
button = Button("Test", self.mock_callback, params=params, enabled=True)
|
||||||
|
|
||||||
|
result = button.execute()
|
||||||
|
|
||||||
|
# Should call callback with params
|
||||||
|
self.mock_callback.assert_called_once_with(data="test_data")
|
||||||
|
self.assertEqual(result, "button_clicked")
|
||||||
|
|
||||||
|
def test_button_execute_disabled(self):
|
||||||
|
"""Test executing disabled button."""
|
||||||
|
button = Button("Test", self.mock_callback, enabled=False)
|
||||||
|
|
||||||
|
result = button.execute()
|
||||||
|
|
||||||
|
# Should not call callback and return None
|
||||||
|
self.mock_callback.assert_not_called()
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_button_execute_no_callback(self):
|
||||||
|
"""Test executing button without callback."""
|
||||||
|
button = Button("Test", None, enabled=True)
|
||||||
|
|
||||||
|
result = button.execute()
|
||||||
|
|
||||||
|
# Should return None
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormFieldType(unittest.TestCase):
|
||||||
|
"""Test cases for FormFieldType enum."""
|
||||||
|
|
||||||
|
def test_form_field_types(self):
|
||||||
|
"""Test that all expected form field types exist."""
|
||||||
|
expected_types = [
|
||||||
|
'TEXT', 'PASSWORD', 'CHECKBOX', 'RADIO', 'SELECT', 'TEXTAREA',
|
||||||
|
'NUMBER', 'DATE', 'TIME', 'EMAIL', 'URL', 'COLOR', 'RANGE', 'HIDDEN'
|
||||||
|
]
|
||||||
|
|
||||||
|
for type_name in expected_types:
|
||||||
|
self.assertTrue(hasattr(FormFieldType, type_name))
|
||||||
|
|
||||||
|
# Test some specific values
|
||||||
|
self.assertEqual(FormFieldType.TEXT.value, 1)
|
||||||
|
self.assertEqual(FormFieldType.PASSWORD.value, 2)
|
||||||
|
self.assertEqual(FormFieldType.HIDDEN.value, 14)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormField(unittest.TestCase):
|
||||||
|
"""Test cases for FormField class."""
|
||||||
|
|
||||||
|
def test_form_field_creation_minimal(self):
|
||||||
|
"""Test form field creation with minimal parameters."""
|
||||||
|
field = FormField("username", FormFieldType.TEXT)
|
||||||
|
|
||||||
|
self.assertEqual(field.name, "username")
|
||||||
|
self.assertEqual(field.field_type, FormFieldType.TEXT)
|
||||||
|
self.assertEqual(field.label, "username") # Default to name
|
||||||
|
self.assertIsNone(field.value)
|
||||||
|
self.assertFalse(field.required)
|
||||||
|
self.assertEqual(field.options, [])
|
||||||
|
self.assertIsNone(field.form)
|
||||||
|
|
||||||
|
def test_form_field_creation_full(self):
|
||||||
|
"""Test form field creation with all parameters."""
|
||||||
|
options = [("value1", "Label 1"), ("value2", "Label 2")]
|
||||||
|
field = FormField(
|
||||||
|
name="country",
|
||||||
|
field_type=FormFieldType.SELECT,
|
||||||
|
label="Country",
|
||||||
|
value="value1",
|
||||||
|
required=True,
|
||||||
|
options=options
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(field.name, "country")
|
||||||
|
self.assertEqual(field.field_type, FormFieldType.SELECT)
|
||||||
|
self.assertEqual(field.label, "Country")
|
||||||
|
self.assertEqual(field.value, "value1")
|
||||||
|
self.assertTrue(field.required)
|
||||||
|
self.assertEqual(field.options, options)
|
||||||
|
|
||||||
|
def test_form_field_value_property(self):
|
||||||
|
"""Test form field value getter and setter."""
|
||||||
|
field = FormField("test", FormFieldType.TEXT, value="initial")
|
||||||
|
|
||||||
|
# Test getter
|
||||||
|
self.assertEqual(field.value, "initial")
|
||||||
|
|
||||||
|
# Test setter
|
||||||
|
field.value = "new_value"
|
||||||
|
self.assertEqual(field.value, "new_value")
|
||||||
|
|
||||||
|
def test_form_field_form_property(self):
|
||||||
|
"""Test form field form getter and setter."""
|
||||||
|
field = FormField("test", FormFieldType.TEXT)
|
||||||
|
mock_form = Mock()
|
||||||
|
|
||||||
|
# Initial state
|
||||||
|
self.assertIsNone(field.form)
|
||||||
|
|
||||||
|
# Test setter
|
||||||
|
field.form = mock_form
|
||||||
|
self.assertEqual(field.form, mock_form)
|
||||||
|
|
||||||
|
def test_form_field_properties(self):
|
||||||
|
"""Test all form field property getters."""
|
||||||
|
options = [("opt1", "Option 1")]
|
||||||
|
field = FormField(
|
||||||
|
name="test_field",
|
||||||
|
field_type=FormFieldType.CHECKBOX,
|
||||||
|
label="Test Field",
|
||||||
|
value=True,
|
||||||
|
required=True,
|
||||||
|
options=options
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test all getters
|
||||||
|
self.assertEqual(field.name, "test_field")
|
||||||
|
self.assertEqual(field.field_type, FormFieldType.CHECKBOX)
|
||||||
|
self.assertEqual(field.label, "Test Field")
|
||||||
|
self.assertTrue(field.value)
|
||||||
|
self.assertTrue(field.required)
|
||||||
|
self.assertEqual(field.options, options)
|
||||||
|
|
||||||
|
|
||||||
|
class TestForm(unittest.TestCase):
|
||||||
|
"""Test cases for Form class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_callback = Mock(return_value="form_submitted")
|
||||||
|
|
||||||
|
def test_form_creation_minimal(self):
|
||||||
|
"""Test form creation with minimal parameters."""
|
||||||
|
form = Form("test_form")
|
||||||
|
|
||||||
|
self.assertEqual(form.form_id, "test_form")
|
||||||
|
self.assertIsNone(form.action)
|
||||||
|
self.assertIsNone(form._callback)
|
||||||
|
self.assertEqual(len(form._fields), 0)
|
||||||
|
|
||||||
|
def test_form_creation_full(self):
|
||||||
|
"""Test form creation with all parameters."""
|
||||||
|
form = Form(
|
||||||
|
form_id="contact_form",
|
||||||
|
action="/submit",
|
||||||
|
callback=self.mock_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(form.form_id, "contact_form")
|
||||||
|
self.assertEqual(form.action, "/submit")
|
||||||
|
self.assertEqual(form._callback, self.mock_callback)
|
||||||
|
|
||||||
|
def test_form_field_management(self):
|
||||||
|
"""Test adding and retrieving form fields."""
|
||||||
|
form = Form("test_form")
|
||||||
|
|
||||||
|
# Create fields
|
||||||
|
field1 = FormField("username", FormFieldType.TEXT, value="john")
|
||||||
|
field2 = FormField("password", FormFieldType.PASSWORD, value="secret")
|
||||||
|
field3 = FormField("email", FormFieldType.EMAIL, value="john@example.com")
|
||||||
|
|
||||||
|
# Add fields
|
||||||
|
form.add_field(field1)
|
||||||
|
form.add_field(field2)
|
||||||
|
form.add_field(field3)
|
||||||
|
|
||||||
|
# Test that fields are stored correctly
|
||||||
|
self.assertEqual(len(form._fields), 3)
|
||||||
|
|
||||||
|
# Test field retrieval
|
||||||
|
self.assertEqual(form.get_field("username"), field1)
|
||||||
|
self.assertEqual(form.get_field("password"), field2)
|
||||||
|
self.assertEqual(form.get_field("email"), field3)
|
||||||
|
self.assertIsNone(form.get_field("nonexistent"))
|
||||||
|
|
||||||
|
# Test that fields have form reference
|
||||||
|
self.assertEqual(field1.form, form)
|
||||||
|
self.assertEqual(field2.form, form)
|
||||||
|
self.assertEqual(field3.form, form)
|
||||||
|
|
||||||
|
def test_form_get_values(self):
|
||||||
|
"""Test getting form values."""
|
||||||
|
form = Form("test_form")
|
||||||
|
|
||||||
|
# Add fields with values
|
||||||
|
form.add_field(FormField("name", FormFieldType.TEXT, value="John Doe"))
|
||||||
|
form.add_field(FormField("age", FormFieldType.NUMBER, value=30))
|
||||||
|
form.add_field(FormField("subscribe", FormFieldType.CHECKBOX, value=True))
|
||||||
|
|
||||||
|
# Get values
|
||||||
|
values = form.get_values()
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"name": "John Doe",
|
||||||
|
"age": 30,
|
||||||
|
"subscribe": True
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(values, expected)
|
||||||
|
|
||||||
|
def test_form_get_values_empty(self):
|
||||||
|
"""Test getting values from empty form."""
|
||||||
|
form = Form("empty_form")
|
||||||
|
values = form.get_values()
|
||||||
|
|
||||||
|
self.assertEqual(values, {})
|
||||||
|
|
||||||
|
def test_form_execute_with_callback(self):
|
||||||
|
"""Test executing form with callback."""
|
||||||
|
form = Form("test_form", callback=self.mock_callback)
|
||||||
|
|
||||||
|
# Add some fields
|
||||||
|
form.add_field(FormField("field1", FormFieldType.TEXT, value="value1"))
|
||||||
|
form.add_field(FormField("field2", FormFieldType.TEXT, value="value2"))
|
||||||
|
|
||||||
|
result = form.execute()
|
||||||
|
|
||||||
|
# Should call callback with form_id and values
|
||||||
|
expected_values = {"field1": "value1", "field2": "value2"}
|
||||||
|
self.mock_callback.assert_called_once_with("test_form", expected_values)
|
||||||
|
self.assertEqual(result, "form_submitted")
|
||||||
|
|
||||||
|
def test_form_execute_without_callback(self):
|
||||||
|
"""Test executing form without callback."""
|
||||||
|
form = Form("test_form")
|
||||||
|
|
||||||
|
# Add a field
|
||||||
|
form.add_field(FormField("test", FormFieldType.TEXT, value="test_value"))
|
||||||
|
|
||||||
|
result = form.execute()
|
||||||
|
|
||||||
|
# Should return the form values
|
||||||
|
expected = {"test": "test_value"}
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_form_properties(self):
|
||||||
|
"""Test form property getters."""
|
||||||
|
form = Form("test_form", action="/submit")
|
||||||
|
|
||||||
|
self.assertEqual(form.form_id, "test_form")
|
||||||
|
self.assertEqual(form.action, "/submit")
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormIntegration(unittest.TestCase):
|
||||||
|
"""Integration tests for form functionality."""
|
||||||
|
|
||||||
|
def test_complete_form_workflow(self):
|
||||||
|
"""Test a complete form creation and submission workflow."""
|
||||||
|
# Create form
|
||||||
|
form = Form("registration_form", action="/register")
|
||||||
|
|
||||||
|
# Add various field types
|
||||||
|
form.add_field(FormField(
|
||||||
|
"username", FormFieldType.TEXT,
|
||||||
|
label="Username", required=True, value="testuser"
|
||||||
|
))
|
||||||
|
|
||||||
|
form.add_field(FormField(
|
||||||
|
"password", FormFieldType.PASSWORD,
|
||||||
|
label="Password", required=True, value="secret123"
|
||||||
|
))
|
||||||
|
|
||||||
|
form.add_field(FormField(
|
||||||
|
"email", FormFieldType.EMAIL,
|
||||||
|
label="Email", required=True, value="test@example.com"
|
||||||
|
))
|
||||||
|
|
||||||
|
form.add_field(FormField(
|
||||||
|
"country", FormFieldType.SELECT,
|
||||||
|
label="Country", value="US",
|
||||||
|
options=[("US", "United States"), ("CA", "Canada"), ("UK", "United Kingdom")]
|
||||||
|
))
|
||||||
|
|
||||||
|
form.add_field(FormField(
|
||||||
|
"newsletter", FormFieldType.CHECKBOX,
|
||||||
|
label="Subscribe to newsletter", value=True
|
||||||
|
))
|
||||||
|
|
||||||
|
# Test form state
|
||||||
|
self.assertEqual(len(form._fields), 5)
|
||||||
|
|
||||||
|
# Test individual field access
|
||||||
|
username_field = form.get_field("username")
|
||||||
|
self.assertEqual(username_field.value, "testuser")
|
||||||
|
self.assertTrue(username_field.required)
|
||||||
|
|
||||||
|
# Test getting all values
|
||||||
|
values = form.get_values()
|
||||||
|
expected = {
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "secret123",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"country": "US",
|
||||||
|
"newsletter": True
|
||||||
|
}
|
||||||
|
self.assertEqual(values, expected)
|
||||||
|
|
||||||
|
# Test form submission
|
||||||
|
result = form.execute()
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_form_field_modification(self):
|
||||||
|
"""Test modifying form fields after creation."""
|
||||||
|
form = Form("test_form")
|
||||||
|
|
||||||
|
# Add field
|
||||||
|
field = FormField("test", FormFieldType.TEXT, value="initial")
|
||||||
|
form.add_field(field)
|
||||||
|
|
||||||
|
# Modify field value
|
||||||
|
field.value = "modified"
|
||||||
|
|
||||||
|
# Test that form reflects the change
|
||||||
|
values = form.get_values()
|
||||||
|
self.assertEqual(values["test"], "modified")
|
||||||
|
|
||||||
|
# Test getting the modified field
|
||||||
|
retrieved_field = form.get_field("test")
|
||||||
|
self.assertEqual(retrieved_field.value, "modified")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
520
tests/test_abstract_inline.py
Normal file
520
tests/test_abstract_inline.py
Normal file
@ -0,0 +1,520 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for abstract inline elements.
|
||||||
|
|
||||||
|
Tests the Word and FormattedSpan classes that handle inline text elements
|
||||||
|
and formatting within documents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
from pyWebLayout.abstract.inline import Word, FormattedSpan
|
||||||
|
from pyWebLayout.style import Font
|
||||||
|
|
||||||
|
|
||||||
|
class TestWord(unittest.TestCase):
|
||||||
|
"""Test cases for Word class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.font = Font()
|
||||||
|
# Note: Font background is a tuple (255, 255, 255, 0) by default
|
||||||
|
# Note: Font language is set via constructor parameter (langauge - with typo)
|
||||||
|
|
||||||
|
def test_word_creation_minimal(self):
|
||||||
|
"""Test word creation with minimal parameters."""
|
||||||
|
word = Word("hello", self.font)
|
||||||
|
|
||||||
|
self.assertEqual(word.text, "hello")
|
||||||
|
self.assertEqual(word.style, self.font)
|
||||||
|
self.assertEqual(word.background, self.font.background)
|
||||||
|
self.assertIsNone(word.previous)
|
||||||
|
self.assertIsNone(word.next)
|
||||||
|
self.assertIsNone(word.hyphenated_parts)
|
||||||
|
|
||||||
|
def test_word_creation_with_previous(self):
|
||||||
|
"""Test word creation with previous word reference."""
|
||||||
|
word1 = Word("first", self.font)
|
||||||
|
word2 = Word("second", self.font, previous=word1)
|
||||||
|
|
||||||
|
self.assertEqual(word2.previous, word1)
|
||||||
|
self.assertIsNone(word1.previous)
|
||||||
|
self.assertIsNone(word1.next)
|
||||||
|
self.assertIsNone(word2.next)
|
||||||
|
|
||||||
|
def test_word_creation_with_background_override(self):
|
||||||
|
"""Test word creation with background color override."""
|
||||||
|
word = Word("test", self.font, background="yellow")
|
||||||
|
|
||||||
|
self.assertEqual(word.background, "yellow")
|
||||||
|
# Original font background should be unchanged - it's a tuple
|
||||||
|
self.assertEqual(word.style.background, (255, 255, 255, 0))
|
||||||
|
|
||||||
|
def test_word_properties(self):
|
||||||
|
"""Test word property getters."""
|
||||||
|
word1 = Word("first", self.font)
|
||||||
|
word2 = Word("second", self.font, background="blue", previous=word1)
|
||||||
|
|
||||||
|
# Test all properties
|
||||||
|
self.assertEqual(word2.text, "second")
|
||||||
|
self.assertEqual(word2.style, self.font)
|
||||||
|
self.assertEqual(word2.background, "blue")
|
||||||
|
self.assertEqual(word2.previous, word1)
|
||||||
|
self.assertIsNone(word2.next)
|
||||||
|
self.assertIsNone(word2.hyphenated_parts)
|
||||||
|
|
||||||
|
def test_add_next_word(self):
|
||||||
|
"""Test linking words with add_next method."""
|
||||||
|
word1 = Word("first", self.font)
|
||||||
|
word2 = Word("second", self.font)
|
||||||
|
word3 = Word("third", self.font)
|
||||||
|
|
||||||
|
# Link words
|
||||||
|
word1.add_next(word2)
|
||||||
|
word2.add_next(word3)
|
||||||
|
|
||||||
|
# Test forward links
|
||||||
|
self.assertEqual(word1.next, word2)
|
||||||
|
self.assertEqual(word2.next, word3)
|
||||||
|
self.assertIsNone(word3.next)
|
||||||
|
|
||||||
|
# Backward links should remain as set in constructor
|
||||||
|
self.assertIsNone(word1.previous)
|
||||||
|
self.assertIsNone(word2.previous)
|
||||||
|
self.assertIsNone(word3.previous)
|
||||||
|
|
||||||
|
def test_word_chain(self):
|
||||||
|
"""Test creating a chain of linked words."""
|
||||||
|
word1 = Word("first", self.font)
|
||||||
|
word2 = Word("second", self.font, previous=word1)
|
||||||
|
word3 = Word("third", self.font, previous=word2)
|
||||||
|
|
||||||
|
# Add forward links
|
||||||
|
word1.add_next(word2)
|
||||||
|
word2.add_next(word3)
|
||||||
|
|
||||||
|
# Test complete chain
|
||||||
|
self.assertIsNone(word1.previous)
|
||||||
|
self.assertEqual(word1.next, word2)
|
||||||
|
|
||||||
|
self.assertEqual(word2.previous, word1)
|
||||||
|
self.assertEqual(word2.next, word3)
|
||||||
|
|
||||||
|
self.assertEqual(word3.previous, word2)
|
||||||
|
self.assertIsNone(word3.next)
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_can_hyphenate_true(self, mock_pyphen):
|
||||||
|
"""Test can_hyphenate method when word can be hyphenated."""
|
||||||
|
# Mock pyphen behavior
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_dic.inserted.return_value = "hy-phen-ated"
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
word = Word("hyphenated", self.font)
|
||||||
|
result = word.can_hyphenate()
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
# Font language is set as "en_EN" by default (with typo in constructor param)
|
||||||
|
mock_pyphen.Pyphen.assert_called_once_with(lang="en_EN")
|
||||||
|
mock_dic.inserted.assert_called_once_with("hyphenated", hyphen='-')
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_can_hyphenate_false(self, mock_pyphen):
|
||||||
|
"""Test can_hyphenate method when word cannot be hyphenated."""
|
||||||
|
# Mock pyphen behavior for non-hyphenatable word
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_dic.inserted.return_value = "cat" # No hyphens added
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
word = Word("cat", self.font)
|
||||||
|
result = word.can_hyphenate()
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
|
mock_dic.inserted.assert_called_once_with("cat", hyphen='-')
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_can_hyphenate_with_language_override(self, mock_pyphen):
|
||||||
|
"""Test can_hyphenate with explicit language parameter."""
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_dic.inserted.return_value = "hy-phen"
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
word = Word("hyphen", self.font)
|
||||||
|
result = word.can_hyphenate("de_DE")
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
mock_pyphen.Pyphen.assert_called_once_with(lang="de_DE")
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_hyphenate_success(self, mock_pyphen):
|
||||||
|
"""Test successful word hyphenation."""
|
||||||
|
# Mock pyphen behavior
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_dic.inserted.return_value = "hy-phen-ation"
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
word = Word("hyphenation", self.font)
|
||||||
|
result = word.hyphenate()
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
self.assertEqual(word.hyphenated_parts, ["hy-", "phen-", "ation"])
|
||||||
|
mock_pyphen.Pyphen.assert_called_once_with(lang="en_EN")
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_hyphenate_failure(self, mock_pyphen):
|
||||||
|
"""Test word hyphenation when word cannot be hyphenated."""
|
||||||
|
# Mock pyphen behavior for non-hyphenatable word
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_dic.inserted.return_value = "cat" # No hyphens
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
word = Word("cat", self.font)
|
||||||
|
result = word.hyphenate()
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
|
self.assertIsNone(word.hyphenated_parts)
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_hyphenate_with_language_override(self, mock_pyphen):
|
||||||
|
"""Test hyphenation with explicit language parameter."""
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_dic.inserted.return_value = "Wort-teil"
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
word = Word("Wortteil", self.font)
|
||||||
|
result = word.hyphenate("de_DE")
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
self.assertEqual(word.hyphenated_parts, ["Wort-", "teil"])
|
||||||
|
mock_pyphen.Pyphen.assert_called_once_with(lang="de_DE")
|
||||||
|
|
||||||
|
def test_dehyphenate(self):
|
||||||
|
"""Test removing hyphenation from word."""
|
||||||
|
word = Word("test", self.font)
|
||||||
|
# Simulate hyphenated state
|
||||||
|
word._hyphenated_parts = ["test-", "ing"]
|
||||||
|
|
||||||
|
word.dehyphenate()
|
||||||
|
|
||||||
|
self.assertIsNone(word.hyphenated_parts)
|
||||||
|
|
||||||
|
def test_get_hyphenated_part(self):
|
||||||
|
"""Test getting specific hyphenated parts."""
|
||||||
|
word = Word("testing", self.font)
|
||||||
|
# Simulate hyphenated state
|
||||||
|
word._hyphenated_parts = ["test-", "ing"]
|
||||||
|
|
||||||
|
# Test valid indices
|
||||||
|
self.assertEqual(word.get_hyphenated_part(0), "test-")
|
||||||
|
self.assertEqual(word.get_hyphenated_part(1), "ing")
|
||||||
|
|
||||||
|
# Test invalid index
|
||||||
|
with self.assertRaises(IndexError):
|
||||||
|
word.get_hyphenated_part(2)
|
||||||
|
|
||||||
|
def test_get_hyphenated_part_not_hyphenated(self):
|
||||||
|
"""Test getting hyphenated part from non-hyphenated word."""
|
||||||
|
word = Word("test", self.font)
|
||||||
|
|
||||||
|
with self.assertRaises(IndexError) as context:
|
||||||
|
word.get_hyphenated_part(0)
|
||||||
|
|
||||||
|
self.assertIn("Word has not been hyphenated", str(context.exception))
|
||||||
|
|
||||||
|
def test_get_hyphenated_part_count(self):
|
||||||
|
"""Test getting hyphenated part count."""
|
||||||
|
word = Word("test", self.font)
|
||||||
|
|
||||||
|
# Test non-hyphenated word
|
||||||
|
self.assertEqual(word.get_hyphenated_part_count(), 0)
|
||||||
|
|
||||||
|
# Test hyphenated word
|
||||||
|
word._hyphenated_parts = ["hy-", "phen-", "ated"]
|
||||||
|
self.assertEqual(word.get_hyphenated_part_count(), 3)
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_complex_hyphenation_scenario(self, mock_pyphen):
|
||||||
|
"""Test complex hyphenation with multiple syllables."""
|
||||||
|
# Mock pyphen for a complex word
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_dic.inserted.return_value = "un-der-stand-ing"
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
word = Word("understanding", self.font)
|
||||||
|
result = word.hyphenate()
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
expected_parts = ["un-", "der-", "stand-", "ing"]
|
||||||
|
self.assertEqual(word.hyphenated_parts, expected_parts)
|
||||||
|
self.assertEqual(word.get_hyphenated_part_count(), 4)
|
||||||
|
|
||||||
|
# Test getting individual parts
|
||||||
|
for i, expected_part in enumerate(expected_parts):
|
||||||
|
self.assertEqual(word.get_hyphenated_part(i), expected_part)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormattedSpan(unittest.TestCase):
|
||||||
|
"""Test cases for FormattedSpan class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.font = Font()
|
||||||
|
# Font background is a tuple, not a string
|
||||||
|
|
||||||
|
def test_formatted_span_creation_minimal(self):
|
||||||
|
"""Test formatted span creation with minimal parameters."""
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
self.assertEqual(span.style, self.font)
|
||||||
|
self.assertEqual(span.background, self.font.background)
|
||||||
|
self.assertEqual(len(span.words), 0)
|
||||||
|
|
||||||
|
def test_formatted_span_creation_with_background(self):
|
||||||
|
"""Test formatted span creation with background override."""
|
||||||
|
span = FormattedSpan(self.font, background="yellow")
|
||||||
|
|
||||||
|
self.assertEqual(span.style, self.font)
|
||||||
|
self.assertEqual(span.background, "yellow")
|
||||||
|
self.assertNotEqual(span.background, self.font.background)
|
||||||
|
|
||||||
|
def test_formatted_span_properties(self):
|
||||||
|
"""Test formatted span property getters."""
|
||||||
|
span = FormattedSpan(self.font, background="blue")
|
||||||
|
|
||||||
|
self.assertEqual(span.style, self.font)
|
||||||
|
self.assertEqual(span.background, "blue")
|
||||||
|
self.assertIsInstance(span.words, list)
|
||||||
|
self.assertEqual(len(span.words), 0)
|
||||||
|
|
||||||
|
def test_add_single_word(self):
|
||||||
|
"""Test adding a single word to formatted span."""
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
word = span.add_word("hello")
|
||||||
|
|
||||||
|
# Test returned word
|
||||||
|
self.assertIsInstance(word, Word)
|
||||||
|
self.assertEqual(word.text, "hello")
|
||||||
|
self.assertEqual(word.style, self.font)
|
||||||
|
self.assertEqual(word.background, self.font.background)
|
||||||
|
self.assertIsNone(word.previous)
|
||||||
|
|
||||||
|
# Test span state
|
||||||
|
self.assertEqual(len(span.words), 1)
|
||||||
|
self.assertEqual(span.words[0], word)
|
||||||
|
|
||||||
|
def test_add_multiple_words(self):
|
||||||
|
"""Test adding multiple words to formatted span."""
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
word1 = span.add_word("first")
|
||||||
|
word2 = span.add_word("second")
|
||||||
|
word3 = span.add_word("third")
|
||||||
|
|
||||||
|
# Test span contains all words
|
||||||
|
self.assertEqual(len(span.words), 3)
|
||||||
|
self.assertEqual(span.words[0], word1)
|
||||||
|
self.assertEqual(span.words[1], word2)
|
||||||
|
self.assertEqual(span.words[2], word3)
|
||||||
|
|
||||||
|
# Test word linking
|
||||||
|
self.assertIsNone(word1.previous)
|
||||||
|
self.assertEqual(word1.next, word2)
|
||||||
|
|
||||||
|
self.assertEqual(word2.previous, word1)
|
||||||
|
self.assertEqual(word2.next, word3)
|
||||||
|
|
||||||
|
self.assertEqual(word3.previous, word2)
|
||||||
|
self.assertIsNone(word3.next)
|
||||||
|
|
||||||
|
def test_add_word_with_background_override(self):
|
||||||
|
"""Test that added words inherit span background."""
|
||||||
|
span = FormattedSpan(self.font, background="red")
|
||||||
|
|
||||||
|
word = span.add_word("test")
|
||||||
|
|
||||||
|
# Word should inherit span's background
|
||||||
|
self.assertEqual(word.background, "red")
|
||||||
|
self.assertEqual(word.style, self.font)
|
||||||
|
|
||||||
|
def test_word_style_consistency(self):
|
||||||
|
"""Test that all words in span have consistent style."""
|
||||||
|
span = FormattedSpan(self.font, background="green")
|
||||||
|
|
||||||
|
words = []
|
||||||
|
for text in ["this", "is", "a", "test"]:
|
||||||
|
words.append(span.add_word(text))
|
||||||
|
|
||||||
|
# All words should have same style and background
|
||||||
|
for word in words:
|
||||||
|
self.assertEqual(word.style, self.font)
|
||||||
|
self.assertEqual(word.background, "green")
|
||||||
|
|
||||||
|
def test_word_chain_integrity(self):
|
||||||
|
"""Test that word chain is properly maintained."""
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
words = []
|
||||||
|
for i in range(5):
|
||||||
|
words.append(span.add_word(f"word{i}"))
|
||||||
|
|
||||||
|
# Test complete chain
|
||||||
|
for i in range(5):
|
||||||
|
word = words[i]
|
||||||
|
|
||||||
|
# Test previous link
|
||||||
|
if i == 0:
|
||||||
|
self.assertIsNone(word.previous)
|
||||||
|
else:
|
||||||
|
self.assertEqual(word.previous, words[i-1])
|
||||||
|
|
||||||
|
# Test next link
|
||||||
|
if i == 4:
|
||||||
|
self.assertIsNone(word.next)
|
||||||
|
else:
|
||||||
|
self.assertEqual(word.next, words[i+1])
|
||||||
|
|
||||||
|
def test_empty_span_operations(self):
|
||||||
|
"""Test operations on empty formatted span."""
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
# Test empty state
|
||||||
|
self.assertEqual(len(span.words), 0)
|
||||||
|
self.assertEqual(span.words, [])
|
||||||
|
|
||||||
|
# Add first word
|
||||||
|
first_word = span.add_word("first")
|
||||||
|
self.assertIsNone(first_word.previous)
|
||||||
|
self.assertIsNone(first_word.next)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWordFormattedSpanIntegration(unittest.TestCase):
|
||||||
|
"""Integration tests between Word and FormattedSpan classes."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.font = Font()
|
||||||
|
# Font background is a tuple, not a string
|
||||||
|
|
||||||
|
def test_sentence_creation(self):
|
||||||
|
"""Test creating a complete sentence with formatted span."""
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
sentence_words = ["The", "quick", "brown", "fox", "jumps"]
|
||||||
|
words = []
|
||||||
|
|
||||||
|
for word_text in sentence_words:
|
||||||
|
words.append(span.add_word(word_text))
|
||||||
|
|
||||||
|
# Test sentence structure
|
||||||
|
self.assertEqual(len(span.words), 5)
|
||||||
|
|
||||||
|
# Test word content
|
||||||
|
for i, expected_text in enumerate(sentence_words):
|
||||||
|
self.assertEqual(words[i].text, expected_text)
|
||||||
|
|
||||||
|
# Test linking
|
||||||
|
for i in range(5):
|
||||||
|
if i > 0:
|
||||||
|
self.assertEqual(words[i].previous, words[i-1])
|
||||||
|
if i < 4:
|
||||||
|
self.assertEqual(words[i].next, words[i+1])
|
||||||
|
|
||||||
|
@patch('pyWebLayout.abstract.inline.pyphen')
|
||||||
|
def test_span_with_hyphenated_words(self, mock_pyphen):
|
||||||
|
"""Test formatted span containing hyphenated words."""
|
||||||
|
# Mock pyphen
|
||||||
|
mock_dic = Mock()
|
||||||
|
mock_pyphen.Pyphen.return_value = mock_dic
|
||||||
|
|
||||||
|
def mock_inserted(word, hyphen='-'):
|
||||||
|
if word == "understanding":
|
||||||
|
return "un-der-stand-ing"
|
||||||
|
elif word == "hyphenation":
|
||||||
|
return "hy-phen-ation"
|
||||||
|
else:
|
||||||
|
return word # No hyphenation
|
||||||
|
|
||||||
|
mock_dic.inserted.side_effect = mock_inserted
|
||||||
|
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
# Add words, some of which can be hyphenated
|
||||||
|
word1 = span.add_word("The")
|
||||||
|
word2 = span.add_word("understanding")
|
||||||
|
word3 = span.add_word("of")
|
||||||
|
word4 = span.add_word("hyphenation")
|
||||||
|
|
||||||
|
# Test hyphenation
|
||||||
|
self.assertTrue(word2.can_hyphenate())
|
||||||
|
self.assertTrue(word2.hyphenate())
|
||||||
|
self.assertFalse(word1.can_hyphenate())
|
||||||
|
self.assertTrue(word4.can_hyphenate())
|
||||||
|
self.assertTrue(word4.hyphenate())
|
||||||
|
|
||||||
|
# Test hyphenated parts
|
||||||
|
self.assertEqual(word2.hyphenated_parts, ["un-", "der-", "stand-", "ing"])
|
||||||
|
self.assertEqual(word4.hyphenated_parts, ["hy-", "phen-", "ation"])
|
||||||
|
self.assertIsNone(word1.hyphenated_parts)
|
||||||
|
self.assertIsNone(word3.hyphenated_parts)
|
||||||
|
|
||||||
|
def test_multiple_spans_same_style(self):
|
||||||
|
"""Test creating multiple spans with the same style."""
|
||||||
|
font = Font()
|
||||||
|
|
||||||
|
span1 = FormattedSpan(font)
|
||||||
|
span2 = FormattedSpan(font)
|
||||||
|
|
||||||
|
# Add words to both spans
|
||||||
|
span1_words = [span1.add_word("First"), span1.add_word("span")]
|
||||||
|
span2_words = [span2.add_word("Second"), span2.add_word("span")]
|
||||||
|
|
||||||
|
# Test that spans are independent
|
||||||
|
self.assertEqual(len(span1.words), 2)
|
||||||
|
self.assertEqual(len(span2.words), 2)
|
||||||
|
|
||||||
|
# Test that words in different spans are not linked
|
||||||
|
self.assertIsNone(span1_words[1].next)
|
||||||
|
self.assertIsNone(span2_words[0].previous)
|
||||||
|
|
||||||
|
# But words within spans are linked
|
||||||
|
self.assertEqual(span1_words[0].next, span1_words[1])
|
||||||
|
self.assertEqual(span2_words[1].previous, span2_words[0])
|
||||||
|
|
||||||
|
def test_span_style_inheritance(self):
|
||||||
|
"""Test that words properly inherit span styling."""
|
||||||
|
font = Font()
|
||||||
|
# Font background is a tuple (255, 255, 255, 0)
|
||||||
|
|
||||||
|
# Test with span background override
|
||||||
|
span = FormattedSpan(font, background="lightgreen")
|
||||||
|
|
||||||
|
word1 = span.add_word("styled")
|
||||||
|
word2 = span.add_word("text")
|
||||||
|
|
||||||
|
# Both words should have span's background, not font's
|
||||||
|
self.assertEqual(word1.background, "lightgreen")
|
||||||
|
self.assertEqual(word2.background, "lightgreen")
|
||||||
|
|
||||||
|
# But they should have font's other properties
|
||||||
|
self.assertEqual(word1.style, font)
|
||||||
|
self.assertEqual(word2.style, font)
|
||||||
|
|
||||||
|
def test_word_modification_after_creation(self):
|
||||||
|
"""Test modifying words after they've been added to span."""
|
||||||
|
span = FormattedSpan(self.font)
|
||||||
|
|
||||||
|
word = span.add_word("original")
|
||||||
|
|
||||||
|
# Verify initial state
|
||||||
|
self.assertEqual(word.text, "original")
|
||||||
|
self.assertEqual(len(span.words), 1)
|
||||||
|
|
||||||
|
# Words are immutable by design (no setter for text property)
|
||||||
|
# But we can test that the reference is maintained
|
||||||
|
self.assertEqual(span.words[0], word)
|
||||||
|
self.assertEqual(span.words[0].text, "original")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
176
tests/test_create_and_add_pattern.py
Normal file
176
tests/test_create_and_add_pattern.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Demonstration of the new create_and_add_to pattern in pyWebLayout.
|
||||||
|
|
||||||
|
This script shows how the pattern enables automatic style and language inheritance
|
||||||
|
throughout the document hierarchy without copying strings - using object references instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mock the style system for this demonstration
|
||||||
|
class MockFont:
|
||||||
|
def __init__(self, family="Arial", size=12, language="en-US", background="white"):
|
||||||
|
self.family = family
|
||||||
|
self.size = size
|
||||||
|
self.language = language
|
||||||
|
self.background = background
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Font(family={self.family}, size={self.size}, lang={self.language}, bg={self.background})"
|
||||||
|
|
||||||
|
# Import the abstract classes
|
||||||
|
from pyWebLayout.abstract import (
|
||||||
|
Document, Paragraph, Heading, HeadingLevel, Quote, HList, ListStyle,
|
||||||
|
Table, TableRow, TableCell, Word, FormattedSpan
|
||||||
|
)
|
||||||
|
|
||||||
|
def demonstrate_create_and_add_pattern():
|
||||||
|
"""Demonstrate the create_and_add_to pattern with style inheritance."""
|
||||||
|
|
||||||
|
print("=== pyWebLayout create_and_add_to Pattern Demonstration ===\n")
|
||||||
|
|
||||||
|
# Create a document with a default style
|
||||||
|
document_style = MockFont(family="Georgia", size=14, language="en-US", background="white")
|
||||||
|
doc = Document("Style Inheritance Demo", default_style=document_style)
|
||||||
|
|
||||||
|
print(f"1. Document created with style: {document_style}")
|
||||||
|
print(f" Document default style: {doc.default_style}\n")
|
||||||
|
|
||||||
|
# Create a paragraph using the new pattern - it inherits the document's style
|
||||||
|
para1 = Paragraph.create_and_add_to(doc)
|
||||||
|
print(f"2. Paragraph created with inherited style: {para1.style}")
|
||||||
|
print(f" Style object ID matches document: {id(para1.style) == id(doc.default_style)}")
|
||||||
|
print(f" Number of blocks in document: {len(doc.blocks)}\n")
|
||||||
|
|
||||||
|
# Create words using the paragraph's create_word method
|
||||||
|
word1 = para1.create_word("Hello")
|
||||||
|
word2 = para1.create_word("World")
|
||||||
|
|
||||||
|
print(f"3. Words created with inherited paragraph style:")
|
||||||
|
print(f" Word 1 '{word1.text}' style: {word1.style}")
|
||||||
|
print(f" Word 2 '{word2.text}' style: {word2.style}")
|
||||||
|
print(f" Style object IDs match paragraph: {id(word1.style) == id(para1.style)}")
|
||||||
|
print(f" Word count in paragraph: {para1.word_count}\n")
|
||||||
|
|
||||||
|
# Create a quote with a different style
|
||||||
|
quote_style = MockFont(family="Times", size=13, language="en-US", background="lightgray")
|
||||||
|
quote = Quote.create_and_add_to(doc, style=quote_style)
|
||||||
|
|
||||||
|
print(f"4. Quote created with custom style: {quote.style}")
|
||||||
|
print(f" Style object ID different from document: {id(quote.style) != id(doc.default_style)}")
|
||||||
|
|
||||||
|
# Create a paragraph inside the quote - it inherits the quote's style
|
||||||
|
quote_para = Paragraph.create_and_add_to(quote)
|
||||||
|
print(f" Quote paragraph inherits quote style: {quote_para.style}")
|
||||||
|
print(f" Style object ID matches quote: {id(quote_para.style) == id(quote.style)}\n")
|
||||||
|
|
||||||
|
# Create a heading with specific styling
|
||||||
|
heading_style = MockFont(family="Arial Black", size=18, language="en-US", background="white")
|
||||||
|
heading = Heading.create_and_add_to(doc, HeadingLevel.H1, style=heading_style)
|
||||||
|
|
||||||
|
print(f"5. Heading created with custom style: {heading.style}")
|
||||||
|
|
||||||
|
# Add words to the heading
|
||||||
|
heading.create_word("Chapter")
|
||||||
|
heading.create_word("One")
|
||||||
|
|
||||||
|
print(f" Heading words inherit heading style:")
|
||||||
|
for i, word in heading.words():
|
||||||
|
print(f" - Word {i}: '{word.text}' with style: {word.style}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Create a list with inherited style
|
||||||
|
list_obj = HList.create_and_add_to(doc, ListStyle.UNORDERED)
|
||||||
|
print(f"6. List created with inherited document style: {list_obj.default_style}")
|
||||||
|
|
||||||
|
# Create list items that inherit from the list
|
||||||
|
item1 = list_obj.create_item()
|
||||||
|
item2 = list_obj.create_item()
|
||||||
|
|
||||||
|
print(f" List item 1 style: {item1.style}")
|
||||||
|
print(f" List item 2 style: {item2.style}")
|
||||||
|
print(f" Both inherit from list: {id(item1.style) == id(list_obj.default_style)}")
|
||||||
|
|
||||||
|
# Create paragraphs in list items
|
||||||
|
item1_para = item1.create_paragraph()
|
||||||
|
item2_para = item2.create_paragraph()
|
||||||
|
|
||||||
|
print(f" Item 1 paragraph style: {item1_para.style}")
|
||||||
|
print(f" Item 2 paragraph style: {item2_para.style}")
|
||||||
|
print(f" Both inherit from list item: {id(item1_para.style) == id(item1.style)}\n")
|
||||||
|
|
||||||
|
# Create a table with inherited style
|
||||||
|
table = Table.create_and_add_to(doc, "Example Table")
|
||||||
|
print(f"7. Table created with inherited document style: {table.style}")
|
||||||
|
|
||||||
|
# Create table rows and cells
|
||||||
|
header_row = table.create_row("header")
|
||||||
|
header_cell1 = header_row.create_cell(is_header=True)
|
||||||
|
header_cell2 = header_row.create_cell(is_header=True)
|
||||||
|
|
||||||
|
print(f" Header row style: {header_row.style}")
|
||||||
|
print(f" Header cell 1 style: {header_cell1.style}")
|
||||||
|
print(f" Header cell 2 style: {header_cell2.style}")
|
||||||
|
|
||||||
|
# Create paragraphs in cells
|
||||||
|
cell1_para = header_cell1.create_paragraph()
|
||||||
|
cell2_para = header_cell2.create_paragraph()
|
||||||
|
|
||||||
|
print(f" Cell 1 paragraph style: {cell1_para.style}")
|
||||||
|
print(f" Cell 2 paragraph style: {cell2_para.style}")
|
||||||
|
|
||||||
|
# Add words to cell paragraphs
|
||||||
|
cell1_para.create_word("Name")
|
||||||
|
cell2_para.create_word("Age")
|
||||||
|
|
||||||
|
print(f" All styles inherit properly through the hierarchy\n")
|
||||||
|
|
||||||
|
# Create a formatted span to show style inheritance
|
||||||
|
span = para1.create_span()
|
||||||
|
span_word = span.add_word("formatted")
|
||||||
|
|
||||||
|
print(f"8. FormattedSpan and Word inheritance:")
|
||||||
|
print(f" Span style: {span.style}")
|
||||||
|
print(f" Span word style: {span_word.style}")
|
||||||
|
print(f" Both inherit from paragraph: {id(span.style) == id(para1.style)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Demonstrate the object reference pattern vs string copying
|
||||||
|
print("9. Object Reference vs String Copying Demonstration:")
|
||||||
|
print(" - All child elements reference the SAME style object")
|
||||||
|
print(" - No string copying occurs - efficient memory usage")
|
||||||
|
print(" - Changes to parent style affect all children automatically")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Show the complete hierarchy
|
||||||
|
print("10. Document Structure Summary:")
|
||||||
|
print(f" Document blocks: {len(doc.blocks)}")
|
||||||
|
for i, block in enumerate(doc.blocks):
|
||||||
|
if hasattr(block, 'word_count'):
|
||||||
|
print(f" - Block {i}: {type(block).__name__} with {block.word_count} words")
|
||||||
|
elif hasattr(block, 'item_count'):
|
||||||
|
print(f" - Block {i}: {type(block).__name__} with {block.item_count} items")
|
||||||
|
elif hasattr(block, 'row_count'):
|
||||||
|
counts = block.row_count
|
||||||
|
print(f" - Block {i}: {type(block).__name__} with {counts['total']} total rows")
|
||||||
|
else:
|
||||||
|
print(f" - Block {i}: {type(block).__name__}")
|
||||||
|
|
||||||
|
print("\n=== Pattern Benefits ===")
|
||||||
|
print("✓ Automatic style inheritance throughout document hierarchy")
|
||||||
|
print("✓ Object references instead of string copying (memory efficient)")
|
||||||
|
print("✓ Consistent API pattern across all container/child relationships")
|
||||||
|
print("✓ Language and styling properties inherited as objects")
|
||||||
|
print("✓ Easy to use fluent interface for document building")
|
||||||
|
print("✓ Type safety with proper return types")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
demonstrate_create_and_add_pattern()
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Import error: {e}")
|
||||||
|
print("Note: This demo requires the pyWebLayout abstract classes")
|
||||||
|
print("Make sure the pyWebLayout package is in your Python path")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during demonstration: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
44
tests/test_epub_fix.py
Normal file
44
tests/test_epub_fix.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test script to verify that the EPUB reader fixes are working correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the pyWebLayout directory to the Python path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'pyWebLayout'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyWebLayout.io.readers.epub_reader import read_epub
|
||||||
|
print("Successfully imported epub_reader module")
|
||||||
|
|
||||||
|
# Test reading the EPUB file
|
||||||
|
epub_path = os.path.join('pyWebLayout', 'examples', 'pg174-images-3.epub')
|
||||||
|
|
||||||
|
if not os.path.exists(epub_path):
|
||||||
|
print(f"EPUB file not found: {epub_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Reading EPUB file: {epub_path}")
|
||||||
|
|
||||||
|
# Try to read the EPUB
|
||||||
|
book = read_epub(epub_path)
|
||||||
|
|
||||||
|
print(f"Successfully read EPUB file!")
|
||||||
|
print(f"Book title: {book.title}")
|
||||||
|
print(f"Number of chapters: {len(book.chapters)}")
|
||||||
|
|
||||||
|
# Check first chapter
|
||||||
|
if book.chapters:
|
||||||
|
first_chapter = book.chapters[0]
|
||||||
|
print(f"First chapter title: {first_chapter.title}")
|
||||||
|
print(f"First chapter has {len(first_chapter.blocks)} blocks")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Test completed successfully!")
|
||||||
Loading…
x
Reference in New Issue
Block a user