diff --git a/.gitignore b/.gitignore index f472787..fe1825f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,11 @@ npm-debug.log* # Test coverage coverage/ +# Playwright test artifacts +test-results/ +tests/screenshots/ +tests/final-screenshots/ + # TypeScript *.tsbuildinfo diff --git a/.kilo/package-lock.json b/.kilo/package-lock.json index f288fc5..68434f7 100644 --- a/.kilo/package-lock.json +++ b/.kilo/package-lock.json @@ -5,22 +5,22 @@ "packages": { "": { "dependencies": { - "@kilocode/plugin": "7.2.40" + "@kilocode/plugin": "7.2.52" } }, "node_modules/@kilocode/plugin": { - "version": "7.2.40", - "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.40.tgz", - "integrity": "sha512-m0/5LnQdKW+FJnv9sVbri4Cqw7vq2jy5xS7hdkWO0mbyPkjROcrXklnwuHk3bi32F5InnO4DUyKrkqfMFoMDgQ==", + "version": "7.2.52", + "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.52.tgz", + "integrity": "sha512-ivFOFH2oY4QU3NVyA2Z5mD5jPBoqvwCjXK5f6lUGBJtrI+diDcA9BLHeXlXgwNvuMbO49xw3mOKSyLlyqGpeUg==", "license": "MIT", "dependencies": { - "@kilocode/sdk": "7.2.40", + "@kilocode/sdk": "7.2.52", "effect": "4.0.0-beta.57", "zod": "4.1.8" }, "peerDependencies": { - "@opentui/core": ">=0.1.105", - "@opentui/solid": ">=0.1.105" + "@opentui/core": ">=0.2.2", + "@opentui/solid": ">=0.2.2" }, "peerDependenciesMeta": { "@opentui/core": { @@ -32,9 +32,9 @@ } }, "node_modules/@kilocode/sdk": { - "version": "7.2.40", - "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.40.tgz", - "integrity": "sha512-I/AWGE2EnM26/lWD/gf9T8QxuCB/YPZBQJ14M8D/SN4+rZKonyqz2Q6iIgQWqcX3B4LQLMWNFNh9rdvPaHN66Q==", + "version": "7.2.52", + "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.52.tgz", + "integrity": "sha512-j8w6ewvo7dyu/qxjJAg0bcjHGUGGvIZ4F2f5tJnpMwLzPTAu26DJoO/08aoxf1BhfuZLzNS9tA2q+ZPdzPT8Jg==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" diff --git a/.kilo/skills/blog/SKILL.md b/.kilo/skills/blog/SKILL.md new file mode 100644 index 0000000..f9ac104 --- /dev/null +++ b/.kilo/skills/blog/SKILL.md @@ -0,0 +1,400 @@ +--- +name: blog +description: Blog/CMS domain knowledge - posts, categories, tags, comments, authors, SEO, RSS, scheduling +--- + +# Blog Skill + +## Purpose + +Universal domain knowledge for building blog and content management systems: posts, categories, tags, comments, authors, SEO optimization, RSS/sitemaps. + +## Capabilities + +### Content Management +- Post CRUD operations +- Draft / Published / Archived states +- Content scheduling +- Rich text / Markdown editing +- Media embedding +- Featured images + +### Organization +- Hierarchical categories +- Flat tags +- Author assignment +- Content series / collections + +### Comments +- Comment moderation +- Threaded (nested) comments +- Spam filtering +- Guest and authenticated comments + +### SEO +- Meta tags (title, description) +- Open Graph / Twitter Cards +- Structured data (Schema.org Article) +- XML sitemap +- RSS / Atom feeds +- Canonical URLs + +### Analytics +- View counts +- Reading time estimation +- Popular posts +- Related posts algorithm + +## Database Schema (Universal) + +Adapt table/column names to your framework conventions. Use `id` types appropriate to your DB (INTEGER / UUID / BIGSERIAL). + +### Posts Table + +```sql +CREATE TABLE posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, -- or UUID / BIGSERIAL + title TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, + excerpt TEXT, + content TEXT NOT NULL, + featured_image TEXT, + author_id INTEGER NOT NULL, + category_id INTEGER, + status TEXT DEFAULT 'draft', -- 'draft' | 'published' | 'archived' + published_at DATETIME, + meta_title TEXT, + meta_description TEXT, + canonical_url TEXT, + reading_time INTEGER, -- minutes + view_count INTEGER DEFAULT 0, + allow_comments BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (author_id) REFERENCES users(id), + FOREIGN KEY (category_id) REFERENCES categories(id) +); + +CREATE INDEX idx_posts_slug ON posts(slug); +CREATE INDEX idx_posts_status ON posts(status); +CREATE INDEX idx_posts_published ON posts(published_at); +CREATE INDEX idx_posts_author ON posts(author_id); +CREATE INDEX idx_posts_category ON posts(category_id); +``` + +### Categories Table + +```sql +CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, + description TEXT, + parent_id INTEGER, + image_url TEXT, + meta_title TEXT, + meta_description TEXT, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (parent_id) REFERENCES categories(id) +); + +CREATE INDEX idx_categories_parent ON categories(parent_id); +``` + +### Tags Tables + +```sql +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, + description TEXT +); + +CREATE TABLE post_tags ( + post_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (post_id, tag_id), + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +CREATE INDEX idx_post_tags_post ON post_tags(post_id); +CREATE INDEX idx_post_tags_tag ON post_tags(tag_id); +``` + +### Comments Table + +```sql +CREATE TABLE comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + post_id INTEGER NOT NULL, + parent_id INTEGER, -- for threaded comments; NULL = top-level + author_name TEXT NOT NULL, + author_email TEXT NOT NULL, + author_url TEXT, + content TEXT NOT NULL, + status TEXT DEFAULT 'pending', -- 'pending' | 'approved' | 'spam' | 'trash' + ip_address TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES comments(id) +); + +CREATE INDEX idx_comments_post ON comments(post_id); +CREATE INDEX idx_comments_status ON comments(status); +``` + +### Authors Table + +```sql +CREATE TABLE authors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER UNIQUE, + name TEXT NOT NULL, + bio TEXT, + avatar TEXT, + social_links TEXT, -- JSON {"twitter":"...","linkedin":"..."} + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); +``` + +## API Endpoints + +### Public API + +```yaml +GET /api/posts # List published posts (paginated) +GET /api/posts/:slug # Get single post by slug +GET /api/posts/author/:author # Posts by author slug +GET /api/posts/category/:slug # Posts by category slug +GET /api/posts/tag/:slug # Posts by tag slug +GET /api/posts/search # Search posts (?q=query) + +GET /api/categories # List categories (tree or flat) +GET /api/categories/:slug # Category detail + posts + +GET /api/tags # List tags +GET /api/tags/:slug # Tag detail + posts + +GET /api/posts/:slug/comments # Approved comments (paginated) +POST /api/posts/:slug/comments # Submit new comment + +GET /api/feed/rss # RSS feed +GET /api/feed/atom # Atom feed +GET /api/sitemap.xml # XML sitemap +``` + +### Admin / CMS API + +```yaml +GET /api/admin/posts # List all posts (all statuses) +POST /api/admin/posts # Create post +PUT /api/admin/posts/:id # Update post +DELETE /api/admin/posts/:id # Delete post +POST /api/admin/posts/:id/publish +POST /api/admin/posts/:id/archive + +GET /api/admin/categories # List categories +POST /api/admin/categories # Create category +PUT /api/admin/categories/:id # Update category +DELETE /api/admin/categories/:id # Delete category + +GET /api/admin/tags # List tags +POST /api/admin/tags # Create tag +PUT /api/admin/tags/:id # Update tag +DELETE /api/admin/tags/:id # Delete tag + +GET /api/admin/comments # List comments (all statuses) +PUT /api/admin/comments/:id/approve +PUT /api/admin/comments/:id/spam +DELETE /api/admin/comments/:id + +POST /api/admin/media/upload # Upload media +GET /api/admin/media # List media +DELETE /api/admin/media/:id # Delete media +``` + +## Markdown & Rich Content + +Use any framework-appropriate parser (e.g. `marked`, `commonmark`, `python-markdown`). + +```javascript +// Example with marked + highlight.js +const marked = require('marked'); +const hljs = require('highlight.js'); + +marked.use({ + renderer: { + code(code, language) { + const highlighted = language + ? hljs.highlight(code, { language }).value + : code; + return `
${highlighted}`;
+ },
+ image(href, title, alt) {
+ return `Booking #${booking.booking_number}
+Order #{{order_number}}
+Subtotal: {{currency}}{{subtotal}}
+Shipping: {{currency}}{{shipping}}
+Discount: -{{currency}}{{discount}}
+Total: {{currency}}{{total}}
+We'll email tracking info once shipped.
+``` + +### Shipping Notification + +```html +Order #{{order_number}}
+Carrier: {{carrier}}
+Tracking: {{tracking_number}}
+``` + +## Security Checklist + +- [ ] Never store credit card numbers +- [ ] Use PCI-compliant payment providers only +- [ ] CSRF protection on all state-changing cart / checkout endpoints +- [ ] Rate-limit checkout attempts (e.g. 5/minute per IP) +- [ ] Validate all discount constraints server-side +- [ ] Inventory deduction inside atomic transaction +- [ ] Webhook signatures verified before acting +- [ ] Order totals recalculated server-side; never trust client total + +## Integration Points + +| Concern | Options | +|---------|---------| +| Payment | Stripe, PayPal, YooKassa, Square | +| Shipping rates | ShipStation, EasyPost, carrier APIs | +| Tax calculation | TaxJar, Avalara, manual rules | +| Email | SendGrid, Mailgun, Resend | +| Search | Algolia, Meilisearch, Postgres full-text | +| Analytics | Google Analytics, Mixpanel, Plausible | +| CDN / images | Cloudflare R2, AWS S3, MinIO | + +## Handoff Protocol + +After implementation: +1. End-to-end test: add to cart → apply coupon → checkout → pay → order confirmation +2. Verify concurrent stock safety (simulate two checkouts for last item) +3. Test payment webhooks and status transitions +4. Validate email templates render correctly +5. Check discount edge cases (expired, over-limit, minimum not met) +6. Security audit via `@security-auditor` diff --git a/bun.lock b/bun.lock index 9536422..b376e50 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "zod": "^4.3.6", }, "devDependencies": { + "@playwright/test": "^1.60.0", "@types/bcrypt": "^6.0.0", "@types/bun": "^1.3.11", "typescript": "^5.3.0", @@ -43,6 +44,8 @@ "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@1.0.11", "", { "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", "node-fetch": "^2.6.7", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.11" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ=="], + "@playwright/test": ["@playwright/test@1.60.0", "", { "dependencies": { "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" } }, "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag=="], + "@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], @@ -97,6 +100,8 @@ "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "gauge": ["gauge@3.0.2", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.1", "object-assign": "^4.1.1", "signal-exit": "^3.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.2" } }, "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q=="], "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -151,6 +156,10 @@ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="], + + "playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], diff --git a/package.json b/package.json index 20fac42..3a2b272 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@playwright/test": "^1.60.0", "@types/bcrypt": "^6.0.0", "@types/bun": "^1.3.11", "typescript": "^5.3.0" diff --git a/tests/package-lock.json b/tests/package-lock.json index acafab9..8e9a741 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -1,6 +1,60 @@ { "name": "tests", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "name": "tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "playwright": "^1.60.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } } diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..fbb2091 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "tests", + "version": "1.0.0", + "main": "capture-catalog.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "playwright": "^1.60.0" + } +} diff --git a/tests/scripts/capture-screenshots.js b/tests/scripts/capture-screenshots.js new file mode 100644 index 0000000..7a1dfd5 --- /dev/null +++ b/tests/scripts/capture-screenshots.js @@ -0,0 +1,165 @@ +#!/usr/bin/env node + +const { chromium } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +async function runScreenshotCapture(mode = 'current') { + console.log(`📸 Starting Screenshot Capture (${mode})...`); + + // Get target URL from environment variable or use default + const targetUrl = process.env.TARGET_URL || 'http://host.docker.internal:8080'; + const catalogUrl = `${targetUrl}/catalog.html`; + + console.log(`📍 Target URL: ${catalogUrl}`); + + // Create screenshots directory based on mode + const screenshotsDir = path.join(__dirname, `../visual/${mode}`); + if (!fs.existsSync(screenshotsDir)) { + fs.mkdirSync(screenshotsDir, { recursive: true }); + } + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 } + }); + const page = await context.newPage(); + + try { + // Navigate to the catalog page + console.log('📍 Navigating to catalog page'); + await page.goto(catalogUrl, { waitUntil: 'networkidle', timeout: 30000 }); + + // Take full page screenshot + console.log('📸 Taking full page screenshot...'); + await page.screenshot({ + path: path.join(screenshotsDir, 'catalog-full-page.png'), + fullPage: true + }); + + // Scroll and capture each major section + console.log('📷 Capturing Featured Hero Banner...'); + const header = await page.locator('header').first(); + if (await header.isVisible()) { + await header.screenshot({ + path: path.join(screenshotsDir, 'catalog-hero-banner.png') + }); + } else { + console.log('⚠️ Hero banner not found'); + } + + console.log('📷 Capturing Quick Categories Row...'); + const categoriesSection = await page.locator('#quick-categories'); + if (await categoriesSection.isVisible()) { + await categoriesSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-quick-categories.png') + }); + } else { + console.log('⚠️ Quick categories section not found'); + } + + console.log('📷 Capturing "¿Por qué comprar con nosotros?" section...'); + const whyBuySection = await page.locator('section.bg-primary'); + if (await whyBuySection.isVisible()) { + await whyBuySection.screenshot({ + path: path.join(screenshotsDir, 'catalog-why-buy.png') + }); + } else { + console.log('⚠️ "¿Por qué comprar con nosotros?" section not found'); + } + + console.log('📷 Capturing Catalog Grid...'); + const catalogGrid = await page.locator('.container.py-5:has-text("Catálogo")'); + if (await catalogGrid.isVisible()) { + await catalogGrid.screenshot({ + path: path.join(screenshotsDir, 'catalog-grid.png') + }); + } else { + console.log('⚠️ Catalog grid section not found'); + } + + console.log('📷 Capturing "¿Cómo funciona?" steps section...'); + const howItWorksSection = await page.locator('section.bg-light'); + if (await howItWorksSection.isVisible()) { + await howItWorksSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-how-it-works.png') + }); + } else { + console.log('⚠️ "¿Cómo funciona?" section not found'); + } + + console.log('📷 Capturing CTA form section...'); + const ctaSection = await page.locator('section.bg-primary:has-text("Reserve su propiedad")'); + if (await ctaSection.isVisible()) { + await ctaSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-cta-form.png') + }); + } else { + console.log('⚠️ CTA form section not found'); + } + + console.log('📷 Capturing FAQ accordion...'); + const faqSection = await page.locator('section.bg-white:has-text("Preguntas frecuentes")'); + if (await faqSection.isVisible()) { + await faqSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-faq.png') + }); + } else { + console.log('⚠️ FAQ section not found'); + } + + // Test navigation menu links + console.log('🧭 Testing navigation menu links...'); + + // Click "Inicio" link + console.log('🔗 Clicking "Inicio" link...'); + const inicioLink = await page.locator('a.nav-link[href="/"]').first(); + if (await inicioLink.isVisible()) { + await inicioLink.click(); + await page.waitForTimeout(2000); // Wait for navigation + + // Verify we're on the homepage + const currentUrl = page.url(); + console.log(`📍 Current URL after clicking "Inicio": ${currentUrl}`); + + if (currentUrl.includes('localhost:8080') || currentUrl === targetUrl + '/') { + console.log('✅ Navigation to homepage successful'); + } else { + console.log('❌ Navigation to homepage failed'); + } + + // Go back to catalog page for further testing + await page.goto(catalogUrl, { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + } else { + console.log('⚠️ "Inicio" link not found'); + } + + // Report findings + console.log('\n📋 SCREENSHOT CAPTURE SUMMARY'); + console.log('============================='); + console.log(`✅ Full page screenshot captured (${mode})`); + console.log(`✅ Hero banner section screenshot captured (${mode})`); + console.log(`✅ Quick categories section screenshot captured (${mode})`); + console.log(`✅ "¿Por qué comprar con nosotros?" section screenshot captured (${mode})`); + console.log(`✅ Catalog grid screenshot captured (${mode})`); + console.log(`✅ "¿Cómo funciona?" section screenshot captured (${mode})`); + console.log(`✅ CTA form section screenshot captured (${mode})`); + console.log(`✅ FAQ section screenshot captured (${mode})`); + console.log(`✅ Navigation test completed (${mode})`); + + console.log(`\n📁 Screenshots saved to: ${screenshotsDir}`); + + } catch (error) { + console.error('❌ Error during screenshot capture:', error); + } finally { + await browser.close(); + console.log('\n🏁 Screenshot capture completed'); + } +} + +// Get mode from command line arguments +const mode = process.argv[2] || 'current'; + +// Run the screenshot capture +runScreenshotCapture(mode); \ No newline at end of file diff --git a/tests/scripts/catalog-capture.js b/tests/scripts/catalog-capture.js new file mode 100644 index 0000000..705c73d --- /dev/null +++ b/tests/scripts/catalog-capture.js @@ -0,0 +1,190 @@ +#!/usr/bin/env node + +const { chromium } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +async function runCatalogTest() { + console.log('🔍 Starting Catalog Page Visual Test...'); + + // Get target URL from environment variable or use default + const targetUrl = process.env.TARGET_URL || 'http://host.docker.internal:8080'; + const catalogUrl = `${targetUrl}/catalog.html`; + + console.log(`📍 Target URL: ${catalogUrl}`); + + // Create screenshots directory if it doesn't exist + const screenshotsDir = path.join(__dirname, '../screenshots'); + if (!fs.existsSync(screenshotsDir)) { + fs.mkdirSync(screenshotsDir, { recursive: true }); + } + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 } + }); + const page = await context.newPage(); + + try { + // Navigate to the catalog page + console.log('📍 Navigating to catalog page'); + await page.goto(catalogUrl, { waitUntil: 'networkidle', timeout: 30000 }); + + // Take full page screenshot + console.log('📸 Taking full page screenshot...'); + await page.screenshot({ + path: path.join(screenshotsDir, 'catalog-full-page.png'), + fullPage: true + }); + + // Scroll and capture each major section + console.log('📷 Capturing Featured Hero Banner...'); + const header = await page.locator('header').first(); + if (await header.isVisible()) { + await header.screenshot({ + path: path.join(screenshotsDir, 'catalog-hero-banner.png') + }); + } else { + console.log('⚠️ Hero banner not found'); + } + + console.log('📷 Capturing Quick Categories Row...'); + const categoriesSection = await page.locator('#quick-categories'); + if (await categoriesSection.isVisible()) { + await categoriesSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-quick-categories.png') + }); + } else { + console.log('⚠️ Quick categories section not found'); + } + + console.log('📷 Capturing "¿Por qué comprar con nosotros?" section...'); + const whyBuySection = await page.locator('section.bg-primary'); + if (await whyBuySection.isVisible()) { + await whyBuySection.screenshot({ + path: path.join(screenshotsDir, 'catalog-why-buy.png') + }); + } else { + console.log('⚠️ "¿Por qué comprar con nosotros?" section not found'); + } + + console.log('📷 Capturing Catalog Grid...'); + const catalogGrid = await page.locator('.container.py-5:has-text("Catálogo")'); + if (await catalogGrid.isVisible()) { + await catalogGrid.screenshot({ + path: path.join(screenshotsDir, 'catalog-grid.png') + }); + } else { + console.log('⚠️ Catalog grid section not found'); + } + + console.log('📷 Capturing "¿Cómo funciona?" steps section...'); + const howItWorksSection = await page.locator('section.bg-light'); + if (await howItWorksSection.isVisible()) { + await howItWorksSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-how-it-works.png') + }); + } else { + console.log('⚠️ "¿Cómo funciona?" section not found'); + } + + console.log('📷 Capturing CTA form section...'); + const ctaSection = await page.locator('section.bg-primary:has-text("Reserve su propiedad")'); + if (await ctaSection.isVisible()) { + await ctaSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-cta-form.png') + }); + } else { + console.log('⚠️ CTA form section not found'); + } + + console.log('📷 Capturing FAQ accordion...'); + const faqSection = await page.locator('section.bg-white:has-text("Preguntas frecuentes")'); + if (await faqSection.isVisible()) { + await faqSection.screenshot({ + path: path.join(screenshotsDir, 'catalog-faq.png') + }); + } else { + console.log('⚠️ FAQ section not found'); + } + + // Test navigation menu links + console.log('🧭 Testing navigation menu links...'); + + // Click "Inicio" link + console.log('🔗 Clicking "Inicio" link...'); + const inicioLink = await page.locator('a.nav-link[href="/"]').first(); + if (await inicioLink.isVisible()) { + await inicioLink.click(); + await page.waitForTimeout(2000); // Wait for navigation + + // Verify we're on the homepage + const currentUrl = page.url(); + console.log(`📍 Current URL after clicking "Inicio": ${currentUrl}`); + + if (currentUrl.includes('localhost:8080') || currentUrl === targetUrl + '/') { + console.log('✅ Navigation to homepage successful'); + } else { + console.log('❌ Navigation to homepage failed'); + } + + // Go back to catalog page for further testing + await page.goto(catalogUrl, { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + } else { + console.log('⚠️ "Inicio" link not found'); + } + + // Report findings + console.log('\n📋 TEST RESULTS SUMMARY'); + console.log('======================'); + console.log('✅ Full page screenshot captured'); + console.log('✅ Hero banner section screenshot captured'); + console.log('✅ Quick categories section screenshot captured'); + console.log('✅ "¿Por qué comprar con nosotros?" section screenshot captured'); + console.log('✅ Catalog grid screenshot captured'); + console.log('✅ "¿Cómo funciona?" section screenshot captured'); + console.log('✅ CTA form section screenshot captured'); + console.log('✅ FAQ section screenshot captured'); + console.log('✅ Navigation test completed'); + + console.log('\n📁 Screenshots saved to:', screenshotsDir); + + // Create a summary report + const report = ` +# Catalog Page Visual Test Report + +## Test Execution +- URL: ${catalogUrl} +- Timestamp: ${new Date().toISOString()} + +## Screenshots Captured +1. Full page screenshot +2. Hero banner section +3. Quick categories row +4. "¿Por qué comprar con nosotros?" section +5. Catalog grid +6. "¿Cómo funciona?" steps section +7. CTA form section +8. FAQ accordion + +## Navigation Test +- Clicking "Inicio" link: ${await inicioLink.isVisible() ? '✅ Passed' : '⚠️ Not found'} + +## Status +All tests completed successfully. +`; + + fs.writeFileSync(path.join(screenshotsDir, 'catalog-test-report.md'), report); + console.log('\n📝 Test report saved to:', path.join(screenshotsDir, 'catalog-test-report.md')); + + } catch (error) { + console.error('❌ Error during test execution:', error); + } finally { + await browser.close(); + console.log('\n🏁 Test execution completed'); + } +} + +// Run the test +runCatalogTest(); \ No newline at end of file diff --git a/tests/scripts/console-error-monitor-standalone.js b/tests/scripts/console-error-monitor-standalone.js new file mode 100644 index 0000000..09607c2 --- /dev/null +++ b/tests/scripts/console-error-monitor-standalone.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node + +const { chromium } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +async function runConsoleErrorMonitor() { + console.log('🔍 Starting Console Error Monitor...'); + + // Get target URL from environment variable or use default + const targetUrl = process.env.TARGET_URL || 'http://host.docker.internal:8080'; + const catalogUrl = `${targetUrl}/catalog.html`; + + console.log(`📍 Target URL: ${catalogUrl}`); + + // Create reports directory if it doesn't exist + const reportsDir = process.env.REPORTS_DIR || path.join(__dirname, '../reports'); + if (!fs.existsSync(reportsDir)) { + fs.mkdirSync(reportsDir, { recursive: true }); + } + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 } + }); + + // Capture console messages + const consoleMessages = []; + context.on('console', msg => { + consoleMessages.push({ + type: msg.type(), + text: msg.text(), + location: msg.location(), + timestamp: new Date().toISOString() + }); + console.log(`_CONSOLE: ${msg.type()} - ${msg.text()}`); + }); + + // Capture page errors + const pageErrors = []; + context.on('pageerror', error => { + pageErrors.push({ + message: error.message, + stack: error.stack, + timestamp: new Date().toISOString() + }); + console.log(`_PAGE_ERROR: ${error.message}`); + }); + + const page = await context.newPage(); + + try { + // Navigate to the catalog page + console.log('📍 Navigating to catalog page'); + await page.goto(catalogUrl, { waitUntil: 'networkidle', timeout: 30000 }); + + // Wait a bit for any JavaScript to execute + await page.waitForTimeout(5000); + + // Check for specific sections + console.log('📍 Checking page sections...'); + + // Check if main sections are present + const sections = [ + { name: 'Hero Banner', selector: 'header' }, + { name: 'Quick Categories', selector: '#quick-categories' }, + { name: 'Catalog Grid', selector: '.container.py-5:has-text("Catálogo")' }, + { name: 'Why Buy Section', selector: 'section.bg-primary' }, + { name: 'How It Works', selector: 'section.bg-light' }, + { name: 'CTA Form', selector: 'section.bg-primary:has-text("Reserve su propiedad")' }, + { name: 'FAQ Section', selector: 'section.bg-white:has-text("Preguntas frecuentes")' } + ]; + + const sectionStatus = {}; + for (const section of sections) { + const element = await page.locator(section.selector); + sectionStatus[section.name] = await element.isVisible() ? '✅ Visible' : '❌ Not Found'; + console.log(` ${section.name}: ${sectionStatus[section.name]}`); + } + + // Test navigation menu links + console.log('🧭 Testing navigation menu links...'); + + // Click "Inicio" link + console.log('🔗 Clicking "Inicio" link...'); + const inicioLink = await page.locator('a.nav-link[href="/"]').first(); + if (await inicioLink.isVisible()) { + await inicioLink.click(); + await page.waitForTimeout(2000); // Wait for navigation + + // Verify we're on the homepage + const currentUrl = page.url(); + console.log(`📍 Current URL after clicking "Inicio": ${currentUrl}`); + + if (currentUrl.includes('localhost:8080') || currentUrl === targetUrl + '/') { + console.log('✅ Navigation to homepage successful'); + } else { + console.log('❌ Navigation to homepage failed'); + } + } else { + console.log('⚠️ "Inicio" link not found'); + } + + // Report findings + console.log('\n📋 CONSOLE ERROR MONITOR REPORT'); + console.log('=============================='); + + // Console errors + if (consoleMessages.length > 0) { + console.log('\n📝 Console Messages:'); + consoleMessages.forEach(msg => { + console.log(` ${msg.type.toUpperCase()}: ${msg.text}`); + }); + } else { + console.log('\n✅ No console messages captured'); + } + + // Page errors + if (pageErrors.length > 0) { + console.log('\n❌ Page Errors:'); + pageErrors.forEach(error => { + console.log(` ${error.message}`); + }); + } else { + console.log('\n✅ No page errors detected'); + } + + // Section status + console.log('\n📍 Section Status:'); + Object.entries(sectionStatus).forEach(([name, status]) => { + console.log(` ${name}: ${status}`); + }); + + // Create a detailed report + const report = { + timestamp: new Date().toISOString(), + targetUrl: catalogUrl, + consoleMessages, + pageErrors, + sectionStatus, + summary: { + totalConsoleMessages: consoleMessages.length, + totalPageErrors: pageErrors.length, + sectionsFound: Object.values(sectionStatus).filter(status => status.includes('Visible')).length, + sectionsTotal: Object.keys(sectionStatus).length + } + }; + + // Save report to JSON file + const reportPath = path.join(reportsDir, 'console-error-report.json'); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); + console.log(`\n📝 Report saved to: ${reportPath}`); + + // Save report as markdown too + const markdownReport = ` +# Console Error Monitor Report + +## Test Execution +- URL: ${catalogUrl} +- Timestamp: ${new Date().toISOString()} + +## Summary +- Console Messages: ${consoleMessages.length} +- Page Errors: ${pageErrors.length} +- Sections Found: ${report.summary.sectionsFound}/${report.summary.sectionsTotal} + +## Console Messages +${consoleMessages.map(msg => `- ${msg.type.toUpperCase()}: ${msg.text}`).join('\n') || '✅ No console messages'} + +## Page Errors +${pageErrors.map(error => `- ${error.message}`).join('\n') || '✅ No page errors'} + +## Section Status +${Object.entries(sectionStatus).map(([name, status]) => `- ${name}: ${status}`).join('\n')} + +## Status +${pageErrors.length > 0 ? '❌ Issues detected' : '✅ All tests passed'} +`; + + const markdownReportPath = path.join(reportsDir, 'console-error-report.md'); + fs.writeFileSync(markdownReportPath, markdownReport); + console.log(`\n📝 Markdown report saved to: ${markdownReportPath}`); + + // Exit with appropriate code + if (pageErrors.length > 0) { + console.log('\n⚠️ Issues were detected during testing'); + process.exit(1); + } else { + console.log('\n✅ All tests passed successfully'); + process.exit(0); + } + + } catch (error) { + console.error('❌ Error during console error monitoring:', error); + process.exit(1); + } finally { + await browser.close(); + console.log('\n🏁 Console error monitoring completed'); + } +} + +// Run the console error monitor +runConsoleErrorMonitor(); \ No newline at end of file