testing

Standardized test page creation for Jupyter notebooks with FastHTML

create_theme_persistence_script


def create_theme_persistence_script(
    
)->Script:

Create a JavaScript script that handles theme persistence using localStorage. This script: 1. Loads saved theme on page load 2. Listens for theme changes and saves them 3. Syncs radio button state with saved theme

Theme Selector Component

A reusable theme selector for testing:


create_theme_selector


def create_theme_selector(
    custom_themes:Optional=None, # Optional list of custom theme names to include
)->functools.partial(<function ft_hx at 0x7fe0ac72df80>, 'div'): # Div containing theme selector dropdown

Create a daisyUI theme selector dropdown component.

Test App Creation

A standardized way to create test apps in Jupyter notebooks:


create_test_app


def create_test_app(
    theme:Union=<DaisyUITheme.LIGHT: 'light'>, # Default theme
    custom_css:Optional=None, # Additional CSS
    custom_js:Optional=None, # Additional JS
    custom_theme_css:Optional=None, # Custom theme CSS as string
    custom_theme_paths:Optional=None, # List of paths to custom theme CSS files
    custom_theme_names:Optional=None, # Names of custom themes to include in selector
    enable_theme_persistence:bool=True, # Enable localStorage theme persistence
    debug:bool=True, # Enable debug mode
    kwargs:VAR_KEYWORD
)->tuple: # Tuple containing (app, rt) - FastHTML app instance and route decorator

Create a standardized test app for Jupyter notebooks with daisyUI and Tailwind.

Test Page Wrapper

A wrapper for creating consistent test pages:


create_test_page


def create_test_page(
    title:str, # Page title
    content:VAR_POSITIONAL, # Page content elements
    include_theme_selector:bool=True, # Include theme selector
    use_container:bool=True, # Wrap in container
    custom_theme_names:Optional=None, # Custom themes for selector
)->functools.partial(<function ft_hx at 0x7fe0ac72df80>, 'div'): # Div containing complete page layout with navbar and content

Create a standardized test page layout with optional theme selector.

Jupyter Notebook Utilities

Helper functions for working with FastHTML in Jupyter:


start_test_server


def start_test_server(
    app:FastHTML, # FastHTML app instance created by create_test_app or fast_app
    port:int=8000, # Port
)->JupyUvi: # JupyUvi server instance for Jupyter notebook testing

Start a test server and return the JupyUvi instance.

Usage: server = start_test_server(app) HTMX() # Display the app

# Later, in another cell:
server.stop()

Loading Custom Themes from CSS Files

You can also load custom themes from CSS files using Path objects:

# First, let's create a second custom theme and save it to a CSS file
from cjm_fasthtml_daisyui.core.themes import ThemeConfig, save_theme_css
from nbdev.config import get_config

# Get project directory
cfg = get_config()
project_dir = cfg.config_path
css_dir = project_dir / "css"
css_dir.mkdir(exist_ok=True, parents=True)

# Create a theme
custom_theme: ThemeConfig = {
    "name": "netwatch_cyberpunk",
    "default": False,
    "prefersdark": True,
    "color_scheme": "dark",
    "colors": {
        "base_100": "oklch(6.72% 0.000 0)",
        "base_200": "oklch(12% 0.005 240)",
        "base_300": "oklch(8% 0.01 235)",
        "base_content": "oklch(79.82% 0.136 184.06)",
        "primary": "oklch(61.39% 0.244 12.03)",
        "primary_content": "oklch(6.72% 0.000 0)",
        "secondary": "oklch(46.40% 0.184 10.98)",
        "secondary_content": "oklch(79.82% 0.136 184.06)",
        "accent": "oklch(25.77% 0.075 12.95)",
        "accent_content": "oklch(79.82% 0.136 184.06)",
        "neutral": "oklch(20% 0.01 240)",
        "neutral_content": "oklch(79.82% 0.136 184.06)",
        "info": "oklch(79.82% 0.136 184.06)",
        "info_content": "oklch(6.72% 0.000 0)",
        "success": "oklch(62% 0.20 140)",
        "success_content": "oklch(6.72% 0.000 0)",
        "warning": "oklch(75% 0.25 60)",
        "warning_content": "oklch(6.72% 0.000 0)",
        "error": "oklch(61.39% 0.244 12.03)",
        "error_content": "oklch(6.72% 0.000 0)"
    },
    "radius_selector": "0rem",
    "radius_field": "0rem",
    "radius_box": "0rem",
    "size_selector": "0.125rem",
    "size_field": "0.125rem",
    "border": "1px",
    "depth": 0,
    "noise": 0
}

