page-analyzer 1.0.1 → 1.2.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/LICENSE +21 -0
- package/README.md +72 -9
- package/index.js +206 -22
- package/llm/analyzers/event-analyzer/event-analyzer-blocks.js +23 -2
- package/llm/analyzers/event-analyzer/event-analyzer-constants.js +1 -1
- package/llm/analyzers/event-analyzer/event-analyzer.js +1 -1
- package/package.json +6 -3
- package/page-extractor.js +562 -36
- package/result-viewer.html +1064 -0
- package/scripts/analyze.js +51 -0
- package/scripts/build-result-viewer.js +1076 -0
- package/scripts/serve-result-viewer.js +68 -0
- package/test/smoke.test.js +454 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
8
|
+
const preferredPort = Number.parseInt(process.argv[2] || process.env.PORT || '4173', 10);
|
|
9
|
+
|
|
10
|
+
const contentTypes = new Map([
|
|
11
|
+
['.html', 'text/html; charset=utf-8'],
|
|
12
|
+
['.json', 'application/json; charset=utf-8'],
|
|
13
|
+
['.js', 'text/javascript; charset=utf-8'],
|
|
14
|
+
['.css', 'text/css; charset=utf-8'],
|
|
15
|
+
['.png', 'image/png'],
|
|
16
|
+
['.jpg', 'image/jpeg'],
|
|
17
|
+
['.jpeg', 'image/jpeg'],
|
|
18
|
+
['.webp', 'image/webp'],
|
|
19
|
+
['.svg', 'image/svg+xml']
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function resolveRequestPath(requestUrl) {
|
|
23
|
+
const url = new URL(requestUrl || '/', 'http://127.0.0.1');
|
|
24
|
+
const pathname = url.pathname === '/' ? '/result-viewer.html' : decodeURIComponent(url.pathname);
|
|
25
|
+
const filePath = path.resolve(projectRoot, `.${pathname}`);
|
|
26
|
+
if (!filePath.startsWith(projectRoot)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return filePath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const server = http.createServer(async (request, response) => {
|
|
33
|
+
const filePath = resolveRequestPath(request.url);
|
|
34
|
+
if (!filePath) {
|
|
35
|
+
response.writeHead(403);
|
|
36
|
+
response.end('Forbidden');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const body = await fs.readFile(filePath);
|
|
42
|
+
response.writeHead(200, {
|
|
43
|
+
'Content-Type': contentTypes.get(path.extname(filePath).toLowerCase()) || 'application/octet-stream',
|
|
44
|
+
'Cache-Control': 'no-store'
|
|
45
|
+
});
|
|
46
|
+
response.end(body);
|
|
47
|
+
} catch {
|
|
48
|
+
response.writeHead(404);
|
|
49
|
+
response.end('Not found');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function listen(port, attemptsLeft = 20) {
|
|
54
|
+
server.once('error', (error) => {
|
|
55
|
+
if (error.code === 'EADDRINUSE' && attemptsLeft > 0) {
|
|
56
|
+
listen(port + 1, attemptsLeft - 1);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.listen(port, '127.0.0.1', () => {
|
|
63
|
+
const address = server.address();
|
|
64
|
+
console.log(`Result viewer: http://127.0.0.1:${address.port}/result-viewer.html`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
listen(Number.isFinite(preferredPort) ? preferredPort : 4173);
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { EventAnalyzer } from '../llm/analyzers/event-analyzer/event-analyzer.js';
|
|
3
|
+
import { buildBlockAnalysisArtifact } from '../llm/analyzers/event-analyzer/event-analyzer-blocks.js';
|
|
4
|
+
import { OpenAiProvider } from '../llm/providers/openai-provider.js';
|
|
5
|
+
import { PageExtractor } from '../page-extractor.js';
|
|
6
|
+
import { analyzeUrl } from '../index.js';
|
|
7
|
+
|
|
8
|
+
class FakeProvider {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.calls = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async analyze(prompt) {
|
|
14
|
+
if (prompt.includes('DOM CSV')) {
|
|
15
|
+
this.calls.push('event');
|
|
16
|
+
return 'csv_id,event_type,attributes_kv\n0,signup,intent=cta';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.calls.push('special');
|
|
20
|
+
return [
|
|
21
|
+
'Demo page with a primary CTA',
|
|
22
|
+
'blockIdxs,blockName,blockDescription,blockPossibleEvents',
|
|
23
|
+
'0,CTASection,Primary CTA section,signup.cta_click'
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class FakeLocator {
|
|
29
|
+
constructor({ count = 1, throwOnScreenshot = false } = {}) {
|
|
30
|
+
this.countValue = count;
|
|
31
|
+
this.throwOnScreenshot = throwOnScreenshot;
|
|
32
|
+
this.screenshots = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
first() {
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async count() {
|
|
40
|
+
return this.countValue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async screenshot(options) {
|
|
44
|
+
this.screenshots.push(options);
|
|
45
|
+
if (this.throwOnScreenshot) {
|
|
46
|
+
throw new Error('selector screenshot failed');
|
|
47
|
+
}
|
|
48
|
+
return Buffer.from(`locator screenshot:${options?.path || 'buffer'}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class FakePage {
|
|
53
|
+
constructor(locator) {
|
|
54
|
+
this.locatorInstance = locator;
|
|
55
|
+
this.locatorSelectors = [];
|
|
56
|
+
this.evaluateCalls = [];
|
|
57
|
+
this.pageScreenshots = [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
locator(selector) {
|
|
61
|
+
this.locatorSelectors.push(selector);
|
|
62
|
+
return this.locatorInstance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async evaluate(_fn, arg) {
|
|
66
|
+
this.evaluateCalls.push(arg);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async screenshot(options) {
|
|
71
|
+
this.pageScreenshots.push(options);
|
|
72
|
+
return Buffer.from(`page screenshot:${options?.path || 'buffer'}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class FakeS3Client {
|
|
77
|
+
constructor({ failPredicate = null } = {}) {
|
|
78
|
+
this.failPredicate = failPredicate;
|
|
79
|
+
this.commands = [];
|
|
80
|
+
this.attemptsByKey = new Map();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async send(command) {
|
|
84
|
+
const input = command.input;
|
|
85
|
+
this.commands.push(input);
|
|
86
|
+
const attempts = (this.attemptsByKey.get(input.Key) || 0) + 1;
|
|
87
|
+
this.attemptsByKey.set(input.Key, attempts);
|
|
88
|
+
|
|
89
|
+
if (this.failPredicate?.(input, attempts)) {
|
|
90
|
+
throw new Error(`s3 upload failed for ${input.Key}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const csvContent = [
|
|
98
|
+
'idx,blockIdx,tag,imageAlt,text,context,href',
|
|
99
|
+
'0,0,a,,Sign up,,https://example.com/signup'
|
|
100
|
+
].join('\n');
|
|
101
|
+
|
|
102
|
+
const blocks = [{
|
|
103
|
+
blockIdx: 0,
|
|
104
|
+
branchPath: 'body.0',
|
|
105
|
+
depth: 1,
|
|
106
|
+
domOrder: 1,
|
|
107
|
+
tag: 'section',
|
|
108
|
+
fixed: false,
|
|
109
|
+
top: 0,
|
|
110
|
+
left: 0,
|
|
111
|
+
width: 1000,
|
|
112
|
+
height: 200,
|
|
113
|
+
textPreview: 'Sign up',
|
|
114
|
+
childInteractiveCount: 1
|
|
115
|
+
}];
|
|
116
|
+
|
|
117
|
+
async function analyzeWith(options = {}) {
|
|
118
|
+
const provider = new FakeProvider();
|
|
119
|
+
const analyzer = new EventAnalyzer(provider, {});
|
|
120
|
+
const result = await analyzer.analyzeEvents(csvContent, '', [], {
|
|
121
|
+
blocks,
|
|
122
|
+
...options
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return { provider, result };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
const { provider, result } = await analyzeWith();
|
|
130
|
+
|
|
131
|
+
assert.deepEqual(provider.calls, ['special']);
|
|
132
|
+
assert.equal(result.events_by_node.length, 0);
|
|
133
|
+
assert.equal(result.block_analysis.stats.llm_blocks, 1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
const { provider, result } = await analyzeWith({ analyzeNodeEvents: true });
|
|
138
|
+
|
|
139
|
+
assert.deepEqual(provider.calls, ['special', 'event']);
|
|
140
|
+
assert.equal(result.events_by_node.length, 1);
|
|
141
|
+
assert.equal(result.events_by_node[0].event_type, 'signup');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
const originalWithPreparedPage = PageExtractor.prototype.withPreparedPage;
|
|
146
|
+
const originalExtractPreparedPage = PageExtractor.prototype.extractPreparedPage;
|
|
147
|
+
const originalCaptureScreenshots = PageExtractor.prototype.captureScreenshots;
|
|
148
|
+
const originalCaptureUrlScreenshots = PageExtractor.prototype.captureUrlScreenshots;
|
|
149
|
+
const originalAnalyze = OpenAiProvider.prototype.analyze;
|
|
150
|
+
const calls = [];
|
|
151
|
+
const fakePage = { pageId: 'prepared-page' };
|
|
152
|
+
|
|
153
|
+
PageExtractor.prototype.withPreparedPage = async function withPreparedPage(inputUrl, callback) {
|
|
154
|
+
calls.push(['withPreparedPage', inputUrl]);
|
|
155
|
+
return await callback(fakePage, String(inputUrl || '').trim());
|
|
156
|
+
};
|
|
157
|
+
PageExtractor.prototype.extractPreparedPage = async function extractPreparedPage(page, targetUrl) {
|
|
158
|
+
calls.push(['extractPreparedPage', page, targetUrl]);
|
|
159
|
+
assert.equal(page, fakePage);
|
|
160
|
+
return {
|
|
161
|
+
html: [
|
|
162
|
+
'<!doctype html><html><head><title>Demo</title></head><body>',
|
|
163
|
+
'<main><section><a href="/signup">Sign up</a></section></main>',
|
|
164
|
+
'</body></html>'
|
|
165
|
+
].join(''),
|
|
166
|
+
blocks: [{
|
|
167
|
+
blockIdx: 0,
|
|
168
|
+
blockCssPath: 'body > main:nth-of-type(1) > section:nth-of-type(1)',
|
|
169
|
+
top: 0,
|
|
170
|
+
left: 0,
|
|
171
|
+
width: 1000,
|
|
172
|
+
height: 200,
|
|
173
|
+
textPreview: 'Sign up'
|
|
174
|
+
}],
|
|
175
|
+
elementGeometries: [{
|
|
176
|
+
tag: 'a',
|
|
177
|
+
text: 'Sign up',
|
|
178
|
+
href: 'https://example.com/signup',
|
|
179
|
+
top: 0,
|
|
180
|
+
left: 0,
|
|
181
|
+
width: 80,
|
|
182
|
+
height: 24,
|
|
183
|
+
selectorNthOfType: 'body > main:nth-of-type(1) > section:nth-of-type(1) > a:nth-of-type(1)'
|
|
184
|
+
}],
|
|
185
|
+
screenshots: { fullPage: '/tmp/full-page.png' },
|
|
186
|
+
pageSize: { width: 1000, height: 800 }
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
PageExtractor.prototype.captureScreenshots = async function captureScreenshots(page, targetUrl, screenshotBlocks, options) {
|
|
190
|
+
calls.push(['captureScreenshots', page, targetUrl, screenshotBlocks.length, options]);
|
|
191
|
+
assert.equal(page, fakePage);
|
|
192
|
+
return {
|
|
193
|
+
blocks: screenshotBlocks.map((_block, index) => ({
|
|
194
|
+
blockIdx: index,
|
|
195
|
+
path: `/tmp/logical-block-${index}.png`
|
|
196
|
+
}))
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
PageExtractor.prototype.captureUrlScreenshots = async function captureUrlScreenshots() {
|
|
200
|
+
calls.push(['captureUrlScreenshots']);
|
|
201
|
+
throw new Error('captureUrlScreenshots should not be called by analyzeUrl block screenshots');
|
|
202
|
+
};
|
|
203
|
+
OpenAiProvider.prototype.analyze = async function analyze(prompt) {
|
|
204
|
+
calls.push(['llm', prompt.includes('DOM CSV') ? 'event' : 'special']);
|
|
205
|
+
return [
|
|
206
|
+
'Demo page with a primary CTA',
|
|
207
|
+
'blockIdxs,blockName,blockDescription,blockPossibleEvents',
|
|
208
|
+
'0,CTASection,Primary CTA section,signup.cta_click'
|
|
209
|
+
].join('\n');
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const result = await analyzeUrl(' https://example.com/demo ', {
|
|
214
|
+
llm: {
|
|
215
|
+
apiKey: 'test-key',
|
|
216
|
+
apiEndpoint: 'https://llm.example.invalid/v1/chat/completions',
|
|
217
|
+
model: 'test-model'
|
|
218
|
+
},
|
|
219
|
+
fullPageScreenshot: true,
|
|
220
|
+
blockScreenshots: true,
|
|
221
|
+
showBlockIdx: true
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
assert.equal(calls.filter((call) => call[0] === 'withPreparedPage').length, 1);
|
|
225
|
+
assert.equal(calls.some((call) => call[0] === 'captureUrlScreenshots'), false);
|
|
226
|
+
assert.equal(calls.filter((call) => call[0] === 'captureScreenshots').length, 1);
|
|
227
|
+
assert.equal(result.screenshots.fullPage, '/tmp/full-page.png');
|
|
228
|
+
assert.equal(result.screenshots.blocks[0].path, '/tmp/logical-block-0.png');
|
|
229
|
+
assert.equal(
|
|
230
|
+
result.analysis.block_analysis.blocks[0].blockScreenshotPaths[0],
|
|
231
|
+
'/tmp/logical-block-0.png'
|
|
232
|
+
);
|
|
233
|
+
} finally {
|
|
234
|
+
PageExtractor.prototype.withPreparedPage = originalWithPreparedPage;
|
|
235
|
+
PageExtractor.prototype.extractPreparedPage = originalExtractPreparedPage;
|
|
236
|
+
PageExtractor.prototype.captureScreenshots = originalCaptureScreenshots;
|
|
237
|
+
PageExtractor.prototype.captureUrlScreenshots = originalCaptureUrlScreenshots;
|
|
238
|
+
OpenAiProvider.prototype.analyze = originalAnalyze;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
{
|
|
243
|
+
const extractor = new PageExtractor();
|
|
244
|
+
const locator = new FakeLocator({ count: 0 });
|
|
245
|
+
const page = new FakePage(locator);
|
|
246
|
+
const captured = await extractor.captureBlockScreenshot(page, {
|
|
247
|
+
blockCssPath: 'main > section:nth-of-type(1)',
|
|
248
|
+
left: 0,
|
|
249
|
+
top: 0,
|
|
250
|
+
width: 1200,
|
|
251
|
+
height: 300
|
|
252
|
+
}, '/tmp/block.png');
|
|
253
|
+
|
|
254
|
+
assert.equal(captured, false);
|
|
255
|
+
assert.deepEqual(page.locatorSelectors, ['main > section:nth-of-type(1)']);
|
|
256
|
+
assert.equal(page.pageScreenshots.length, 0);
|
|
257
|
+
assert.equal(page.evaluateCalls.length, 0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
{
|
|
261
|
+
const extractor = new PageExtractor();
|
|
262
|
+
const locator = new FakeLocator();
|
|
263
|
+
const page = new FakePage(locator);
|
|
264
|
+
const captured = await extractor.captureBlockScreenshot(page, {
|
|
265
|
+
blockCssPath: 'main > section:nth-of-type(2)'
|
|
266
|
+
}, '/tmp/block.png');
|
|
267
|
+
|
|
268
|
+
assert.equal(captured, true);
|
|
269
|
+
assert.deepEqual(locator.screenshots, [{ path: '/tmp/block.png' }]);
|
|
270
|
+
assert.equal(page.pageScreenshots.length, 0);
|
|
271
|
+
assert.equal(page.evaluateCalls.length, 2);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
{
|
|
275
|
+
const extractor = new PageExtractor();
|
|
276
|
+
const locator = new FakeLocator({ throwOnScreenshot: true });
|
|
277
|
+
const page = new FakePage(locator);
|
|
278
|
+
const captured = await extractor.captureBlockScreenshot(page, {
|
|
279
|
+
blockCssPath: 'main > section:nth-of-type(3)',
|
|
280
|
+
left: 0,
|
|
281
|
+
top: 0,
|
|
282
|
+
width: 1200,
|
|
283
|
+
height: 300
|
|
284
|
+
}, '/tmp/block.png');
|
|
285
|
+
|
|
286
|
+
assert.equal(captured, false);
|
|
287
|
+
assert.equal(page.pageScreenshots.length, 0);
|
|
288
|
+
assert.equal(page.evaluateCalls.length, 2);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
{
|
|
292
|
+
const extractor = new PageExtractor({
|
|
293
|
+
snapshotDir: '/tmp/page-analyzer-smoke-snapshots'
|
|
294
|
+
});
|
|
295
|
+
const locator = new FakeLocator();
|
|
296
|
+
const page = new FakePage(locator);
|
|
297
|
+
const screenshots = await extractor.captureScreenshots(page, 'https://example.com/demo', [
|
|
298
|
+
{ blockName: 'Hero', blockCssPath: '#hero' },
|
|
299
|
+
{ blockName: 'Footer', blockCssPath: '#footer' }
|
|
300
|
+
], {
|
|
301
|
+
fullPageScreenshot: false,
|
|
302
|
+
blockScreenshots: true
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
assert.equal(screenshots.blocks.length, 2);
|
|
306
|
+
assert.deepEqual(
|
|
307
|
+
screenshots.blocks.map((item) => ({ blockIdx: item.blockIdx, blockName: item.blockName })),
|
|
308
|
+
[
|
|
309
|
+
{ blockIdx: 0, blockName: 'Hero' },
|
|
310
|
+
{ blockIdx: 1, blockName: 'Footer' }
|
|
311
|
+
]
|
|
312
|
+
);
|
|
313
|
+
assert.equal(page.pageScreenshots.length, 0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
{
|
|
317
|
+
assert.throws(
|
|
318
|
+
() => new PageExtractor({ s3: { region: 'ap-northeast-1' } }),
|
|
319
|
+
/extractorConfig\.s3\.bucket is required/
|
|
320
|
+
);
|
|
321
|
+
assert.throws(
|
|
322
|
+
() => new PageExtractor({ s3: { bucket: 'page-analyzer-test' } }),
|
|
323
|
+
/extractorConfig\.s3\.region is required/
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
{
|
|
328
|
+
const s3Client = new FakeS3Client();
|
|
329
|
+
const extractor = new PageExtractor({
|
|
330
|
+
s3: {
|
|
331
|
+
bucket: 'page-analyzer-test',
|
|
332
|
+
region: 'ap-northeast-1',
|
|
333
|
+
prefix: '/page-analyzer/snapshots/',
|
|
334
|
+
publicBaseUrl: 'https://cdn.example.com/page-analyzer/snapshots/',
|
|
335
|
+
client: s3Client
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
const locator = new FakeLocator();
|
|
339
|
+
const page = new FakePage(locator);
|
|
340
|
+
const screenshots = await extractor.captureScreenshots(page, 'https://example.com/demo', [
|
|
341
|
+
{ blockName: 'Hero', blockCssPath: '#hero' }
|
|
342
|
+
], {
|
|
343
|
+
fullPageScreenshot: true,
|
|
344
|
+
blockScreenshots: true
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
assert.equal(page.pageScreenshots.length, 1);
|
|
348
|
+
assert.deepEqual(page.pageScreenshots[0], { fullPage: true });
|
|
349
|
+
assert.equal(locator.screenshots.length, 1);
|
|
350
|
+
assert.deepEqual(locator.screenshots[0], {});
|
|
351
|
+
assert.equal(s3Client.commands.length, 2);
|
|
352
|
+
|
|
353
|
+
const [fullPageUpload, blockUpload] = s3Client.commands;
|
|
354
|
+
assert.equal(fullPageUpload.Bucket, 'page-analyzer-test');
|
|
355
|
+
assert.equal(fullPageUpload.ContentType, 'image/png');
|
|
356
|
+
assert.equal(Buffer.isBuffer(fullPageUpload.Body), true);
|
|
357
|
+
assert.match(fullPageUpload.Key, /^page-analyzer\/snapshots\/example-com-demo-.*-full-page\.png$/);
|
|
358
|
+
assert.match(blockUpload.Key, /^page-analyzer\/snapshots\/example-com-demo-.*-block-000\.png$/);
|
|
359
|
+
|
|
360
|
+
const fullPageFilename = fullPageUpload.Key.split('/').pop();
|
|
361
|
+
const blockFilename = blockUpload.Key.split('/').pop();
|
|
362
|
+
assert.equal(
|
|
363
|
+
screenshots.fullPage,
|
|
364
|
+
`https://cdn.example.com/page-analyzer/snapshots/${fullPageFilename}`
|
|
365
|
+
);
|
|
366
|
+
assert.equal(screenshots.blocks[0].path, `https://cdn.example.com/page-analyzer/snapshots/${blockFilename}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
{
|
|
370
|
+
const s3Client = new FakeS3Client();
|
|
371
|
+
const extractor = new PageExtractor({
|
|
372
|
+
s3: {
|
|
373
|
+
bucket: 'page-analyzer-test',
|
|
374
|
+
region: 'ap-northeast-1',
|
|
375
|
+
prefix: 'nested/prefix',
|
|
376
|
+
client: s3Client
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
const locator = new FakeLocator();
|
|
380
|
+
const page = new FakePage(locator);
|
|
381
|
+
const screenshots = await extractor.captureScreenshots(page, 'https://example.com/demo', [
|
|
382
|
+
{ blockName: 'Hero', blockCssPath: '#hero' }
|
|
383
|
+
], {
|
|
384
|
+
fullPageScreenshot: false,
|
|
385
|
+
blockScreenshots: true
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const uploadedKey = s3Client.commands[0].Key;
|
|
389
|
+
assert.match(uploadedKey, /^nested\/prefix\/example-com-demo-.*-block-000\.png$/);
|
|
390
|
+
assert.equal(
|
|
391
|
+
screenshots.blocks[0].path,
|
|
392
|
+
`https://page-analyzer-test.s3.ap-northeast-1.amazonaws.com/${uploadedKey}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
{
|
|
397
|
+
const originalWarn = console.warn;
|
|
398
|
+
const warnings = [];
|
|
399
|
+
console.warn = (message) => warnings.push(message);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const s3Client = new FakeS3Client({
|
|
403
|
+
failPredicate: (input) => input.Key.endsWith('-block-000.png')
|
|
404
|
+
});
|
|
405
|
+
const extractor = new PageExtractor({
|
|
406
|
+
s3: {
|
|
407
|
+
bucket: 'page-analyzer-test',
|
|
408
|
+
region: 'ap-northeast-1',
|
|
409
|
+
prefix: 'page-analyzer/snapshots',
|
|
410
|
+
publicBaseUrl: 'https://cdn.example.com/page-analyzer/snapshots',
|
|
411
|
+
client: s3Client
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
const locator = new FakeLocator();
|
|
415
|
+
const page = new FakePage(locator);
|
|
416
|
+
const screenshots = await extractor.captureScreenshots(page, 'https://example.com/demo', [
|
|
417
|
+
{ blockName: 'Hero', blockCssPath: '#hero' },
|
|
418
|
+
{ blockName: 'Footer', blockCssPath: '#footer' }
|
|
419
|
+
], {
|
|
420
|
+
fullPageScreenshot: false,
|
|
421
|
+
blockScreenshots: true
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
assert.equal(screenshots.blocks.length, 1);
|
|
425
|
+
assert.equal(screenshots.blocks[0].blockIdx, 1);
|
|
426
|
+
assert.equal(s3Client.commands.filter((input) => input.Key.endsWith('-block-000.png')).length, 3);
|
|
427
|
+
assert.equal(s3Client.commands.filter((input) => input.Key.endsWith('-block-001.png')).length, 1);
|
|
428
|
+
assert.equal(warnings.some((message) => message.includes('retrying')), true);
|
|
429
|
+
assert.equal(warnings.some((message) => message.includes('Failed to capture/upload block 0')), true);
|
|
430
|
+
} finally {
|
|
431
|
+
console.warn = originalWarn;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
{
|
|
436
|
+
const artifact = buildBlockAnalysisArtifact('Demo', [{
|
|
437
|
+
blockIdx: 3,
|
|
438
|
+
blockIdxKey: '3.4',
|
|
439
|
+
blockName: 'ContentSection',
|
|
440
|
+
blockDescription: '',
|
|
441
|
+
possibleEvents: [],
|
|
442
|
+
semanticLabels: [],
|
|
443
|
+
semanticGroups: [],
|
|
444
|
+
rows: [],
|
|
445
|
+
sourceBlocks: [
|
|
446
|
+
{ blockCssPath: 'body > main:nth-of-type(1) > section:nth-of-type(1)' },
|
|
447
|
+
{ blockCssPath: 'body > main:nth-of-type(1) > section:nth-of-type(2)' }
|
|
448
|
+
]
|
|
449
|
+
}], []);
|
|
450
|
+
|
|
451
|
+
assert.equal(artifact.blocks[0].blockCssPath, 'body > main:nth-of-type(1)');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log('smoke tests passed');
|