@studiomeyer/mcp-video 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/workflows/ci.yml +34 -0
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/USAGE.md +144 -0
- package/dist/handlers/capcut.d.ts +6 -0
- package/dist/handlers/capcut.js +229 -0
- package/dist/handlers/capcut.js.map +1 -0
- package/dist/handlers/editing.d.ts +6 -0
- package/dist/handlers/editing.js +242 -0
- package/dist/handlers/editing.js.map +1 -0
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +33 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/post-production.d.ts +5 -0
- package/dist/handlers/post-production.js +109 -0
- package/dist/handlers/post-production.js.map +1 -0
- package/dist/handlers/smart-screenshot.d.ts +5 -0
- package/dist/handlers/smart-screenshot.js +83 -0
- package/dist/handlers/smart-screenshot.js.map +1 -0
- package/dist/handlers/tts.d.ts +5 -0
- package/dist/handlers/tts.js +83 -0
- package/dist/handlers/tts.js.map +1 -0
- package/dist/handlers/video.d.ts +5 -0
- package/dist/handlers/video.js +127 -0
- package/dist/handlers/video.js.map +1 -0
- package/dist/lib/dual-transport.d.ts +42 -0
- package/dist/lib/dual-transport.js +208 -0
- package/dist/lib/dual-transport.js.map +1 -0
- package/dist/lib/logger.d.ts +12 -0
- package/dist/lib/logger.js +42 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/types.js +15 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/schemas/capcut.d.ts +608 -0
- package/dist/schemas/capcut.js +411 -0
- package/dist/schemas/capcut.js.map +1 -0
- package/dist/schemas/editing.d.ts +822 -0
- package/dist/schemas/editing.js +466 -0
- package/dist/schemas/editing.js.map +1 -0
- package/dist/schemas/index.d.ts +2366 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/post-production.d.ts +379 -0
- package/dist/schemas/post-production.js +268 -0
- package/dist/schemas/post-production.js.map +1 -0
- package/dist/schemas/smart-screenshot.d.ts +127 -0
- package/dist/schemas/smart-screenshot.js +122 -0
- package/dist/schemas/smart-screenshot.js.map +1 -0
- package/dist/schemas/tts.d.ts +220 -0
- package/dist/schemas/tts.js +194 -0
- package/dist/schemas/tts.js.map +1 -0
- package/dist/schemas/video.d.ts +236 -0
- package/dist/schemas/video.js +210 -0
- package/dist/schemas/video.js.map +1 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +239 -0
- package/dist/server.js.map +1 -0
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +87 -0
- package/dist/server.test.js.map +1 -0
- package/dist/tools/engine/audio-mixer.d.ts +40 -0
- package/dist/tools/engine/audio-mixer.js +169 -0
- package/dist/tools/engine/audio-mixer.js.map +1 -0
- package/dist/tools/engine/audio.d.ts +22 -0
- package/dist/tools/engine/audio.js +73 -0
- package/dist/tools/engine/audio.js.map +1 -0
- package/dist/tools/engine/beat-sync.d.ts +31 -0
- package/dist/tools/engine/beat-sync.js +270 -0
- package/dist/tools/engine/beat-sync.js.map +1 -0
- package/dist/tools/engine/capture.d.ts +12 -0
- package/dist/tools/engine/capture.js +290 -0
- package/dist/tools/engine/capture.js.map +1 -0
- package/dist/tools/engine/chroma-key.d.ts +27 -0
- package/dist/tools/engine/chroma-key.js +154 -0
- package/dist/tools/engine/chroma-key.js.map +1 -0
- package/dist/tools/engine/concat.d.ts +49 -0
- package/dist/tools/engine/concat.js +149 -0
- package/dist/tools/engine/concat.js.map +1 -0
- package/dist/tools/engine/cursor.d.ts +26 -0
- package/dist/tools/engine/cursor.js +185 -0
- package/dist/tools/engine/cursor.js.map +1 -0
- package/dist/tools/engine/easing.d.ts +15 -0
- package/dist/tools/engine/easing.js +100 -0
- package/dist/tools/engine/easing.js.map +1 -0
- package/dist/tools/engine/editing.d.ts +158 -0
- package/dist/tools/engine/editing.js +541 -0
- package/dist/tools/engine/editing.js.map +1 -0
- package/dist/tools/engine/encoder.d.ts +31 -0
- package/dist/tools/engine/encoder.js +154 -0
- package/dist/tools/engine/encoder.js.map +1 -0
- package/dist/tools/engine/index.d.ts +30 -0
- package/dist/tools/engine/index.js +23 -0
- package/dist/tools/engine/index.js.map +1 -0
- package/dist/tools/engine/lut-presets.d.ts +25 -0
- package/dist/tools/engine/lut-presets.js +141 -0
- package/dist/tools/engine/lut-presets.js.map +1 -0
- package/dist/tools/engine/narrated-video.d.ts +63 -0
- package/dist/tools/engine/narrated-video.js +163 -0
- package/dist/tools/engine/narrated-video.js.map +1 -0
- package/dist/tools/engine/scenes.d.ts +17 -0
- package/dist/tools/engine/scenes.js +223 -0
- package/dist/tools/engine/scenes.js.map +1 -0
- package/dist/tools/engine/smart-screenshot.d.ts +80 -0
- package/dist/tools/engine/smart-screenshot.js +744 -0
- package/dist/tools/engine/smart-screenshot.js.map +1 -0
- package/dist/tools/engine/social-format.d.ts +66 -0
- package/dist/tools/engine/social-format.js +107 -0
- package/dist/tools/engine/social-format.js.map +1 -0
- package/dist/tools/engine/template-renderer.d.ts +45 -0
- package/dist/tools/engine/template-renderer.js +233 -0
- package/dist/tools/engine/template-renderer.js.map +1 -0
- package/dist/tools/engine/templates.d.ts +87 -0
- package/dist/tools/engine/templates.js +272 -0
- package/dist/tools/engine/templates.js.map +1 -0
- package/dist/tools/engine/text-animations.d.ts +33 -0
- package/dist/tools/engine/text-animations.js +192 -0
- package/dist/tools/engine/text-animations.js.map +1 -0
- package/dist/tools/engine/text-overlay.d.ts +27 -0
- package/dist/tools/engine/text-overlay.js +84 -0
- package/dist/tools/engine/text-overlay.js.map +1 -0
- package/dist/tools/engine/tts.d.ts +54 -0
- package/dist/tools/engine/tts.js +186 -0
- package/dist/tools/engine/tts.js.map +1 -0
- package/dist/tools/engine/types.d.ts +166 -0
- package/dist/tools/engine/types.js +13 -0
- package/dist/tools/engine/types.js.map +1 -0
- package/dist/tools/engine/voice-effects.d.ts +18 -0
- package/dist/tools/engine/voice-effects.js +215 -0
- package/dist/tools/engine/voice-effects.js.map +1 -0
- package/dist/tools/index.d.ts +32 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +56 -0
- package/scripts/check-deps.js +39 -0
- package/src/handlers/capcut.ts +245 -0
- package/src/handlers/editing.ts +260 -0
- package/src/handlers/index.ts +34 -0
- package/src/handlers/post-production.ts +136 -0
- package/src/handlers/smart-screenshot.ts +86 -0
- package/src/handlers/tts.ts +103 -0
- package/src/handlers/video.ts +137 -0
- package/src/lib/dual-transport.ts +272 -0
- package/src/lib/logger.ts +59 -0
- package/src/lib/types.ts +25 -0
- package/src/schemas/capcut.ts +418 -0
- package/src/schemas/editing.ts +476 -0
- package/src/schemas/index.ts +15 -0
- package/src/schemas/post-production.ts +273 -0
- package/src/schemas/smart-screenshot.ts +122 -0
- package/src/schemas/tts.ts +197 -0
- package/src/schemas/video.ts +211 -0
- package/src/server.test.ts +99 -0
- package/src/server.ts +289 -0
- package/src/tools/engine/audio-mixer.ts +244 -0
- package/src/tools/engine/audio.ts +115 -0
- package/src/tools/engine/beat-sync.ts +356 -0
- package/src/tools/engine/capture.ts +360 -0
- package/src/tools/engine/chroma-key.ts +202 -0
- package/src/tools/engine/concat.ts +242 -0
- package/src/tools/engine/cursor.ts +222 -0
- package/src/tools/engine/easing.ts +120 -0
- package/src/tools/engine/editing.ts +809 -0
- package/src/tools/engine/encoder.ts +208 -0
- package/src/tools/engine/index.ts +33 -0
- package/src/tools/engine/lut-presets.ts +235 -0
- package/src/tools/engine/narrated-video.ts +267 -0
- package/src/tools/engine/scenes.ts +309 -0
- package/src/tools/engine/smart-screenshot.ts +923 -0
- package/src/tools/engine/social-format.ts +146 -0
- package/src/tools/engine/template-renderer.ts +294 -0
- package/src/tools/engine/templates.ts +370 -0
- package/src/tools/engine/text-animations.ts +282 -0
- package/src/tools/engine/text-overlay.ts +143 -0
- package/src/tools/engine/tts.ts +284 -0
- package/src/tools/engine/types.ts +191 -0
- package/src/tools/engine/voice-effects.ts +258 -0
- package/src/tools/index.ts +67 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Screenshot Engine
|
|
3
|
+
*
|
|
4
|
+
* Element-aware screenshot system that can target specific page features.
|
|
5
|
+
* Instead of full-page screenshots, this finds and captures specific UI elements
|
|
6
|
+
* like chat widgets, booking forms, pricing sections, wizards, etc.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* smartScreenshot({ url: '...', targets: ['chat', 'pricing', 'booking'] })
|
|
10
|
+
* smartScreenshot({ url: '...', targets: [{ selector: '.hero-section' }] })
|
|
11
|
+
* smartScreenshot({ url: '...', targets: ['all'] }) // auto-detect all features
|
|
12
|
+
*/
|
|
13
|
+
import { chromium } from 'playwright';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { logger } from '../../lib/logger.js';
|
|
17
|
+
const OUTPUT_DIR = process.env.VIDEO_OUTPUT_DIR || './output';
|
|
18
|
+
const FEATURE_PATTERNS = [
|
|
19
|
+
{
|
|
20
|
+
name: 'Hero Section',
|
|
21
|
+
keywords: ['hero', 'header', 'banner', 'above-fold', 'startseite', 'landing'],
|
|
22
|
+
selectors: [
|
|
23
|
+
'[class*="hero"]', '[id*="hero"]',
|
|
24
|
+
'[class*="Hero"]', '[id*="Hero"]',
|
|
25
|
+
'section:first-of-type',
|
|
26
|
+
'main > section:first-child',
|
|
27
|
+
'main > div:first-child',
|
|
28
|
+
'[class*="banner"]:not([class*="cookie"])',
|
|
29
|
+
'[class*="landing"]',
|
|
30
|
+
'[class*="jumbotron"]',
|
|
31
|
+
],
|
|
32
|
+
textPatterns: [],
|
|
33
|
+
ariaRoles: ['banner'],
|
|
34
|
+
minSize: { width: 600, height: 300 },
|
|
35
|
+
padding: 0,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Chat Widget',
|
|
39
|
+
keywords: ['chat', 'chatbot', 'messenger', 'live-chat', 'support-chat', 'bot'],
|
|
40
|
+
selectors: [
|
|
41
|
+
'[class*="chat"]', '[id*="chat"]',
|
|
42
|
+
'[class*="Chat"]', '[id*="Chat"]',
|
|
43
|
+
'[class*="chatbot"]', '[id*="chatbot"]',
|
|
44
|
+
'[class*="messenger"]',
|
|
45
|
+
'[class*="widget"]',
|
|
46
|
+
'[class*="intercom"]',
|
|
47
|
+
'[class*="crisp"]',
|
|
48
|
+
'[class*="tawk"]',
|
|
49
|
+
'[class*="zendesk"]',
|
|
50
|
+
'[data-testid*="chat"]',
|
|
51
|
+
'iframe[src*="chat"]',
|
|
52
|
+
],
|
|
53
|
+
textPatterns: [/chat/i, /nachricht/i, /fragen/i, /hilfe/i],
|
|
54
|
+
ariaRoles: ['dialog', 'complementary'],
|
|
55
|
+
minSize: { width: 200, height: 200 },
|
|
56
|
+
padding: 10,
|
|
57
|
+
scrollTo: false,
|
|
58
|
+
waitFor: true,
|
|
59
|
+
revealSelector: '[class*="chat"] button, [class*="Chat"] button, [class*="chat-trigger"], [class*="chat-toggle"], button[aria-label*="chat" i], button[aria-label*="Chat"]',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Booking System',
|
|
63
|
+
keywords: ['booking', 'buchung', 'reservation', 'reservierung', 'buchen', 'termin', 'appointment', 'kalender', 'calendar'],
|
|
64
|
+
selectors: [
|
|
65
|
+
'[class*="booking"]', '[id*="booking"]',
|
|
66
|
+
'[class*="Booking"]', '[id*="Booking"]',
|
|
67
|
+
'[class*="reservation"]', '[id*="reservation"]',
|
|
68
|
+
'[class*="calendar"]', '[id*="calendar"]',
|
|
69
|
+
'[class*="appointment"]',
|
|
70
|
+
'[class*="scheduler"]',
|
|
71
|
+
'[class*="datepicker"]',
|
|
72
|
+
'form[action*="book"]',
|
|
73
|
+
'form[action*="reserv"]',
|
|
74
|
+
],
|
|
75
|
+
textPatterns: [/buchen/i, /reserv/i, /termin/i, /book/i, /appointment/i],
|
|
76
|
+
ariaRoles: ['form'],
|
|
77
|
+
minSize: { width: 300, height: 200 },
|
|
78
|
+
padding: 20,
|
|
79
|
+
scrollTo: true,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Pricing Section',
|
|
83
|
+
keywords: ['pricing', 'preise', 'preis', 'tarife', 'pakete', 'plans', 'packages', 'kosten', 'abo'],
|
|
84
|
+
selectors: [
|
|
85
|
+
'[class*="pricing"]', '[id*="pricing"]',
|
|
86
|
+
'[class*="Pricing"]', '[id*="Pricing"]',
|
|
87
|
+
'[class*="price"]', '[id*="price"]',
|
|
88
|
+
'[class*="plans"]', '[id*="plans"]',
|
|
89
|
+
'[class*="packages"]', '[id*="packages"]',
|
|
90
|
+
'[class*="tarif"]', '[id*="tarif"]',
|
|
91
|
+
],
|
|
92
|
+
textPatterns: [/\d+\s*[€$£]/i, /pro monat/i, /per month/i, /\/mo/i, /pricing/i, /preise/i],
|
|
93
|
+
ariaRoles: [],
|
|
94
|
+
minSize: { width: 400, height: 200 },
|
|
95
|
+
padding: 20,
|
|
96
|
+
scrollTo: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'Contact Form',
|
|
100
|
+
keywords: ['contact', 'kontakt', 'form', 'formular', 'anfrage', 'inquiry', 'nachricht', 'message'],
|
|
101
|
+
selectors: [
|
|
102
|
+
'[class*="contact"]', '[id*="contact"]',
|
|
103
|
+
'[class*="Contact"]', '[id*="Contact"]',
|
|
104
|
+
'[class*="kontakt"]', '[id*="kontakt"]',
|
|
105
|
+
'form:not([class*="search"]):not([class*="login"]):not([class*="newsletter"])',
|
|
106
|
+
'[class*="inquiry"]',
|
|
107
|
+
'[class*="anfrage"]',
|
|
108
|
+
],
|
|
109
|
+
textPatterns: [/kontakt/i, /contact/i, /nachricht senden/i, /send message/i, /anfrage/i],
|
|
110
|
+
ariaRoles: ['form'],
|
|
111
|
+
minSize: { width: 300, height: 200 },
|
|
112
|
+
padding: 20,
|
|
113
|
+
scrollTo: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'Navigation',
|
|
117
|
+
keywords: ['nav', 'navigation', 'menu', 'header-nav', 'navbar', 'menubar'],
|
|
118
|
+
selectors: [
|
|
119
|
+
'nav', 'header nav',
|
|
120
|
+
'[class*="nav"]', '[id*="nav"]',
|
|
121
|
+
'[class*="Nav"]', '[id*="Nav"]',
|
|
122
|
+
'[class*="navbar"]',
|
|
123
|
+
'[class*="menu"]:not([class*="footer"])',
|
|
124
|
+
'[role="navigation"]',
|
|
125
|
+
],
|
|
126
|
+
textPatterns: [],
|
|
127
|
+
ariaRoles: ['navigation', 'menubar'],
|
|
128
|
+
minSize: { width: 600, height: 40 },
|
|
129
|
+
padding: 5,
|
|
130
|
+
scrollTo: false,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Footer',
|
|
134
|
+
keywords: ['footer', 'fusszeile'],
|
|
135
|
+
selectors: [
|
|
136
|
+
'footer', '[class*="footer"]', '[id*="footer"]',
|
|
137
|
+
'[class*="Footer"]', '[id*="Footer"]',
|
|
138
|
+
'[role="contentinfo"]',
|
|
139
|
+
],
|
|
140
|
+
textPatterns: [],
|
|
141
|
+
ariaRoles: ['contentinfo'],
|
|
142
|
+
minSize: { width: 600, height: 100 },
|
|
143
|
+
padding: 0,
|
|
144
|
+
scrollTo: true,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Gallery / Portfolio',
|
|
148
|
+
keywords: ['gallery', 'galerie', 'portfolio', 'showcase', 'projekte', 'projects', 'work', 'arbeiten'],
|
|
149
|
+
selectors: [
|
|
150
|
+
'[class*="gallery"]', '[id*="gallery"]',
|
|
151
|
+
'[class*="Gallery"]', '[id*="Gallery"]',
|
|
152
|
+
'[class*="portfolio"]', '[id*="portfolio"]',
|
|
153
|
+
'[class*="Portfolio"]', '[id*="Portfolio"]',
|
|
154
|
+
'[class*="showcase"]',
|
|
155
|
+
'[class*="projects"]', '[id*="projects"]',
|
|
156
|
+
'[class*="grid"]:has(img)',
|
|
157
|
+
],
|
|
158
|
+
textPatterns: [/portfolio/i, /projekte/i, /projects/i, /galerie/i, /gallery/i, /unsere arbeit/i],
|
|
159
|
+
ariaRoles: [],
|
|
160
|
+
minSize: { width: 400, height: 300 },
|
|
161
|
+
padding: 20,
|
|
162
|
+
scrollTo: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'Wizard / Stepper',
|
|
166
|
+
keywords: ['wizard', 'stepper', 'steps', 'schritte', 'onboarding', 'flow', 'multi-step', 'progress'],
|
|
167
|
+
selectors: [
|
|
168
|
+
'[class*="wizard"]', '[id*="wizard"]',
|
|
169
|
+
'[class*="Wizard"]', '[id*="Wizard"]',
|
|
170
|
+
'[class*="stepper"]', '[id*="stepper"]',
|
|
171
|
+
'[class*="steps"]', '[id*="steps"]',
|
|
172
|
+
'[class*="onboarding"]', '[id*="onboarding"]',
|
|
173
|
+
'[class*="progress-bar"]',
|
|
174
|
+
'[class*="step-indicator"]',
|
|
175
|
+
'[role="progressbar"]',
|
|
176
|
+
],
|
|
177
|
+
textPatterns: [/schritt \d/i, /step \d/i],
|
|
178
|
+
ariaRoles: ['progressbar'],
|
|
179
|
+
minSize: { width: 300, height: 200 },
|
|
180
|
+
padding: 20,
|
|
181
|
+
scrollTo: true,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'Testimonials / Reviews',
|
|
185
|
+
keywords: ['testimonial', 'review', 'bewertung', 'kundenstimmen', 'feedback', 'referenzen'],
|
|
186
|
+
selectors: [
|
|
187
|
+
'[class*="testimonial"]', '[id*="testimonial"]',
|
|
188
|
+
'[class*="Testimonial"]', '[id*="Testimonial"]',
|
|
189
|
+
'[class*="review"]', '[id*="review"]',
|
|
190
|
+
'[class*="feedback"]',
|
|
191
|
+
'[class*="quote"]',
|
|
192
|
+
],
|
|
193
|
+
textPatterns: [/testimonial/i, /bewertung/i, /kundenstimm/i, /review/i],
|
|
194
|
+
ariaRoles: [],
|
|
195
|
+
minSize: { width: 300, height: 150 },
|
|
196
|
+
padding: 20,
|
|
197
|
+
scrollTo: true,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'Services / Features',
|
|
201
|
+
keywords: ['services', 'leistungen', 'features', 'funktionen', 'angebot', 'offering'],
|
|
202
|
+
selectors: [
|
|
203
|
+
'[class*="services"]', '[id*="services"]',
|
|
204
|
+
'[class*="Services"]', '[id*="Services"]',
|
|
205
|
+
'[class*="features"]', '[id*="features"]',
|
|
206
|
+
'[class*="Features"]', '[id*="Features"]',
|
|
207
|
+
'[class*="leistung"]', '[id*="leistung"]',
|
|
208
|
+
'[class*="offering"]',
|
|
209
|
+
],
|
|
210
|
+
textPatterns: [/leistungen/i, /services/i, /features/i, /was wir/i, /what we/i],
|
|
211
|
+
ariaRoles: [],
|
|
212
|
+
minSize: { width: 400, height: 200 },
|
|
213
|
+
padding: 20,
|
|
214
|
+
scrollTo: true,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'CTA / Call-to-Action',
|
|
218
|
+
keywords: ['cta', 'call-to-action', 'button', 'action', 'jetzt-starten', 'get-started'],
|
|
219
|
+
selectors: [
|
|
220
|
+
'[class*="cta"]', '[id*="cta"]',
|
|
221
|
+
'[class*="CTA"]', '[id*="CTA"]',
|
|
222
|
+
'[class*="call-to-action"]',
|
|
223
|
+
'a[class*="btn-primary"]',
|
|
224
|
+
'a[class*="button-primary"]',
|
|
225
|
+
'[class*="hero"] a[class*="btn"]',
|
|
226
|
+
'[class*="hero"] button',
|
|
227
|
+
],
|
|
228
|
+
textPatterns: [/jetzt starten/i, /get started/i, /kostenlos/i, /free trial/i, /jetzt buchen/i],
|
|
229
|
+
ariaRoles: [],
|
|
230
|
+
minSize: { width: 100, height: 30 },
|
|
231
|
+
padding: 30,
|
|
232
|
+
scrollTo: true,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'Map / Location',
|
|
236
|
+
keywords: ['map', 'karte', 'standort', 'location', 'anfahrt', 'directions'],
|
|
237
|
+
selectors: [
|
|
238
|
+
'[class*="map"]', '[id*="map"]',
|
|
239
|
+
'[class*="Map"]', '[id*="Map"]',
|
|
240
|
+
'[class*="location"]', '[id*="location"]',
|
|
241
|
+
'[class*="standort"]',
|
|
242
|
+
'iframe[src*="maps"]',
|
|
243
|
+
'iframe[src*="google.com/maps"]',
|
|
244
|
+
'.mapboxgl-map',
|
|
245
|
+
'.leaflet-container',
|
|
246
|
+
],
|
|
247
|
+
textPatterns: [/standort/i, /location/i, /anfahrt/i],
|
|
248
|
+
ariaRoles: [],
|
|
249
|
+
minSize: { width: 300, height: 200 },
|
|
250
|
+
padding: 10,
|
|
251
|
+
scrollTo: true,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'Video Section',
|
|
255
|
+
keywords: ['video', 'player', 'media'],
|
|
256
|
+
selectors: [
|
|
257
|
+
'video', '[class*="video"]', '[id*="video"]',
|
|
258
|
+
'[class*="Video"]', '[id*="Video"]',
|
|
259
|
+
'[class*="player"]',
|
|
260
|
+
'iframe[src*="youtube"]',
|
|
261
|
+
'iframe[src*="vimeo"]',
|
|
262
|
+
],
|
|
263
|
+
textPatterns: [],
|
|
264
|
+
ariaRoles: [],
|
|
265
|
+
minSize: { width: 300, height: 200 },
|
|
266
|
+
padding: 10,
|
|
267
|
+
scrollTo: true,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'Newsletter / Subscribe',
|
|
271
|
+
keywords: ['newsletter', 'subscribe', 'abonnieren', 'email-signup'],
|
|
272
|
+
selectors: [
|
|
273
|
+
'[class*="newsletter"]', '[id*="newsletter"]',
|
|
274
|
+
'[class*="Newsletter"]', '[id*="Newsletter"]',
|
|
275
|
+
'[class*="subscribe"]', '[id*="subscribe"]',
|
|
276
|
+
'form:has(input[type="email"]):not(:has(input[type="password"]))',
|
|
277
|
+
],
|
|
278
|
+
textPatterns: [/newsletter/i, /abonnieren/i, /subscribe/i],
|
|
279
|
+
ariaRoles: [],
|
|
280
|
+
minSize: { width: 200, height: 80 },
|
|
281
|
+
padding: 20,
|
|
282
|
+
scrollTo: true,
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
// ─── Main Function ────────────────────────────────────────────────────
|
|
286
|
+
export async function smartScreenshot(config) {
|
|
287
|
+
const { url, targets, outputDir = path.join(OUTPUT_DIR, 'smart-screenshots'), viewport = { width: 1920, height: 1080 }, deviceScaleFactor = 1, darkMode = false, waitAfterLoad = 2000, includeFullPage = false, maxWidth = 1920, maxHeight = 2000, } = config;
|
|
288
|
+
if (!fs.existsSync(outputDir)) {
|
|
289
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
const startTime = Date.now();
|
|
292
|
+
let browser;
|
|
293
|
+
try {
|
|
294
|
+
logger.info(`Smart Screenshot: ${url}`);
|
|
295
|
+
logger.info(`Targets: ${targets.map(t => typeof t === 'string' ? t : t.feature).join(', ')}`);
|
|
296
|
+
// ─── 1. Launch Browser ──────────────────────────────────────
|
|
297
|
+
browser = await chromium.launch({
|
|
298
|
+
headless: true,
|
|
299
|
+
args: [
|
|
300
|
+
'--no-sandbox',
|
|
301
|
+
'--disable-setuid-sandbox',
|
|
302
|
+
'--disable-dev-shm-usage',
|
|
303
|
+
'--disable-gpu',
|
|
304
|
+
'--hide-scrollbars',
|
|
305
|
+
],
|
|
306
|
+
});
|
|
307
|
+
const context = await browser.newContext({
|
|
308
|
+
viewport,
|
|
309
|
+
deviceScaleFactor,
|
|
310
|
+
colorScheme: darkMode ? 'dark' : 'light',
|
|
311
|
+
});
|
|
312
|
+
const page = await context.newPage();
|
|
313
|
+
// ─── 2. Navigate ────────────────────────────────────────────
|
|
314
|
+
logger.info(`Navigating to ${url}...`);
|
|
315
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }).catch(async () => {
|
|
316
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
317
|
+
});
|
|
318
|
+
await page.waitForTimeout(waitAfterLoad);
|
|
319
|
+
// ─── 3. Dismiss overlays ────────────────────────────────────
|
|
320
|
+
await dismissOverlays(page);
|
|
321
|
+
await page.waitForTimeout(500);
|
|
322
|
+
// ─── 4. Pre-scroll to trigger lazy loading ──────────────────
|
|
323
|
+
await preloadContent(page, viewport.height);
|
|
324
|
+
// Hide scrollbar
|
|
325
|
+
await page.addStyleTag({
|
|
326
|
+
content: `::-webkit-scrollbar { display: none !important; } * { scrollbar-width: none !important; }`,
|
|
327
|
+
});
|
|
328
|
+
// ─── 5. Detect features ─────────────────────────────────────
|
|
329
|
+
const normalizedTargets = normalizeTargets(targets);
|
|
330
|
+
const isAutoDetect = normalizedTargets.some(t => t.feature === 'all');
|
|
331
|
+
let detectedFeatures;
|
|
332
|
+
if (isAutoDetect) {
|
|
333
|
+
logger.info('Auto-detecting all page features...');
|
|
334
|
+
detectedFeatures = await detectAllFeatures(page, viewport);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
detectedFeatures = [];
|
|
338
|
+
for (const target of normalizedTargets) {
|
|
339
|
+
const features = await detectFeature(page, target, viewport);
|
|
340
|
+
detectedFeatures.push(...features);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
logger.info(`Detected ${detectedFeatures.length} features`);
|
|
344
|
+
for (const f of detectedFeatures) {
|
|
345
|
+
logger.info(` ${f.name}: ${f.bounds.width}x${f.bounds.height} (${f.matchMethod}, ${f.confidence})`);
|
|
346
|
+
}
|
|
347
|
+
// ─── 6. Take screenshots ────────────────────────────────────
|
|
348
|
+
const screenshots = [];
|
|
349
|
+
const domain = new URL(url).hostname.replace(/^www\./, '').replace(/\./g, '-');
|
|
350
|
+
for (let i = 0; i < detectedFeatures.length; i++) {
|
|
351
|
+
const feature = detectedFeatures[i];
|
|
352
|
+
const safeName = feature.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-');
|
|
353
|
+
const ssPath = path.join(outputDir, `${domain}-${safeName}-${i}.png`);
|
|
354
|
+
try {
|
|
355
|
+
// Scroll element into view
|
|
356
|
+
await page.evaluate((sel) => {
|
|
357
|
+
const el = document.querySelector(sel);
|
|
358
|
+
if (el) {
|
|
359
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
360
|
+
}
|
|
361
|
+
}, feature.selector);
|
|
362
|
+
await page.waitForTimeout(300);
|
|
363
|
+
// Get VIEWPORT-RELATIVE bounds (getBoundingClientRect without scrollY)
|
|
364
|
+
const vpBounds = await page.evaluate((sel) => {
|
|
365
|
+
const el = document.querySelector(sel);
|
|
366
|
+
if (!el)
|
|
367
|
+
return null;
|
|
368
|
+
const rect = el.getBoundingClientRect();
|
|
369
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
370
|
+
}, feature.selector);
|
|
371
|
+
if (!vpBounds || vpBounds.width < 10 || vpBounds.height < 10) {
|
|
372
|
+
logger.info(` Skipping ${feature.name}: element not found after scroll`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
// Apply padding
|
|
376
|
+
const padding = feature.pattern === 'custom' ? 20 : (findPattern(feature.pattern)?.padding ?? 20);
|
|
377
|
+
const clip = {
|
|
378
|
+
x: Math.max(0, vpBounds.x - padding),
|
|
379
|
+
y: Math.max(0, vpBounds.y - padding),
|
|
380
|
+
width: Math.min(vpBounds.width + padding * 2, maxWidth),
|
|
381
|
+
height: Math.min(vpBounds.height + padding * 2, maxHeight),
|
|
382
|
+
};
|
|
383
|
+
// Ensure clip doesn't exceed viewport dimensions
|
|
384
|
+
if (clip.x + clip.width > viewport.width) {
|
|
385
|
+
clip.width = viewport.width - clip.x;
|
|
386
|
+
}
|
|
387
|
+
if (clip.y + clip.height > viewport.height) {
|
|
388
|
+
clip.height = viewport.height - clip.y;
|
|
389
|
+
}
|
|
390
|
+
// Skip if clip is too small or invalid
|
|
391
|
+
if (clip.width < 20 || clip.height < 20) {
|
|
392
|
+
logger.info(` Skipping ${feature.name}: clipped area too small`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
await page.screenshot({
|
|
396
|
+
path: ssPath,
|
|
397
|
+
type: 'png',
|
|
398
|
+
clip,
|
|
399
|
+
});
|
|
400
|
+
screenshots.push({
|
|
401
|
+
feature: feature.name,
|
|
402
|
+
path: ssPath,
|
|
403
|
+
bounds: clip,
|
|
404
|
+
matchMethod: feature.matchMethod,
|
|
405
|
+
confidence: feature.confidence,
|
|
406
|
+
});
|
|
407
|
+
logger.info(` Screenshot: ${feature.name} → ${path.basename(ssPath)}`);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
logger.error(` Failed to screenshot ${feature.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ─── 7. Full-page screenshot (optional) ─────────────────────
|
|
414
|
+
let fullPagePath;
|
|
415
|
+
if (includeFullPage) {
|
|
416
|
+
fullPagePath = path.join(outputDir, `${domain}-full-page.png`);
|
|
417
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
418
|
+
await page.waitForTimeout(300);
|
|
419
|
+
await page.screenshot({ path: fullPagePath, fullPage: true, type: 'png' });
|
|
420
|
+
logger.info(` Full-page screenshot → ${path.basename(fullPagePath)}`);
|
|
421
|
+
}
|
|
422
|
+
// ─── 8. Cleanup ─────────────────────────────────────────────
|
|
423
|
+
await context.close();
|
|
424
|
+
await browser.close();
|
|
425
|
+
browser = undefined;
|
|
426
|
+
const totalTime = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
|
|
427
|
+
logger.info(`Smart Screenshot complete: ${screenshots.length} captures in ${totalTime}`);
|
|
428
|
+
return {
|
|
429
|
+
success: true,
|
|
430
|
+
url,
|
|
431
|
+
screenshots,
|
|
432
|
+
detected: detectedFeatures,
|
|
433
|
+
fullPage: fullPagePath,
|
|
434
|
+
totalTime,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (browser) {
|
|
439
|
+
try {
|
|
440
|
+
await browser.close();
|
|
441
|
+
}
|
|
442
|
+
catch { /* ignore */ }
|
|
443
|
+
}
|
|
444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
445
|
+
logger.error(`Smart Screenshot failed: ${message}`);
|
|
446
|
+
throw new Error(`Smart Screenshot failed: ${message}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// ─── Feature Detection ────────────────────────────────────────────────
|
|
450
|
+
function normalizeTargets(targets) {
|
|
451
|
+
return targets.map(t => {
|
|
452
|
+
if (typeof t === 'string') {
|
|
453
|
+
return { feature: t.toLowerCase().trim() };
|
|
454
|
+
}
|
|
455
|
+
return { ...t, feature: t.feature.toLowerCase().trim() };
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function findPattern(keyword) {
|
|
459
|
+
return FEATURE_PATTERNS.find(p => p.keywords.some(k => k === keyword) ||
|
|
460
|
+
p.name.toLowerCase() === keyword);
|
|
461
|
+
}
|
|
462
|
+
function findPatternsByKeyword(keyword) {
|
|
463
|
+
// Exact match first
|
|
464
|
+
const exact = FEATURE_PATTERNS.filter(p => p.keywords.some(k => k === keyword) ||
|
|
465
|
+
p.name.toLowerCase() === keyword);
|
|
466
|
+
if (exact.length > 0)
|
|
467
|
+
return exact;
|
|
468
|
+
// Fuzzy match: keyword is a substring of a pattern keyword or vice versa
|
|
469
|
+
return FEATURE_PATTERNS.filter(p => p.keywords.some(k => k.includes(keyword) || keyword.includes(k)) ||
|
|
470
|
+
p.name.toLowerCase().includes(keyword) ||
|
|
471
|
+
keyword.includes(p.name.toLowerCase()));
|
|
472
|
+
}
|
|
473
|
+
async function detectFeature(page, target, viewport) {
|
|
474
|
+
const results = [];
|
|
475
|
+
// Custom selector override
|
|
476
|
+
if (target.selector) {
|
|
477
|
+
const bounds = await getElementBounds(page, target.selector);
|
|
478
|
+
if (bounds && bounds.width > 10 && bounds.height > 10) {
|
|
479
|
+
results.push({
|
|
480
|
+
name: target.feature || 'Custom Element',
|
|
481
|
+
pattern: 'custom',
|
|
482
|
+
selector: target.selector,
|
|
483
|
+
bounds,
|
|
484
|
+
matchMethod: 'custom',
|
|
485
|
+
confidence: 'high',
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return results;
|
|
489
|
+
}
|
|
490
|
+
// Feature keyword starts with . or # or [ → treat as CSS selector
|
|
491
|
+
if (/^[.#\[]/.test(target.feature)) {
|
|
492
|
+
const bounds = await getElementBounds(page, target.feature);
|
|
493
|
+
if (bounds && bounds.width > 10 && bounds.height > 10) {
|
|
494
|
+
results.push({
|
|
495
|
+
name: target.feature,
|
|
496
|
+
pattern: 'custom',
|
|
497
|
+
selector: target.feature,
|
|
498
|
+
bounds,
|
|
499
|
+
matchMethod: 'custom',
|
|
500
|
+
confidence: 'high',
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return results;
|
|
504
|
+
}
|
|
505
|
+
// Click to reveal if needed (e.g. chat widget)
|
|
506
|
+
const patterns = findPatternsByKeyword(target.feature);
|
|
507
|
+
if (patterns.length === 0) {
|
|
508
|
+
logger.info(` No pattern found for "${target.feature}", trying as text search...`);
|
|
509
|
+
const textResults = await findByVisibleText(page, target.feature, viewport);
|
|
510
|
+
results.push(...textResults);
|
|
511
|
+
return results;
|
|
512
|
+
}
|
|
513
|
+
for (const pattern of patterns) {
|
|
514
|
+
// Try reveal selector first (e.g. click to open chat)
|
|
515
|
+
if ((target.revealFirst || pattern.revealSelector) && pattern.revealSelector) {
|
|
516
|
+
try {
|
|
517
|
+
const revealBtn = page.locator(pattern.revealSelector).first();
|
|
518
|
+
if (await revealBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
519
|
+
await revealBtn.click({ timeout: 2000 });
|
|
520
|
+
await page.waitForTimeout(1000);
|
|
521
|
+
logger.info(` Clicked reveal button for ${pattern.name}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch { /* no reveal button found, continue */ }
|
|
525
|
+
}
|
|
526
|
+
// Try CSS selectors
|
|
527
|
+
for (const selector of pattern.selectors) {
|
|
528
|
+
const bounds = await getElementBounds(page, selector);
|
|
529
|
+
if (bounds && meetsMinSize(bounds, pattern.minSize)) {
|
|
530
|
+
results.push({
|
|
531
|
+
name: pattern.name,
|
|
532
|
+
pattern: pattern.keywords[0],
|
|
533
|
+
selector,
|
|
534
|
+
bounds,
|
|
535
|
+
matchMethod: 'selector',
|
|
536
|
+
confidence: 'high',
|
|
537
|
+
});
|
|
538
|
+
break; // Found via selector, no need to check more
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// If not found by selector, try text patterns
|
|
542
|
+
if (results.filter(r => r.pattern === pattern.keywords[0]).length === 0) {
|
|
543
|
+
for (const textPattern of pattern.textPatterns) {
|
|
544
|
+
const textResults = await findSectionByText(page, textPattern, pattern.name, pattern.keywords[0], viewport);
|
|
545
|
+
results.push(...textResults);
|
|
546
|
+
if (textResults.length > 0)
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Try ARIA roles
|
|
551
|
+
if (results.filter(r => r.pattern === pattern.keywords[0]).length === 0) {
|
|
552
|
+
for (const role of pattern.ariaRoles) {
|
|
553
|
+
const ariaSelector = `[role="${role}"]`;
|
|
554
|
+
const bounds = await getElementBounds(page, ariaSelector);
|
|
555
|
+
if (bounds && meetsMinSize(bounds, pattern.minSize)) {
|
|
556
|
+
results.push({
|
|
557
|
+
name: pattern.name,
|
|
558
|
+
pattern: pattern.keywords[0],
|
|
559
|
+
selector: ariaSelector,
|
|
560
|
+
bounds,
|
|
561
|
+
matchMethod: 'aria',
|
|
562
|
+
confidence: 'medium',
|
|
563
|
+
});
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return results;
|
|
570
|
+
}
|
|
571
|
+
async function detectAllFeatures(page, viewport) {
|
|
572
|
+
const results = [];
|
|
573
|
+
const seenBounds = new Set();
|
|
574
|
+
for (const pattern of FEATURE_PATTERNS) {
|
|
575
|
+
// Try each selector
|
|
576
|
+
for (const selector of pattern.selectors) {
|
|
577
|
+
const bounds = await getElementBounds(page, selector);
|
|
578
|
+
if (bounds && meetsMinSize(bounds, pattern.minSize)) {
|
|
579
|
+
const boundsKey = `${Math.round(bounds.x)}-${Math.round(bounds.y)}-${Math.round(bounds.width)}-${Math.round(bounds.height)}`;
|
|
580
|
+
if (!seenBounds.has(boundsKey)) {
|
|
581
|
+
seenBounds.add(boundsKey);
|
|
582
|
+
results.push({
|
|
583
|
+
name: pattern.name,
|
|
584
|
+
pattern: pattern.keywords[0],
|
|
585
|
+
selector,
|
|
586
|
+
bounds,
|
|
587
|
+
matchMethod: 'selector',
|
|
588
|
+
confidence: 'high',
|
|
589
|
+
});
|
|
590
|
+
break; // One match per pattern
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return results;
|
|
596
|
+
}
|
|
597
|
+
async function findByVisibleText(page, searchText, viewport) {
|
|
598
|
+
const results = [];
|
|
599
|
+
const regex = new RegExp(searchText, 'i');
|
|
600
|
+
const found = await findSectionByText(page, regex, searchText, searchText, viewport);
|
|
601
|
+
results.push(...found);
|
|
602
|
+
return results;
|
|
603
|
+
}
|
|
604
|
+
async function findSectionByText(page, textPattern, featureName, patternKey, _viewport) {
|
|
605
|
+
const results = [];
|
|
606
|
+
// Find the nearest section/container that contains this text
|
|
607
|
+
const element = await page.evaluate((pattern) => {
|
|
608
|
+
const re = new RegExp(pattern, 'i');
|
|
609
|
+
// Search headings first, then paragraphs, then sections
|
|
610
|
+
const candidates = [
|
|
611
|
+
...Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')),
|
|
612
|
+
...Array.from(document.querySelectorAll('section, [class*="section"], article')),
|
|
613
|
+
...Array.from(document.querySelectorAll('p, span, div')),
|
|
614
|
+
];
|
|
615
|
+
for (const el of candidates) {
|
|
616
|
+
const text = el.textContent?.trim() ?? '';
|
|
617
|
+
if (re.test(text)) {
|
|
618
|
+
// Walk up to find a meaningful container
|
|
619
|
+
let container = el;
|
|
620
|
+
for (let i = 0; i < 5; i++) {
|
|
621
|
+
const parent = container.parentElement;
|
|
622
|
+
if (!parent || parent === document.body || parent === document.documentElement)
|
|
623
|
+
break;
|
|
624
|
+
const tag = parent.tagName.toLowerCase();
|
|
625
|
+
if (tag === 'section' || tag === 'article' || tag === 'main' ||
|
|
626
|
+
parent.classList.toString().match(/section|container|wrapper|block|card/i)) {
|
|
627
|
+
container = parent;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
container = parent;
|
|
631
|
+
}
|
|
632
|
+
const rect = container.getBoundingClientRect();
|
|
633
|
+
if (rect.width > 100 && rect.height > 50) {
|
|
634
|
+
// Generate a unique selector for this element
|
|
635
|
+
let sel = container.tagName.toLowerCase();
|
|
636
|
+
if (container.id)
|
|
637
|
+
sel = `#${container.id}`;
|
|
638
|
+
else if (container.className && typeof container.className === 'string') {
|
|
639
|
+
const cls = container.className.split(/\s+/).filter(c => c.length > 0 && !c.includes(':'))[0];
|
|
640
|
+
if (cls)
|
|
641
|
+
sel = `.${cls}`;
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
selector: sel,
|
|
645
|
+
bounds: {
|
|
646
|
+
x: rect.x + window.scrollX,
|
|
647
|
+
y: rect.y + window.scrollY,
|
|
648
|
+
width: rect.width,
|
|
649
|
+
height: rect.height,
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
}, textPattern.source);
|
|
657
|
+
if (element) {
|
|
658
|
+
results.push({
|
|
659
|
+
name: featureName,
|
|
660
|
+
pattern: patternKey,
|
|
661
|
+
selector: element.selector,
|
|
662
|
+
bounds: element.bounds,
|
|
663
|
+
matchMethod: 'text',
|
|
664
|
+
confidence: 'medium',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return results;
|
|
668
|
+
}
|
|
669
|
+
// ─── DOM Helpers ──────────────────────────────────────────────────────
|
|
670
|
+
async function getElementBounds(page, selector) {
|
|
671
|
+
try {
|
|
672
|
+
return await page.evaluate((sel) => {
|
|
673
|
+
const el = document.querySelector(sel);
|
|
674
|
+
if (!el)
|
|
675
|
+
return null;
|
|
676
|
+
const rect = el.getBoundingClientRect();
|
|
677
|
+
return {
|
|
678
|
+
x: rect.x + window.scrollX,
|
|
679
|
+
y: rect.y + window.scrollY,
|
|
680
|
+
width: rect.width,
|
|
681
|
+
height: rect.height,
|
|
682
|
+
};
|
|
683
|
+
}, selector);
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function meetsMinSize(bounds, minSize) {
|
|
690
|
+
if (!minSize)
|
|
691
|
+
return bounds.width > 10 && bounds.height > 10;
|
|
692
|
+
return bounds.width >= minSize.width && bounds.height >= minSize.height;
|
|
693
|
+
}
|
|
694
|
+
// ─── Page Preparation ─────────────────────────────────────────────────
|
|
695
|
+
async function dismissOverlays(page) {
|
|
696
|
+
// Set consent cookies
|
|
697
|
+
await page.evaluate(() => {
|
|
698
|
+
localStorage.setItem('cookie-consent', 'accepted');
|
|
699
|
+
localStorage.setItem('cookieConsent', 'accepted');
|
|
700
|
+
localStorage.setItem('cookie_consent', 'true');
|
|
701
|
+
localStorage.setItem('cookies-accepted', 'true');
|
|
702
|
+
localStorage.setItem('gdpr-consent', 'true');
|
|
703
|
+
localStorage.setItem('CookieConsent', 'true');
|
|
704
|
+
document.cookie = 'cookie-consent=accepted; path=/; max-age=31536000';
|
|
705
|
+
window.dispatchEvent(new Event('cookie-consent-accepted'));
|
|
706
|
+
});
|
|
707
|
+
await page.waitForTimeout(300);
|
|
708
|
+
// Click accept buttons
|
|
709
|
+
for (const text of ['Akzeptieren', 'Accept', 'Alle akzeptieren', 'Accept all', 'OK', 'Verstanden']) {
|
|
710
|
+
try {
|
|
711
|
+
const btn = page.locator(`button:has-text("${text}")`).first();
|
|
712
|
+
if (await btn.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
713
|
+
await btn.click({ timeout: 1000 });
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
catch { /* next */ }
|
|
718
|
+
}
|
|
719
|
+
// Force-hide common overlay selectors
|
|
720
|
+
await page.addStyleTag({
|
|
721
|
+
content: `
|
|
722
|
+
[class*="cookie"], [class*="Cookie"],
|
|
723
|
+
[class*="consent"], [class*="Consent"],
|
|
724
|
+
[id*="cookie"], [id*="consent"],
|
|
725
|
+
[role="dialog"],
|
|
726
|
+
.fixed.bottom-0.left-0.right-0.z-50 {
|
|
727
|
+
display: none !important;
|
|
728
|
+
visibility: hidden !important;
|
|
729
|
+
}
|
|
730
|
+
`,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
async function preloadContent(page, viewportHeight) {
|
|
734
|
+
const scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
735
|
+
const steps = Math.ceil(scrollHeight / (viewportHeight * 0.7));
|
|
736
|
+
for (let i = 0; i <= steps; i++) {
|
|
737
|
+
const y = Math.min(i * viewportHeight * 0.7, scrollHeight);
|
|
738
|
+
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
|
|
739
|
+
await page.waitForTimeout(150);
|
|
740
|
+
}
|
|
741
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
742
|
+
await page.waitForTimeout(400);
|
|
743
|
+
}
|
|
744
|
+
//# sourceMappingURL=smart-screenshot.js.map
|