# Save themes to CSS files
save_theme_css(custom_theme, css_dir / f"{custom_theme['name']}.css")

print(f"Created theme files in: {css_dir}")
Created theme files in: /mnt/SN850X_8TB_EXT4/Projects/GitHub/cj-mills/cjm-fasthtml-daisyui/css

Example Usage

Here’s how to use the testing utilities in a notebook:

from cjm_fasthtml_tailwind.core.base import combine_classes
from cjm_fasthtml_tailwind.utilities.backgrounds import bg
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import flex_wrap, gap, flex_display
from cjm_fasthtml_tailwind.utilities.layout import display_tw
from cjm_fasthtml_tailwind.utilities.spacing import m, space, p
from cjm_fasthtml_tailwind.utilities.typography import font_size, font_weight, font_family, text_color
from cjm_fasthtml_daisyui.utilities.semantic_colors import text_dui
from cjm_fasthtml_daisyui.components.actions.button import btn, btn_colors, btn_styles

from nbdev.config import get_config

# Get project directory
cfg = get_config()
project_dir = cfg.config_path
css_dir = project_dir / "css"
css_dir.mkdir(exist_ok=True, parents=True)

# Now create an app that loads custom themes from CSS files
custom_theme_paths=list(css_dir.glob("*.css"))

custom_theme_names=[theme.stem for theme in custom_theme_paths]
custom_theme_names.sort()

# Create a test app with default settings
app, rt = create_test_app(theme=DaisyUITheme.LIGHT,
                          custom_theme_paths=custom_theme_paths,
                          custom_theme_names=custom_theme_names,  # Add both custom themes to selector
                         )

# Define a test route
@rt
def index():
    return create_test_page(
        "Component Test Page",
        Div(
            H2("Test Components", cls=combine_classes(font_size._2xl, m.b(4))),
            P("This is a test page for daisyUI components.", cls=str(text_dui.base_content)),
            Div(
                # Button colors
                Button("Neutral", cls=combine_classes(btn, btn_colors.neutral)),                    
                Button("Primary", cls=combine_classes(btn, btn_colors.primary)),                    
                Button("Secondary", cls=combine_classes(btn, btn_colors.secondary)),                    
                Button("Accent", cls=combine_classes(btn, btn_colors.accent)),                    
                Button("Info", cls=combine_classes(btn, btn_colors.info)),                    
                Button("Success", cls=combine_classes(btn, btn_colors.success)),                    
                Button("Warning", cls=combine_classes(btn, btn_colors.warning)), 
                Button("Error", cls=combine_classes(btn, btn_colors.error)),
                # Soft buttons
                Button("Default", cls=combine_classes(btn, btn_styles.soft)),                    
                Button("Primary", cls=combine_classes(btn, btn_styles.soft, btn_colors.primary)),                        
                # Outline buttons
                Button("Default", cls=combine_classes(btn, btn_styles.outline)),                    
                Button("Primary", cls=combine_classes(btn, btn_styles.outline, btn_colors.primary)),                        
                # Dash buttons
                Button("Default", cls=combine_classes(btn, btn_styles.dash)),                    
                Button("Primary", cls=combine_classes(btn, btn_styles.dash, btn_colors.primary)),                        
                cls=combine_classes(flex_display, gap(2), flex_wrap.wrap)
            )
        ), 
        use_container=True,
        custom_theme_names=custom_theme_names
    )


# Start the server
server = start_test_server(app)
HTMX()
# Stop the server when done
server.stop()

Custom JavaScript Support

You can include custom JavaScript files and inline scripts in your test app:

# Create a test app with custom JavaScript

