mirror of
https://gitlab.com/itsulu-odoo/itsulu-blog-publisher.git
synced 2026-05-30 23:41:23 +00:00
Created comprehensive E2E test suite for ITSulu Blog Publisher using Playwright and Runboat. Includes: PHASE3_ROADMAP.md: - Goals for E2E coverage (10-30 scenarios) - Performance benchmark targets (latency, tokens, queries, throughput) - Implementation plan with layer-by-layer breakdown - Success criteria and SLO targets - Runboat integration details for CI/CD e2e/ directory structure: - conftest.py: Runboat polling, auth fixtures, page fixture - requirements.txt: pytest, playwright, requests - test_generation.py: On-demand generation workflows (5 tests) - test_scheduling.py: Schedule slot configuration and execution (6 tests) - test_error_recovery.py: Error handling and email notifications (8 tests) Total: 19 E2E test scenarios covering: - On-demand post generation with auto-publish - Scheduled generation with topic queue - Error recovery and retry mechanism - Email notifications with correct content - Social media copy generation - Concurrent post generation - Progress feedback during API calls Tests use: - Playwright sync API with 30s timeout (Odoo JS rendering) - Runboat polling with 180s timeout (instance cold-start) - Session-scoped auth to avoid repeated 30s logins - Data-test-id selectors where available, fallback to get_by_* - Proper wait_for_load_state() for async operations Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
179 lines
6.8 KiB
Python
179 lines
6.8 KiB
Python
"""
|
|
E2E tests for blog post generation workflows.
|
|
Tests the full user journey: navigate → fill form → submit → verify post created.
|
|
"""
|
|
from playwright.sync_api import expect
|
|
|
|
|
|
class TestOnDemandGeneration:
|
|
"""Tests for on-demand generation via the wizard."""
|
|
|
|
def test_user_generates_blog_post_on_demand(self, page, blog_name):
|
|
"""
|
|
User navigates to Blog Publisher, fills the generation form,
|
|
and receives a published blog post with correct metadata.
|
|
|
|
Workflow:
|
|
1. Navigate to Blog Publisher module
|
|
2. Click "Generate Now" button
|
|
3. Fill topic, select provider and model
|
|
4. Set auto-publish = True
|
|
5. Submit form
|
|
6. Verify: blog.post created, published, has SEO fields
|
|
"""
|
|
# Navigate to Blog Publisher
|
|
page.goto('/web')
|
|
page.get_by_role('link', name='Blog').click()
|
|
page.wait_for_url('**/web#*', timeout=10_000)
|
|
|
|
# Click "Generate Now" button (or navigate directly)
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Fill the generation form
|
|
page.get_by_label('Topic').fill('Kubernetes Cost Optimization Best Practices')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('LLM Model').fill('claude-sonnet-4-20250514')
|
|
|
|
# Check auto-publish
|
|
page.get_by_label('Auto-publish').check()
|
|
|
|
# Click Generate button
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Wait for success message or redirect to generated post
|
|
page.wait_for_url('**/blog/**', timeout=60_000) # 60s for LLM API call
|
|
|
|
# Verify post is published
|
|
expect(page.locator('body')).to_contain_text('Kubernetes Cost Optimization')
|
|
|
|
# Verify SEO fields are visible in breadcrumb or heading
|
|
expect(page.locator('h1')).to_contain_text('Kubernetes')
|
|
|
|
def test_user_saves_post_as_draft_for_review(self, page):
|
|
"""
|
|
User generates a post but saves it as draft (unpublished) for review.
|
|
|
|
Workflow:
|
|
1. Fill generation form with auto-publish = False
|
|
2. Submit
|
|
3. Verify: blog.post created, NOT published (draft state)
|
|
4. Verify: editor can manually edit and publish later
|
|
"""
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Fill form
|
|
page.get_by_label('Topic').fill('Cloud Security Incidents 2026')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('LLM Model').fill('claude-sonnet-4-20250514')
|
|
|
|
# DO NOT check auto-publish (leave as draft)
|
|
page.get_by_label('Auto-publish').uncheck()
|
|
|
|
# Submit
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Wait for form to close and log entry to appear
|
|
page.wait_for_url('**/blog/log/**', timeout=60_000)
|
|
|
|
# Verify we're on the log page (not the published post)
|
|
expect(page.locator('body')).to_contain_text('Generation Log')
|
|
|
|
# Verify draft post exists in blog list (unpublished)
|
|
page.goto('/web#model=blog.post&view_type=list')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Find the draft post (should have "Draft" label)
|
|
expect(page.locator('body')).to_contain_text('Cloud Security')
|
|
|
|
def test_generation_shows_error_when_api_key_missing(self, page):
|
|
"""
|
|
User submits generation form without API key configured.
|
|
System shows error and allows user to fix settings and retry.
|
|
|
|
Workflow:
|
|
1. Fill form (API key not configured)
|
|
2. Submit
|
|
3. Verify: error message displayed
|
|
4. Verify: user guided to Settings to fix API key
|
|
"""
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Fill form with provider that has no API key
|
|
page.get_by_label('Topic').fill('Test Topic')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('LLM Model').fill('claude-sonnet-4-20250514')
|
|
|
|
# Submit (will fail if no API key)
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Should show error dialog or message
|
|
# Wait for error to appear (either on form or in popup)
|
|
expect(page.locator('body')).to_contain_text(
|
|
'error|not configured|API key', use_regex=True, timeout=10_000
|
|
)
|
|
|
|
def test_generation_shows_progress_during_api_call(self, page):
|
|
"""
|
|
User sees visual feedback while LLM is processing (loading spinner, progress bar).
|
|
|
|
Workflow:
|
|
1. Fill form and submit
|
|
2. Observe: loading spinner visible
|
|
3. Wait for LLM response
|
|
4. Verify: spinner hidden, post displayed
|
|
"""
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('AI Trends 2026')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('LLM Model').fill('claude-sonnet-4-20250514')
|
|
|
|
# Submit
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Look for loading indicator (might be disabled button, spinner, or modal)
|
|
# This test is loose because indicator type depends on Odoo UI
|
|
page.wait_for_url('**/blog/**', timeout=60_000)
|
|
|
|
# Eventually should show the post (loading done)
|
|
expect(page.locator('body')).to_contain_text('AI Trends')
|
|
|
|
|
|
class TestGenerationWithSocialCopy:
|
|
"""Tests generation that includes social media copy."""
|
|
|
|
def test_generated_post_includes_social_media_copy(self, page):
|
|
"""
|
|
Generated blog post includes social media copy for
|
|
X, BlueSky, Mastodon, and LinkedIn.
|
|
|
|
Workflow:
|
|
1. Generate post
|
|
2. Open post edit form
|
|
3. Verify: Social Media Copy tab/section exists
|
|
4. Verify: Posts for each enabled platform are populated
|
|
"""
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('Enterprise AI Security')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('Auto-publish').check()
|
|
|
|
page.get_by_role('button', name='Generate').click()
|
|
page.wait_for_url('**/blog/**', timeout=60_000)
|
|
|
|
# Open edit form for the post
|
|
page.get_by_role('link', name='Edit').click()
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Look for Social Media Copy section
|
|
page.get_by_role('tab', name='Social Media').click()
|
|
|
|
# Verify social posts are populated
|
|
expect(page.locator('[data-field="twitter_post_a"]')).not_to_have_text('')
|
|
expect(page.locator('[data-field="linkedin_post"]')).not_to_have_text('')
|