""" E2E tests for schedule slot configuration and execution. Tests the workflow of setting up daily scheduled generation. """ from playwright.sync_api import expect class TestScheduleConfiguration: """Tests for configuring and managing schedule slots.""" def test_user_configures_three_schedule_slots(self, page): """ User creates three daily schedule slots (morning, afternoon, evening) with different LLM providers and auto-publish settings. Workflow: 1. Navigate to Schedule Slots 2. For each slot: fill name, time, provider, model, auto-publish 3. Save all slots 4. Verify: all 3 slots appear in list as Active """ # Navigate to Blog Publisher > Schedule Slots page.goto('/web') page.get_by_role('link', name='Blog').click() page.wait_for_url('**/web#*', timeout=10_000) page.get_by_role('link', name='Blog Publisher').click() page.get_by_role('link', name='Schedule Slots').click() page.wait_for_load_state('networkidle') # Create morning slot page.get_by_role('button', name='New').click() page.wait_for_load_state('networkidle') page.get_by_label('Slot Name').fill('Morning Generation') page.get_by_label('Slot').select_option('morning') page.get_by_label('Trigger Time').fill('8.0') # 8:00 UTC page.get_by_label('LLM Provider').select_option('anthropic') page.get_by_label('Auto-publish').check() page.get_by_role('button', name='Save').click() page.wait_for_load_state('networkidle') # Return to list and create afternoon slot page.get_by_role('button', name='New').click() page.wait_for_load_state('networkidle') page.get_by_label('Slot Name').fill('Afternoon Generation') page.get_by_label('Slot').select_option('afternoon') page.get_by_label('Trigger Time').fill('14.0') # 2:00 PM UTC page.get_by_label('LLM Provider').select_option('openai') page.get_by_label('Auto-publish').uncheck() # Save as draft page.get_by_role('button', name='Save').click() page.wait_for_load_state('networkidle') # Create evening slot page.get_by_role('button', name='New').click() page.wait_for_load_state('networkidle') page.get_by_label('Slot Name').fill('Evening Generation') page.get_by_label('Slot').select_option('evening') page.get_by_label('Trigger Time').fill('20.0') # 8:00 PM UTC page.get_by_label('LLM Provider').select_option('gemini') page.get_by_label('Auto-publish').check() page.get_by_role('button', name='Save').click() page.wait_for_load_state('networkidle') # Navigate back to list and verify all 3 slots exist page.goto('/web#model=itsulu.blog.schedule&view_type=list') page.wait_for_load_state('networkidle') expect(page.locator('body')).to_contain_text('Morning Generation') expect(page.locator('body')).to_contain_text('Afternoon Generation') expect(page.locator('body')).to_contain_text('Evening Generation') def test_user_edits_schedule_slot_settings(self, page): """ User opens an existing schedule slot and modifies its settings (LLM provider, auto-publish flag, trigger time). Workflow: 1. Navigate to Schedule Slots 2. Click on existing slot 3. Change settings (provider, time, auto-publish) 4. Save 5. Verify changes persisted """ page.goto('/web#model=itsulu.blog.schedule&view_type=list') page.wait_for_load_state('networkidle') # Click on morning slot page.get_by_role('link', name='Morning Generation').click() page.wait_for_load_state('networkidle') # Edit trigger time page.get_by_label('Trigger Time').fill('7.5') # Change from 8.0 to 7:30 # Change provider page.get_by_label('LLM Provider').select_option('openai') # Toggle auto-publish page.get_by_label('Auto-publish').uncheck() # Save page.get_by_role('button', name='Save').click() # Verify changes by reopening page.goto('/web#model=itsulu.blog.schedule&view_type=list') page.wait_for_load_state('networkidle') page.get_by_role('link', name='Morning Generation').click() # Check that changes were saved expect(page.get_by_label('Trigger Time')).to_have_value('7.5') expect(page.get_by_label('LLM Provider')).to_have_value('openai') expect(page.get_by_label('Auto-publish')).not_to_be_checked() def test_user_disables_schedule_slot(self, page): """ User deactivates a schedule slot. Inactive slots are not executed by the scheduler. Workflow: 1. Navigate to Schedule Slots 2. Click slot 3. Uncheck Active checkbox 4. Save 5. Verify: slot shows as inactive in list """ page.goto('/web#model=itsulu.blog.schedule&view_type=list') page.wait_for_load_state('networkidle') # Click evening slot page.get_by_role('link', name='Evening Generation').click() page.wait_for_load_state('networkidle') # Uncheck Active page.get_by_label('Active').uncheck() page.get_by_role('button', name='Save').click() # Verify in list view (might show as grayed out or with [Draft] label) page.goto('/web#model=itsulu.blog.schedule&view_type=list') page.wait_for_load_state('networkidle') # Evening slot should still appear but inactive expect(page.locator('body')).to_contain_text('Evening Generation') class TestScheduleExecution: """Tests for schedule slot execution (via manual trigger, not real cron).""" def test_user_manually_triggers_schedule_slot(self, page): """ User clicks a "Run Now" button on schedule slot to trigger generation immediately (for testing, not waiting for cron). Workflow: 1. Navigate to schedule slot 2. Click "Run Generation Now" button 3. Verify: blog post created, logged, email sent """ page.goto('/web#model=itsulu.blog.schedule&view_type=list') page.wait_for_load_state('networkidle') # Click morning slot page.get_by_role('link', name='Morning Generation').click() page.wait_for_load_state('networkidle') # Click Run Generation Now button page.get_by_role('button', name='Run Generation Now').click() # Wait for generation to complete page.wait_for_url('**/blog/**', timeout=60_000) # Verify success message or redirect to created post expect(page.locator('body')).to_contain_text('Blog') def test_schedule_slot_falls_back_to_llm_chosen_topic_when_queue_empty(self, page): """ When topic queue is empty, schedule slot asks LLM to choose a topic. LLM returns a relevant topic and post is generated. Workflow: 1. Verify topic queue is empty 2. Trigger schedule slot 3. Verify: blog post created with LLM-chosen title """ # First, clear the topic queue page.goto('/web#model=itsulu.blog.topic&view_type=list&domain=[[\"state\",\"=\",\"pending\"]]') page.wait_for_load_state('networkidle') # Delete all pending topics (or change state to Used) # For this test, assume queue is already empty expect(page.locator('body')).to_contain_text('No records') # Now trigger schedule slot page.goto('/web#model=itsulu.blog.schedule&view_type=form') page.wait_for_load_state('networkidle') page.get_by_role('button', name='Run Generation Now').click() page.wait_for_url('**/blog/**', timeout=60_000) # Post should be created with LLM-chosen topic expect(page.locator('h1')).not_to_have_text('') # Has some title def test_schedule_slot_consumes_topics_from_queue(self, page): """ When topic queue has pending topics, schedule slot uses them in priority order. Topics are marked as Used after generation. Workflow: 1. Create 3 pending topics with priorities 2. Trigger schedule slot 3. Verify: highest priority topic consumed 4. Verify: topic state changed to Used """ # Navigate to topic queue page.goto('/web#model=itsulu.blog.topic&view_type=list') page.wait_for_load_state('networkidle') # Create urgent topic page.get_by_role('button', name='New').click() page.wait_for_load_state('networkidle') page.get_by_label('Topic Name').fill('Critical: Zero-Day Vulnerability Response') page.get_by_label('Priority').select_option('urgent') page.get_by_label('State').select_option('pending') page.get_by_role('button', name='Save').click() page.wait_for_load_state('networkidle') # Now trigger schedule slot (should use the urgent topic) page.goto('/web#model=itsulu.blog.schedule&view_type=list') page.wait_for_load_state('networkidle') page.get_by_role('link', name='Morning Generation').click() page.get_by_role('button', name='Run Generation Now').click() page.wait_for_url('**/blog/**', timeout=60_000) # Verify the generated post used the urgent topic expect(page.locator('h1')).to_contain_text('Vulnerability|Response', use_regex=True) # Verify topic was marked as Used page.goto('/web#model=itsulu.blog.topic&view_type=list') page.wait_for_load_state('networkidle') # Find the used topic page.get_by_role('button', name='Filters').click() page.get_by_role('checkbox', name='State').check() page.get_by_role('option', name='Used').click() # Should appear in Used list expect(page.locator('body')).to_contain_text('Zero-Day')