from cjm_fasthtml_tailwind.core.base import combine_classes
from cjm_fasthtml_tailwind.utilities.backgrounds import bg_linear
from cjm_fasthtml_tailwind.utilities.layout import display_tw
from cjm_fasthtml_tailwind.utilities.effects import shadow
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import flex, justify, items, flex_display
from cjm_fasthtml_tailwind.utilities.sizing import container
from cjm_fasthtml_tailwind.utilities.spacing import m, p
from cjm_fasthtml_tailwind.utilities.typography import font_size, font_weight, font_family, text_color
from cjm_fasthtml_daisyui.utilities.semantic_colors import bg_dui
from cjm_fasthtml_daisyui.utilities.semantic_gradients import from_dui, to_dui
from cjm_fasthtml_daisyui.components.data_display.card import card, card_title
from cjm_fasthtml_daisyui.components.actions.button import btn, btn_colors, btn_sizes
from cjm_fasthtml_daisyui.components.feedback.alert import alert, alert_colors


from cjm_fasthtml_daisyui.core.resources import create_js_script

# Create inline custom JavaScript that adds interactivity
custom_js_inline = Script("""
// Custom JavaScript to add click counter functionality
document.addEventListener('DOMContentLoaded', function() {
    let clickCount = 0;
    const counterElement = document.getElementById('click-counter');
    const resetButton = document.getElementById('reset-counter');
    
    // Add click listener to all buttons with 'count-click' class
    document.querySelectorAll('.count-click').forEach(button => {
        button.addEventListener('click', function() {
            clickCount++;
            if (counterElement) {
                counterElement.textContent = clickCount;
                
                // Add visual feedback
                counterElement.classList.add('text-primary', 'font-bold');
                setTimeout(() => {
                    counterElement.classList.remove('text-primary');
                }, 300);
            }
        });
    });
    
    // Reset counter functionality
    if (resetButton) {
        resetButton.addEventListener('click', function() {
            clickCount = 0;
            if (counterElement) {
                counterElement.textContent = clickCount;
            }
        });
    }
    
    // Log to console to verify script is loaded
    console.log('Custom JavaScript loaded successfully!');
});
""")

# Create a custom external JavaScript reference (example using a CDN library)
# In this case, we'll use confetti for visual effects
confetti_js = create_js_script(
    src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/dist/confetti.browser.min.js",
    defer=True
)

# Create app with custom JavaScript
app, rt = create_test_app(
    theme=DaisyUITheme.CUPCAKE,
    custom_js=[confetti_js, custom_js_inline],
)

# Create a test page that uses the custom JavaScript
@rt
def index():
    return create_test_page(
        "Custom JavaScript Test",
        Div(
            # Info card
            Div(
                H2("JavaScript Integration", cls=str(card_title)),
                P("This example demonstrates custom JavaScript support in test apps."),
                P("Click the buttons below to see the click counter in action!"),
                Div(
                    "Total clicks: ",
                    Span("0", id="click-counter", cls=combine_classes(font_size._2xl, font_weight.bold)),
                    cls=combine_classes(font_size.lg, m.t(4))
                ),
                cls=combine_classes(card, bg_dui.base_200, shadow.xl, p(6), m.b(4))
            ),
            
            # Interactive buttons
            Div(
                H3("Click Counter Demo", cls=combine_classes(font_size.xl, font_weight.bold, m.b(4))),
                Div(
                    Button("Click Me!", cls=combine_classes(btn, btn_colors.primary, "count-click")),
                    Button("Me Too!", cls=combine_classes(btn, btn_colors.secondary, "count-click")),
                    Button("And Me!", cls=combine_classes(btn, btn_colors.accent, "count-click")),
                    Button("Reset Counter", id="reset-counter", cls=combine_classes(btn, btn_colors.neutral)),
                    cls=combine_classes(space.x(2))
                ),
                cls=combine_classes(m.b(6))
            ),
            
            # Confetti button
            Div(
                H3("Visual Effects Demo", cls=combine_classes(font_size.xl, font_weight.bold, m.b(4))),
                P("Using an external JavaScript library (canvas-confetti):"),
                Button(
                    "πŸŽ‰ Celebrate!",
                    cls=combine_classes(btn, btn_sizes.lg, btn_colors.primary),
                    onclick="confetti({particleCount: 100, spread: 70, origin: { y: 0.6 }})"
                ),
                cls=combine_classes(m.b(6))
            ),
            
            # Console message
            Div(
                P("πŸ’‘ Open your browser's developer console to see the custom JavaScript log message."),
                cls=combine_classes(alert, alert_colors.info, m.t(4))
            )
        )
    )

# Start the server
server = start_test_server(app, port=8000)
HTMX()
# Stop the server when done
server.stop()

Export