@appaflytech/zeyrek-ai-mcp 0.0.7 → 0.0.9
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/README.md +27 -21
- package/dist/config.js +2 -0
- package/dist/knowledge/backendConstraints.js +3 -0
- package/dist/knowledge/componentCatalog.js +172 -33
- package/dist/knowledge/componentEvents.js +35 -4
- package/dist/knowledge/errorPath.js +1 -1
- package/dist/knowledge/normalize.js +474 -23
- package/dist/knowledge/propTypes.js +5 -4
- package/dist/knowledge/rules.js +113 -6
- package/dist/knowledge/styleResolve.js +52 -5
- package/dist/knowledge/themeResolve.js +936 -0
- package/dist/knowledge/themeRules.js +221 -0
- package/dist/knowledge/themeSchema.js +335 -0
- package/dist/platform/domSummary.js +144 -0
- package/dist/platform/http.js +10 -2
- package/dist/platform/screenshot.js +492 -29
- package/dist/platform/screenshotMemory.js +55 -0
- package/dist/platform/strictArguments.js +32 -0
- package/dist/platform/writeDiff.js +6 -3
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/page-sign-in.json +1 -1
- package/dist/resources/examples/theme-brand.json +162 -0
- package/dist/resources/guides/component.md +8 -2
- package/dist/resources/guides/expressions.md +6 -0
- package/dist/resources/guides/getting-started.md +6 -0
- package/dist/resources/guides/layout.md +36 -26
- package/dist/resources/guides/microservice.md +160 -6
- package/dist/resources/guides/page.md +165 -59
- package/dist/resources/guides/theme.md +210 -0
- package/dist/resources/guides/translation.md +13 -10
- package/dist/resources/guides/workflow.md +141 -7
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/CreateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/CreateWorkflowCommand.json +4 -1
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/PreviewWorkflowCommand.json +4 -1
- package/dist/resources/schemas/StepDto.json +4 -1
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/UpdateWorkflowCommand.json +4 -1
- package/dist/server.js +52 -42
- package/dist/tools/components.js +8 -0
- package/dist/tools/designSchemaPatch.js +152 -0
- package/dist/tools/flows.js +24 -21
- package/dist/tools/layouts.js +8 -8
- package/dist/tools/microservices.js +74 -10
- package/dist/tools/pages.js +275 -332
- package/dist/tools/projects.js +66 -9
- package/dist/tools/queries.js +72 -6
- package/dist/tools/schemaOps.js +280 -0
- package/dist/tools/screenWrite.js +2 -1
- package/dist/tools/screens.js +0 -7
- package/dist/tools/themes.js +163 -0
- package/dist/tools/workflows.js +10 -6
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PlatformApiError } from './errors.js';
|
|
2
|
+
import { parseDomSummary } from './domSummary.js';
|
|
2
3
|
import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../knowledge/componentCatalog.js';
|
|
3
4
|
import { logStep } from '../observability/trace.js';
|
|
4
5
|
/**
|
|
@@ -29,55 +30,300 @@ export function actionScript(action) {
|
|
|
29
30
|
var node = document.querySelector(${selector});
|
|
30
31
|
if (!node) return { found: false, applied: false, requests: count };
|
|
31
32
|
var target = node.matches('button, a, [role="button"]') ? node : (node.querySelector('button, a, [role="button"]') || node);
|
|
33
|
+
// S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
|
|
34
|
+
// ajan ikinci sekmeyi hiç seçemiyordu (2026-09-16 raporu madde 6). Sekme seçmek 'tab'in işi.
|
|
35
|
+
if (target.closest && target.closest('[role="tablist"]') && !node.matches('[role="tab"]')) {
|
|
36
|
+
return { found: true, applied: false, note: "hedef bir sekme grubu: click İLK sekmeye giderdi — sekme seçmek için { action: 'tab', target: '" + ${JSON.stringify(String(action.target ?? ''))} + "', value: '<sekme anahtarı ya da metni>' } kullanın", requests: count };
|
|
37
|
+
}
|
|
32
38
|
target.click();
|
|
33
39
|
return { found: true, applied: true, requests: count };
|
|
34
40
|
})()`;
|
|
35
41
|
}
|
|
42
|
+
// S-2 — sekme SEÇİLEMİYORDU: kimlik `<elementId>__tab-<key>` (frontend f-02) önce denenir,
|
|
43
|
+
// runtime onu basmıyorsa görünen metinle eşlenir ve rapor hangisiyle eşlediğini söyler —
|
|
44
|
+
// metin çeviriye bağlıdır, dil değişince kırılır.
|
|
45
|
+
if (action.action === 'tab') {
|
|
46
|
+
const roleSelector = JSON.stringify(`[data-element-id="${String(action.target ?? '')}__tab-${String(action.value ?? '')}"]`);
|
|
47
|
+
return `(function () {
|
|
48
|
+
var count = performance.getEntriesByType('resource').length;
|
|
49
|
+
var root = document.querySelector(${selector});
|
|
50
|
+
if (!root) return { found: false, applied: false, requests: count };
|
|
51
|
+
var wanted = String(${value});
|
|
52
|
+
var byRole = document.querySelector(${roleSelector});
|
|
53
|
+
var buttons = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
|
|
54
|
+
var byText = buttons.filter(function (button) { return (button.textContent || '').trim() === wanted; })[0]
|
|
55
|
+
|| buttons.filter(function (button) { return (button.textContent || '').trim().toLowerCase() === wanted.toLowerCase(); })[0];
|
|
56
|
+
var target = byRole || byText;
|
|
57
|
+
|
|
58
|
+
if (!target) {
|
|
59
|
+
var names = buttons.map(function (button) { return (button.textContent || '').trim(); }).join(' · ');
|
|
60
|
+
return { found: true, applied: false, optionsSeen: buttons.length, requests: count, note: buttons.length === 0
|
|
61
|
+
? "bu düğümde sekme yok (role=tab) — hedef gerçekten bir sekme grubu mu? page_schema_get ile tipe bakın"
|
|
62
|
+
: "sekme bulunamadı: " + wanted + " — sekme ANAHTARINI ya da görünen metnini verin; görülenler: " + names };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (target.disabled === true || target.getAttribute('aria-disabled') === 'true') {
|
|
66
|
+
return { found: true, applied: false, note: "sekme devre dışı (disabled): " + wanted, requests: count };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
target.click();
|
|
70
|
+
return { found: true, applied: true, matchedBy: byRole ? 'role' : 'text', requests: count };
|
|
71
|
+
})()`;
|
|
72
|
+
}
|
|
73
|
+
// S-3 — açılır kutuda seçim. 'set' ile aynı gövde; buradaki fark yalnız prop izi tutulmamasıdır
|
|
74
|
+
// (seçim kutunun kendi değerini yazar, DOM'a elle yazılan bir değer yoktur).
|
|
75
|
+
if (action.action === 'select') {
|
|
76
|
+
return `(function () {
|
|
77
|
+
var count = performance.getEntriesByType('resource').length;
|
|
78
|
+
var node = document.querySelector(${selector});
|
|
79
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
80
|
+
var wanted = String(${value});
|
|
81
|
+
var finish = function (result) { return result; };
|
|
82
|
+
|
|
83
|
+
var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
|
|
84
|
+
if (combobox) {
|
|
85
|
+
// Liste BU kutunun kabının içindedir. Sorgular belge kapsamlı kaldığında açık duran başka
|
|
86
|
+
// bir kutunun (multiselect seçimden sonra kapanmıyor) seçeneğine tıklanabiliyordu ve adım
|
|
87
|
+
// "uygulandı" diyordu; komşunun yükleme göstergesi de bu kutunun kararını blokluyordu (B-5).
|
|
88
|
+
var scope = combobox.closest('.ng-select, .ng-multiselect') || node;
|
|
89
|
+
|
|
90
|
+
combobox.click();
|
|
91
|
+
|
|
92
|
+
return new Promise(function (resolve) {
|
|
93
|
+
var waited = 0;
|
|
94
|
+
var step = 100;
|
|
95
|
+
var limit = 3000;
|
|
96
|
+
|
|
97
|
+
var attempt = function () {
|
|
98
|
+
var loading = scope.querySelector('.ng-select__loading, .ng-multiselect__loading');
|
|
99
|
+
var options = Array.prototype.slice.call(scope.querySelectorAll('[role="treeitem"][data-value]'));
|
|
100
|
+
var byValue = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0];
|
|
101
|
+
var byText = options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
|
|
102
|
+
var match = byValue || byText;
|
|
103
|
+
|
|
104
|
+
if (match) {
|
|
105
|
+
var button = match.querySelector('button');
|
|
106
|
+
// Seçilemeyen seçeneğe tıklamak hiçbir şey yapmaz; "uygulandı" demek yanlış olurdu (B-6).
|
|
107
|
+
if (match.getAttribute('aria-disabled') === 'true' || (button && button.disabled)) {
|
|
108
|
+
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: "seçenek devre dışı (aria-disabled): " + wanted }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
(button || match).click();
|
|
113
|
+
resolve(finish({ found: true, applied: true, matchedBy: byValue ? 'value' : 'text', optionsSeen: options.length, waitedMs: waited, requests: count }));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
var settled = !loading && (options.length > 0 || !!scope.querySelector('.ng-select__empty, .ng-multiselect__empty'));
|
|
118
|
+
|
|
119
|
+
if (settled || waited >= limit) {
|
|
120
|
+
var seen = options.slice(0, 10).map(function (option) { return (option.textContent || '').trim(); }).join(' · ');
|
|
121
|
+
// Liste HİÇ açılmadıysa kusur sorguda değil kutunun kendisinde olabilir:
|
|
122
|
+
// handleContainerClick kilitli/salt-okunur/yükleniyor kutuda erken döner.
|
|
123
|
+
var locked = !scope.querySelector('.ng-select__list, .ng-multiselect__list');
|
|
124
|
+
var note = options.length > 0
|
|
125
|
+
? "açılır kutuda bu değere karşılık gelen seçenek yok: " + wanted + " — görülen " + options.length + " seçenek: " + seen
|
|
126
|
+
: locked
|
|
127
|
+
? "açılır kutu AÇILMADI (" + waited + " ms): kutu devre dışı, salt okunur ya da hâlâ yükleniyor olabilir — page_runtime_get ile isDisabled/isReadonly/isLoading prop'larına bakın"
|
|
128
|
+
: "açılır kutu açıldı ama " + waited + " ms içinde hiç seçenek basılmadı — seçenekler bir sorgudan geliyorsa query_preview ile doğrulayın, yavaşsa önce bir wait adımı ekleyin";
|
|
129
|
+
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: note }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
waited += step;
|
|
134
|
+
setTimeout(attempt, step);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
attempt();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return { found: true, applied: false, note: "bu düğümde açılır kutu yok (role=combobox / .ng-select__value-container) — native alan için 'set', butona basmak için 'click' kullanın", requests: count };
|
|
141
|
+
})()`;
|
|
142
|
+
}
|
|
143
|
+
// S-4 — form gönderme. Gönder butonu olmayan ya da Enter ile gönderilen form başsız
|
|
144
|
+
// doğrulanamıyordu; `requestSubmit` formun kendi submit olayını doğduğu gibi doğurur.
|
|
145
|
+
if (action.action === 'submit') {
|
|
146
|
+
return `(function () {
|
|
147
|
+
var count = performance.getEntriesByType('resource').length;
|
|
148
|
+
var node = document.querySelector(${selector});
|
|
149
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
150
|
+
var form = node.matches('form') ? node : (node.querySelector('form') || node.closest('form'));
|
|
151
|
+
|
|
152
|
+
if (form) {
|
|
153
|
+
if (typeof form.requestSubmit === 'function') {
|
|
154
|
+
form.requestSubmit();
|
|
155
|
+
return { found: true, applied: true, submitVia: 'requestSubmit', requests: count };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
159
|
+
return { found: true, applied: true, submitVia: 'dispatch', requests: count };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var button = node.querySelector('button[type="submit"]');
|
|
163
|
+
|
|
164
|
+
if (button) {
|
|
165
|
+
button.click();
|
|
166
|
+
return { found: true, applied: true, submitVia: 'button', requests: count };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { found: true, applied: false, note: "bu düğüm bir form değil ve içinde submit butonu da yok — form komponentinin elementId'sini hedefleyin", requests: count };
|
|
170
|
+
})()`;
|
|
171
|
+
}
|
|
172
|
+
// G-09 — kanban kartı ve takvim etkinliği HTML5 DnD ile taşınır. Olaylar sentetik atılır ve
|
|
173
|
+
// TEK DataTransfer'i paylaşır; koordinat bırakma hedefinin merkezidir, çünkü takvimin saat
|
|
174
|
+
// kolonu bırakılan saati `clientY`'den hesaplar (slot hedefi o saatin ortasına düşer).
|
|
175
|
+
// Playwright `dragTo` gerçek fare sürer ama ScreenshotPage bu yeteneği taşımaz; betik yolu
|
|
176
|
+
// öteki eylemlerle aynı ölçüm döngüsünden (requests · flowRuns) geçer.
|
|
177
|
+
if (action.action === 'drag') {
|
|
178
|
+
const destination = JSON.stringify(`[data-element-id="${String(action.to ?? '')}"]`);
|
|
179
|
+
const destinationName = JSON.stringify(String(action.to ?? ''));
|
|
180
|
+
return `(function () {
|
|
181
|
+
var count = performance.getEntriesByType('resource').length;
|
|
182
|
+
var node = document.querySelector(${selector});
|
|
183
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
184
|
+
var drop = document.querySelector(${destination});
|
|
185
|
+
if (!drop) return { found: false, applied: false, note: "sürükleme kaynağı bulundu ama bırakma hedefi ('to') sayfada yok: " + ${destinationName} + " — kanban kolonu <elementId>__col-<value>, takvim günü <elementId>__day-<yyyy-MM-dd>, saat dilimi <elementId>__slot-<yyyy-MM-dd>T<HH>", requests: count };
|
|
186
|
+
if (node.getAttribute('draggable') !== 'true') return { found: true, applied: false, note: "kaynak sürüklenebilir değil (draggable != true) — kanban isDraggable ya da eventcalendar isEditable kapalı olabilir", requests: count };
|
|
187
|
+
var transfer = new DataTransfer();
|
|
188
|
+
var center = function (element) { var rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; };
|
|
189
|
+
var fire = function (element, type, at) { element.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, composed: true, dataTransfer: transfer, clientX: at.x, clientY: at.y })); };
|
|
190
|
+
var from = center(node);
|
|
191
|
+
var to = center(drop);
|
|
192
|
+
fire(node, 'dragstart', from);
|
|
193
|
+
fire(drop, 'dragenter', to);
|
|
194
|
+
fire(drop, 'dragover', to);
|
|
195
|
+
fire(drop, 'drop', to);
|
|
196
|
+
fire(node, 'dragend', to);
|
|
197
|
+
return { found: true, applied: true, requests: count };
|
|
198
|
+
})()`;
|
|
199
|
+
}
|
|
200
|
+
const target = String(action.target ?? '');
|
|
201
|
+
const root = rootElementId(target);
|
|
202
|
+
// G-14 — `applied` yalnız "DOM'a yazıldı" demekti: tarih seçici olayı dinlemiyordu, kutu dolu
|
|
203
|
+
// görünüyor, prop boş kalıyordu. Kökün prop parmak izi eylemden ÖNCE alınır; runActions
|
|
204
|
+
// yerleşmeden sonra yeniden okuyup `propWritten`'ı çıkarır. Çok alanlı bileşenin kökünde tek
|
|
205
|
+
// değer yoktur — ilk kutuya yazmak yarım değeri "uygulandı" diye raporluyordu, kök reddedilir.
|
|
36
206
|
return `(function () {
|
|
37
207
|
var count = performance.getEntriesByType('resource').length;
|
|
38
208
|
var node = document.querySelector(${selector});
|
|
39
209
|
if (!node) return { found: false, applied: false, requests: count };
|
|
40
210
|
var wanted = String(${value});
|
|
211
|
+
var propsBefore = ${runtimePropsScript(root)};
|
|
212
|
+
var finish = function (result) { if (propsBefore !== undefined) result.propsBefore = propsBefore; return result; };
|
|
213
|
+
|
|
214
|
+
if (${JSON.stringify(root === target)}) {
|
|
215
|
+
var parts = Array.prototype.slice.call(node.querySelectorAll(${JSON.stringify(`[data-element-id^="${root}${INTERNAL_ROLE_SEPARATOR}"]`)}))
|
|
216
|
+
.filter(function (part) { return part.querySelector('input, select, textarea'); })
|
|
217
|
+
.map(function (part) { return part.getAttribute('data-element-id'); });
|
|
218
|
+
if (parts.length > 1) return finish({ found: true, applied: false, note: "çok alanlı bileşen: kökte tek değer yok, değeri kutulardan birine yazın — " + parts.join(' · '), requests: count });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak hiçbir şey
|
|
222
|
+
// yapmaz, seçeneğe TIKLANMASI gerekir. 'set' bu noktada 'select' fiiliyle AYNI yola girer —
|
|
223
|
+
// iki kopya seçim mantığı tutulmaz.
|
|
41
224
|
|
|
42
|
-
// Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak
|
|
43
|
-
// hiçbir şey yapmaz, seçeneğe TIKLANMASI gerekir. Liste ancak kutu açıkken
|
|
44
|
-
// basılır, o yüzden önce kutu açılır, sonra data-value ile seçenek bulunur
|
|
45
|
-
// (bulunamazsa görünen metinle eşlenir).
|
|
46
225
|
var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
|
|
47
226
|
if (combobox) {
|
|
227
|
+
// Liste BU kutunun kabının içindedir. Sorgular belge kapsamlı kaldığında açık duran başka
|
|
228
|
+
// bir kutunun (multiselect seçimden sonra kapanmıyor) seçeneğine tıklanabiliyordu ve adım
|
|
229
|
+
// "uygulandı" diyordu; komşunun yükleme göstergesi de bu kutunun kararını blokluyordu (B-5).
|
|
230
|
+
var scope = combobox.closest('.ng-select, .ng-multiselect') || node;
|
|
231
|
+
|
|
48
232
|
combobox.click();
|
|
49
233
|
|
|
50
|
-
// Liste tıklamadan SONRA basılır; aynı tick'te sorgulamak boş sonuç verir.
|
|
51
|
-
// evaluate döndürülen promise'i bekler, o yüzden seçim bir sonraki tur yapılır.
|
|
52
234
|
return new Promise(function (resolve) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
235
|
+
var waited = 0;
|
|
236
|
+
var step = 100;
|
|
237
|
+
var limit = 3000;
|
|
238
|
+
|
|
239
|
+
var attempt = function () {
|
|
240
|
+
var loading = scope.querySelector('.ng-select__loading, .ng-multiselect__loading');
|
|
241
|
+
var options = Array.prototype.slice.call(scope.querySelectorAll('[role="treeitem"][data-value]'));
|
|
242
|
+
var byValue = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0];
|
|
243
|
+
var byText = options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
|
|
244
|
+
var match = byValue || byText;
|
|
245
|
+
|
|
246
|
+
if (match) {
|
|
247
|
+
var button = match.querySelector('button');
|
|
248
|
+
// Seçilemeyen seçeneğe tıklamak hiçbir şey yapmaz; "uygulandı" demek yanlış olurdu (B-6).
|
|
249
|
+
if (match.getAttribute('aria-disabled') === 'true' || (button && button.disabled)) {
|
|
250
|
+
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: "seçenek devre dışı (aria-disabled): " + wanted }));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
(button || match).click();
|
|
255
|
+
resolve(finish({ found: true, applied: true, matchedBy: byValue ? 'value' : 'text', optionsSeen: options.length, waitedMs: waited, requests: count }));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var settled = !loading && (options.length > 0 || !!scope.querySelector('.ng-select__empty, .ng-multiselect__empty'));
|
|
260
|
+
|
|
261
|
+
if (settled || waited >= limit) {
|
|
262
|
+
var seen = options.slice(0, 10).map(function (option) { return (option.textContent || '').trim(); }).join(' · ');
|
|
263
|
+
// Liste HİÇ açılmadıysa kusur sorguda değil kutunun kendisinde olabilir:
|
|
264
|
+
// handleContainerClick kilitli/salt-okunur/yükleniyor kutuda erken döner.
|
|
265
|
+
var locked = !scope.querySelector('.ng-select__list, .ng-multiselect__list');
|
|
266
|
+
var note = options.length > 0
|
|
267
|
+
? "açılır kutuda bu değere karşılık gelen seçenek yok: " + wanted + " — görülen " + options.length + " seçenek: " + seen
|
|
268
|
+
: locked
|
|
269
|
+
? "açılır kutu AÇILMADI (" + waited + " ms): kutu devre dışı, salt okunur ya da hâlâ yükleniyor olabilir — page_runtime_get ile isDisabled/isReadonly/isLoading prop'larına bakın"
|
|
270
|
+
: "açılır kutu açıldı ama " + waited + " ms içinde hiç seçenek basılmadı — seçenekler bir sorgudan geliyorsa query_preview ile doğrulayın, yavaşsa önce bir wait adımı ekleyin";
|
|
271
|
+
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: note }));
|
|
59
272
|
return;
|
|
60
273
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
274
|
+
|
|
275
|
+
waited += step;
|
|
276
|
+
setTimeout(attempt, step);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
attempt();
|
|
64
280
|
});
|
|
65
281
|
}
|
|
66
282
|
|
|
67
283
|
var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
|
|
68
|
-
if (!field) return { found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count };
|
|
284
|
+
if (!field) return finish({ found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count });
|
|
69
285
|
var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
|
|
70
286
|
: (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
|
|
71
287
|
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
72
288
|
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
73
289
|
if (field.checked !== shouldCheck) field.click();
|
|
74
|
-
return { found: true, applied: true, requests: count };
|
|
290
|
+
return finish({ found: true, applied: true, requests: count });
|
|
75
291
|
}
|
|
76
292
|
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
77
293
|
setter.call(field, ${value});
|
|
78
294
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
79
295
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
80
|
-
return { found: true, applied: true, requests: count };
|
|
296
|
+
return finish({ found: true, applied: true, requests: count });
|
|
297
|
+
})()`;
|
|
298
|
+
}
|
|
299
|
+
/** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */
|
|
300
|
+
export function rootElementId(target) {
|
|
301
|
+
const separator = target.indexOf(INTERNAL_ROLE_SEPARATOR);
|
|
302
|
+
return separator > 0 ? target.slice(0, separator) : target;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Düğümün runtime prop'larının parmak izi (`@ng-apps/flow` → flow-runtime-props.ts,
|
|
306
|
+
* `window.__ngAppsReadProps`). `undefined` okuyucu yayınlanmıyor (eski runtime) ya da prop'lar
|
|
307
|
+
* serileşmiyor; `null` hiçbir makine düğümü tanımıyor; dize karşılaştırılabilir iz.
|
|
308
|
+
*/
|
|
309
|
+
export function runtimePropsScript(elementId) {
|
|
310
|
+
return `(function () { /* runtimePropsSnapshot */
|
|
311
|
+
var read = window.__ngAppsReadProps;
|
|
312
|
+
if (typeof read !== 'function') return undefined;
|
|
313
|
+
try { var props = read(${JSON.stringify(elementId)}); return props === null ? null : JSON.stringify(props); } catch (error) { return undefined; }
|
|
314
|
+
})()`;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Hangi sekme AÇIK? `tab` adımı tıklamanın işe yarayıp yaramadığını ancak bununla kanıtlar:
|
|
318
|
+
* tıklama "uygulandı" dese de bileşen seçimi kendi `selected` prop'una yazmamış olabilir.
|
|
319
|
+
* `undefined` düğüm yok, `null` sekme grubu var ama hiçbiri seçili değil.
|
|
320
|
+
*/
|
|
321
|
+
export function activeTabScript(elementId) {
|
|
322
|
+
return `(function () { /* activeTabSnapshot */
|
|
323
|
+
var root = document.querySelector(${JSON.stringify(`[data-element-id="${elementId}"]`)});
|
|
324
|
+
if (!root) return undefined;
|
|
325
|
+
var active = root.querySelector('[role="tab"][aria-selected="true"]');
|
|
326
|
+
return active ? String(active.textContent || '').replace(/\\s+/g, ' ').trim() : null;
|
|
81
327
|
})()`;
|
|
82
328
|
}
|
|
83
329
|
/**
|
|
@@ -100,6 +346,123 @@ export const contentHeightScript = [
|
|
|
100
346
|
' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
|
|
101
347
|
'})()'
|
|
102
348
|
].join('\n');
|
|
349
|
+
/**
|
|
350
|
+
* Kırpma kutusu CSS pikselinde okunur; kare çekilirken Chromium onu `scale` ile çarpar
|
|
351
|
+
* (ölçüldü 2026-09-16: viewport 1280x800 · scale 0.5 · clip 300x100 → 150x50 raster).
|
|
352
|
+
* `null` düğüm yok demektir — tool bunu HATAYA çevirir, boş kare döndürmez.
|
|
353
|
+
*/
|
|
354
|
+
export function clipRectScript(elementId) {
|
|
355
|
+
return `(function () { /* clipRect */
|
|
356
|
+
var node = document.querySelector(${JSON.stringify(`[data-element-id="${elementId}"]`)});
|
|
357
|
+
if (!node) return null;
|
|
358
|
+
var rect = node.getBoundingClientRect();
|
|
359
|
+
var x = rect.x + (window.scrollX || 0);
|
|
360
|
+
var y = rect.y + (window.scrollY || 0);
|
|
361
|
+
// Negatif kenar kırpılırken BOY da kısalır; yoksa kutu aşağı kayar ve kare düğümün altını gösterir.
|
|
362
|
+
return { x: Math.max(x, 0), y: Math.max(y, 0), width: rect.width + Math.min(x, 0), height: rect.height + Math.min(y, 0) };
|
|
363
|
+
})()`;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Karenin raster boyu başlıktan okunur: PNG'de IHDR, JPEG'de ilk SOF işaretçisi. Ölçeğin
|
|
367
|
+
* gerçekten uygulandığı yanıtta SAYIYLA görünsün diye var — "yarı çözünürlük istedim" ile
|
|
368
|
+
* "yarı çözünürlük aldım" arasındaki farkı başka hiçbir alan söylemiyor.
|
|
369
|
+
*/
|
|
370
|
+
export function imageSize(buffer) {
|
|
371
|
+
if (buffer.length > 24 && buffer.subarray(1, 4).toString() === 'PNG') {
|
|
372
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
373
|
+
}
|
|
374
|
+
if (buffer.length > 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
375
|
+
let offset = 2;
|
|
376
|
+
while (offset + 9 < buffer.length) {
|
|
377
|
+
if (buffer[offset] !== 0xff) {
|
|
378
|
+
offset += 1;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const marker = buffer[offset + 1] ?? 0;
|
|
382
|
+
const length = buffer.readUInt16BE(offset + 2);
|
|
383
|
+
// SOF0-SOF3 · SOF5-SOF7 · SOF9-SOF11 · SOF13-SOF15 boyutu taşır; DHT/DRI gibi işaretçiler atlanır.
|
|
384
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
385
|
+
return { height: buffer.readUInt16BE(offset + 5), width: buffer.readUInt16BE(offset + 7) };
|
|
386
|
+
}
|
|
387
|
+
offset += 2 + length;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* NE GÖRÜNÜYOR — resimsiz (S-5). Aynı sayfa yüklemesinden çıkan üçüncü çıktı: her KÖK runtime
|
|
394
|
+
* düğümünün görünürlüğü, metni, satır sayısı, açık sekmesi, değeri ve hata metinleri.
|
|
395
|
+
*
|
|
396
|
+
* Kök düğüm = kimliğinde `__` OLMAYAN düğüm; iç roller (`tbl__row-3`) satır sayısına dönüşür,
|
|
397
|
+
* listeye girmez. Tavan 300 düğüm, metin 80 karakter (K3) — özet bir kareyi değiştirmeli, onun
|
|
398
|
+
* kadar pahalı olmamalı. Tip `data-component-type`'tan gelir (frontend f-02); runtime basmıyorsa
|
|
399
|
+
* alan YAZILMAZ ve `typed: false` döner — `layoutReportUnavailable` ile aynı dürüstlük kuralı.
|
|
400
|
+
*/
|
|
401
|
+
export function domSummaryScript(viewportHeight) {
|
|
402
|
+
return `(function () { /* domSummary */
|
|
403
|
+
var MAX = 300;
|
|
404
|
+
var CUT = 80;
|
|
405
|
+
// Görünür alanın GERÇEK boyu: fullPage ve clip yolları viewport'u içerik boyuna büyütür ve
|
|
406
|
+
// o anda window.innerHeight = içerik yüksekliği olur — her düğüm "ekran içinde" görünürdü ve
|
|
407
|
+
// "kart katlanmanın altında mı" sorusu HER ZAMAN 'visible' cevabı alırdı (B-10).
|
|
408
|
+
var VH = ${Math.max(Math.round(viewportHeight), 1)};
|
|
409
|
+
var clean = function (value) { return String(value || '').replace(/\\s+/g, ' ').trim(); };
|
|
410
|
+
var ROOT_SELECTOR = '[data-element-id]:not([data-element-id*="__"])';
|
|
411
|
+
var all = Array.prototype.slice.call(document.querySelectorAll('[data-element-id]'));
|
|
412
|
+
var roots = all.filter(function (node) { return String(node.getAttribute('data-element-id') || '').indexOf('__') < 0; });
|
|
413
|
+
var typed = false;
|
|
414
|
+
var nodes = roots.slice(0, MAX).map(function (node) {
|
|
415
|
+
var id = node.getAttribute('data-element-id');
|
|
416
|
+
var type = node.getAttribute('data-component-type');
|
|
417
|
+
if (type) typed = true;
|
|
418
|
+
// Bir olgu, ONU TAŞIYAN düğüme yazılır: en yakın kök ata bu düğüm değilse o olgu
|
|
419
|
+
// çocuğundur ve orada zaten listelenecektir (B-8). Aksi hâlde aynı değer üç satırda çıkar.
|
|
420
|
+
var owned = function (element) { return !!element && element.closest(ROOT_SELECTOR) === node; };
|
|
421
|
+
var rect = node.getBoundingClientRect();
|
|
422
|
+
var visible = typeof node.checkVisibility === 'function'
|
|
423
|
+
? !!node.checkVisibility({ checkVisibilityCSS: true, checkOpacity: true })
|
|
424
|
+
: (rect.width > 0 && rect.height > 0);
|
|
425
|
+
var inViewport = rect.bottom > 0 && rect.right > 0 && rect.top < VH && rect.left < (window.innerWidth || 0);
|
|
426
|
+
// Kap düğümün innerText'i ÇOCUKLARININ metnidir; metin yalnız YAPRAK köklerde yazılır.
|
|
427
|
+
var nested = Array.prototype.slice.call(node.querySelectorAll('[data-element-id]'))
|
|
428
|
+
.some(function (child) { return String(child.getAttribute('data-element-id') || '').indexOf('__') < 0; });
|
|
429
|
+
var text = nested ? '' : clean(node.innerText);
|
|
430
|
+
if (text.length > CUT) text = text.slice(0, CUT) + '…';
|
|
431
|
+
var rows = document.querySelectorAll('[data-element-id^="' + id + '__row-"]').length;
|
|
432
|
+
var tab = node.querySelector('[role="tab"][aria-selected="true"]');
|
|
433
|
+
var field = node.querySelector('input, select, textarea');
|
|
434
|
+
var chosen = node.querySelector('.selected-item');
|
|
435
|
+
var holder = field || chosen;
|
|
436
|
+
var value = !owned(holder) ? '' : (field ? (field.type === 'password' ? '***' : clean(field.value)) : clean(chosen.textContent));
|
|
437
|
+
var media = node.querySelector('img, svg, canvas, video, iframe');
|
|
438
|
+
// İç içe <li> hem grubu hem alt mesajları sayıyordu: tek grup + 2 alt hata 3 görünüyordu (B-7).
|
|
439
|
+
var errorItems = Array.prototype.slice.call(node.querySelectorAll('.form-validation-errors li:not(:has(li))'))
|
|
440
|
+
.filter(owned).map(function (item) { return clean(item.textContent); });
|
|
441
|
+
// aria-invalid hem köke hem input'a yazılabiliyor (Password.svelte); yalnız ALANLAR sayılır.
|
|
442
|
+
var invalid = Array.prototype.slice.call(node.querySelectorAll('input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"]')).filter(owned).length;
|
|
443
|
+
var summary = { elementId: id, visible: !!visible, inViewport: !!inViewport };
|
|
444
|
+
if (type) summary.type = type;
|
|
445
|
+
if (text) summary.text = text;
|
|
446
|
+
if (rows > 0) summary.rows = rows;
|
|
447
|
+
if (owned(tab)) summary.activeTab = clean(tab.textContent);
|
|
448
|
+
if (value) summary.value = value;
|
|
449
|
+
if (visible && !text && !media && !nested) summary.empty = true;
|
|
450
|
+
if (errorItems.length > 0) summary.errors = errorItems;
|
|
451
|
+
else if (invalid > 0) summary.errors = ['aria-invalid: ' + invalid];
|
|
452
|
+
return summary;
|
|
453
|
+
});
|
|
454
|
+
var messages = [];
|
|
455
|
+
Array.prototype.slice.call(document.querySelectorAll('.ng-toast')).forEach(function (toast) {
|
|
456
|
+
var name = String(toast.className || '');
|
|
457
|
+
var kind = name.indexOf('ng-toast--error') >= 0 ? 'toast/error' : (name.indexOf('ng-toast--success') >= 0 ? 'toast/success' : 'toast');
|
|
458
|
+
messages.push({ kind: kind, text: clean(toast.innerText).slice(0, 160) });
|
|
459
|
+
});
|
|
460
|
+
Array.prototype.slice.call(document.querySelectorAll('.view-detail__error, .error-page')).forEach(function (node) {
|
|
461
|
+
messages.push({ kind: 'error', text: clean(node.innerText).slice(0, 160) });
|
|
462
|
+
});
|
|
463
|
+
return { nodes: nodes, messages: messages, truncated: roots.length > MAX, typed: typed };
|
|
464
|
+
})()`;
|
|
465
|
+
}
|
|
103
466
|
/**
|
|
104
467
|
* Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
|
|
105
468
|
* çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
|
|
@@ -201,6 +564,8 @@ const redirectQuietPolls = 12;
|
|
|
201
564
|
const idleCloseMs = 60000;
|
|
202
565
|
/** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
|
|
203
566
|
const maxActionWaitMs = 15000;
|
|
567
|
+
/** Viewport büyütmesinden sonra yeniden yerleşme için tanınan üst süre (B-4). */
|
|
568
|
+
const resizeSettleBudgetMs = 3000;
|
|
204
569
|
/**
|
|
205
570
|
* Navigasyon zaman aşımında toplam deneme sayısı. Soğuk Studio'da ilk `page.goto` Vite
|
|
206
571
|
* derlemesini ISITIR; ikinci deneme neredeyse her zaman geçer (09-08 S-8: aynı kare üç
|
|
@@ -256,6 +621,22 @@ async function measureContentHeight(page) {
|
|
|
256
621
|
const height = measured?.contentHeight;
|
|
257
622
|
return typeof height === 'number' && Number.isFinite(height) && height > 0 ? Math.ceil(height) : undefined;
|
|
258
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* Kırpma kutusunu çözer. Bulunamayan ya da çizilmemiş düğüm BOŞ KARE değil HATA döndürür:
|
|
626
|
+
* "yan sütunu kırptım" deyip bomboş bir PNG almak sessiz yanlıştır.
|
|
627
|
+
*/
|
|
628
|
+
async function resolveClip(page, clip) {
|
|
629
|
+
if (!('element' in clip))
|
|
630
|
+
return clip;
|
|
631
|
+
const raw = await page.evaluate(clipRectScript(clip.element)).catch(() => null);
|
|
632
|
+
if (raw === null || raw === undefined) {
|
|
633
|
+
throw new PlatformApiError(`page_screenshot: clip.element '${clip.element}' sayfada yok — kırpılacak kutu bulunamadı. elementId'leri page_schema_get ile doğrulayın; bir komponentin İÇ kontrolünü kırpıyorsanız <elementId>__<rol> biçimini kullanın.`);
|
|
634
|
+
}
|
|
635
|
+
if (!(raw.width > 0 && raw.height > 0)) {
|
|
636
|
+
throw new PlatformApiError(`page_screenshot: clip.element '${clip.element}' ÇİZİLMEMİŞ (${Math.round(raw.width)}x${Math.round(raw.height)} px) — kırpma boş kare üretirdi. Düğüm gizli ya da sıfır boyutlu olabilir; layoutReport: true ile kutusuna bakın.`);
|
|
637
|
+
}
|
|
638
|
+
return { x: Math.round(raw.x), y: Math.round(raw.y), width: Math.round(raw.width), height: Math.round(raw.height) };
|
|
639
|
+
}
|
|
259
640
|
/**
|
|
260
641
|
* Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
|
|
261
642
|
* 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
|
|
@@ -428,19 +809,41 @@ export class StudioScreenshotter {
|
|
|
428
809
|
: navigated
|
|
429
810
|
? activityAfter.completed
|
|
430
811
|
: activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
|
|
812
|
+
// `tab` adımının KANITI tıklama değil, sonuçta hangi sekmenin açık olduğudur: bileşen
|
|
813
|
+
// seçimi kendi prop'una yazmadıysa kare hâlâ eski sekmeyi gösterir ve rapor bunu söyler.
|
|
814
|
+
const activeTab = action.action === 'tab' && typeof action.target === 'string'
|
|
815
|
+
? await page.evaluate(activeTabScript(action.target)).catch(() => undefined)
|
|
816
|
+
: undefined;
|
|
817
|
+
// G-14 — DOM'a yazmak değerin prop'a ulaştığını kanıtlamaz. İz yalnız okuyucu yayınlanıyor ve
|
|
818
|
+
// makine düğümü tanıyorsa (dize) karşılaştırılır; aksi hâlde propWritten bilinmez kalır.
|
|
819
|
+
const propsAfter = typeof before.propsBefore === 'string' && before.applied === true && !navigated
|
|
820
|
+
? await page.evaluate(runtimePropsScript(rootElementId(String(action.target ?? '')))).catch(() => undefined)
|
|
821
|
+
: undefined;
|
|
822
|
+
const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
|
|
823
|
+
const applied = before.applied === true && propWritten !== false;
|
|
824
|
+
const note = propWritten === false
|
|
825
|
+
? `değer DOM'a yazıldı ama '${rootElementId(String(action.target ?? ''))}' düğümünün runtime prop'ları DEĞİŞMEDİ — bileşen olayı kendi change yoluna çevirmedi ya da değer zaten aynıydı. Çok alanlı bileşende <elementId>__<rol> kutusunu hedefleyin (component_get → internalRoles).`
|
|
826
|
+
: before.note;
|
|
431
827
|
outcomes.push({
|
|
432
828
|
index,
|
|
433
829
|
action: String(action.action),
|
|
434
830
|
...(action.target === undefined ? {} : { target: action.target }),
|
|
831
|
+
...(action.to === undefined ? {} : { to: action.to }),
|
|
435
832
|
found: before.found === true,
|
|
436
|
-
applied
|
|
437
|
-
...(
|
|
833
|
+
applied,
|
|
834
|
+
...(propWritten === undefined ? {} : { propWritten }),
|
|
835
|
+
...(note === undefined ? {} : { note }),
|
|
438
836
|
requests,
|
|
439
837
|
...(flowRuns === undefined ? {} : { flowRuns }),
|
|
838
|
+
...(before.matchedBy === undefined ? {} : { matchedBy: before.matchedBy }),
|
|
839
|
+
...(typeof activeTab === 'string' ? { activeTab } : {}),
|
|
840
|
+
...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
|
|
841
|
+
...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
|
|
842
|
+
...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
|
|
440
843
|
pageState: readiness.state
|
|
441
844
|
});
|
|
442
845
|
}
|
|
443
|
-
logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
|
|
846
|
+
logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''}${outcome.to ? ` ⇒ '${outcome.to}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
|
|
444
847
|
return outcomes;
|
|
445
848
|
}
|
|
446
849
|
/**
|
|
@@ -491,6 +894,18 @@ export class StudioScreenshotter {
|
|
|
491
894
|
}
|
|
492
895
|
return { state: 'not-ready', waitedMs: Date.now() - startedAt, ...(last === undefined ? {} : { snapshot: last }) };
|
|
493
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* Viewport büyütmesinden sonra sayfanın YENİDEN YERLEŞMESİNİ bekler. Eski sırada (ölç → büyüt →
|
|
899
|
+
* eylemler) büyütme sonrası render'ı her eylemin kendi hazırlık döngüsü soğuruyordu; sıra
|
|
900
|
+
* eylemlerden sonraya alınınca geriye yalnız 600 ms kalmıştı ve `onResize`'a bağlı ya da
|
|
901
|
+
* sanallaştırılmış bir tablo o pay içinde yeni satırlarını çizmeyebilirdi — özet ve kare
|
|
902
|
+
* büyütme ÖNCESİ gerçeği anlatırdı (B-4).
|
|
903
|
+
*/
|
|
904
|
+
async settleAfterResize(page, baselineUrl) {
|
|
905
|
+
await page.waitForTimeout(settleMs);
|
|
906
|
+
// Yeni akış beklenmiyor: karar DOM durulmasından çıkar (`ready-no-flows` / `ready-no-counter`).
|
|
907
|
+
await this.waitForReady(page, { budgetMs: resizeSettleBudgetMs, baselineUrl, completedBefore: Number.POSITIVE_INFINITY });
|
|
908
|
+
}
|
|
494
909
|
async captureOne(request) {
|
|
495
910
|
const startedAt = Date.now();
|
|
496
911
|
if (this.idleTimer)
|
|
@@ -502,7 +917,7 @@ export class StudioScreenshotter {
|
|
|
502
917
|
});
|
|
503
918
|
let context;
|
|
504
919
|
try {
|
|
505
|
-
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
|
|
920
|
+
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
|
|
506
921
|
}
|
|
507
922
|
catch {
|
|
508
923
|
this.browserPromise = this.launch();
|
|
@@ -510,7 +925,7 @@ export class StudioScreenshotter {
|
|
|
510
925
|
this.browserPromise = undefined;
|
|
511
926
|
throw error;
|
|
512
927
|
});
|
|
513
|
-
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
|
|
928
|
+
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
|
|
514
929
|
}
|
|
515
930
|
logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}`);
|
|
516
931
|
try {
|
|
@@ -581,27 +996,75 @@ export class StudioScreenshotter {
|
|
|
581
996
|
: readiness.state === 'not-ready' ? false : undefined;
|
|
582
997
|
const finalHref = readiness.snapshot?.href;
|
|
583
998
|
const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
|
|
999
|
+
const actions = await this.runActions(page, request);
|
|
1000
|
+
// Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
|
|
1001
|
+
// boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
|
|
1002
|
+
// tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
|
|
1003
|
+
// (S-1, 2026-09-16 raporu madde 6). Yalnız YÜKSEKLİK büyür, genişlik sabittir: `hideOn`
|
|
1004
|
+
// kırılım noktaları ve duyarlı yerleşim kareyi çekerken kaymaz (K4).
|
|
584
1005
|
const measured = request.fullPage ? await measureContentHeight(page) : undefined;
|
|
585
1006
|
const captureHeight = measured === undefined ? undefined : Math.min(measured, maxCaptureHeight);
|
|
586
|
-
|
|
1007
|
+
let viewportHeight = request.viewport.height;
|
|
1008
|
+
if (captureHeight !== undefined && captureHeight > viewportHeight) {
|
|
587
1009
|
await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
|
|
588
|
-
|
|
1010
|
+
viewportHeight = captureHeight;
|
|
1011
|
+
await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
|
|
1012
|
+
}
|
|
1013
|
+
// K7 — kırpma kutusu katlanmış alanın ALTINDAYSA kare boş çıkardı: viewport kutuyu
|
|
1014
|
+
// kapsayana kadar büyütülür (fullPage ile aynı yol) ve kutu YENİDEN okunur, çünkü
|
|
1015
|
+
// büyüyen viewport yerleşimi kaydırabilir.
|
|
1016
|
+
let clip;
|
|
1017
|
+
if (request.clip) {
|
|
1018
|
+
clip = await resolveClip(page, request.clip);
|
|
1019
|
+
if (clip.y + clip.height > viewportHeight) {
|
|
1020
|
+
const wanted = Math.ceil(clip.y + clip.height) + 8;
|
|
1021
|
+
if (wanted > maxCaptureHeight) {
|
|
1022
|
+
throw new PlatformApiError(`page_screenshot: kırpma kutusu ${Math.round(clip.y)}+${Math.round(clip.height)} px'te bitiyor ve Chromium'un ${maxCaptureHeight} px'lik doku sınırının altına sığmıyor — viewport o kadar büyütülemez ve kare kutunun tamamını gösteremezdi. Kutuyu elle daraltın ({ x, y, width, height }) ya da sayfayı bölerek doğrulayın.`);
|
|
1023
|
+
}
|
|
1024
|
+
const grown = Math.min(wanted, maxCaptureHeight);
|
|
1025
|
+
await page.setViewportSize({ width: request.viewport.width, height: grown });
|
|
1026
|
+
viewportHeight = grown;
|
|
1027
|
+
await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
|
|
1028
|
+
clip = await resolveClip(page, request.clip);
|
|
1029
|
+
}
|
|
589
1030
|
}
|
|
590
|
-
const actions = await this.runActions(page, request);
|
|
591
1031
|
const layout = request.layoutReport === true
|
|
592
1032
|
? await page.evaluate(layoutReportScript).then(value => Array.isArray(value) ? value : []).catch(() => [])
|
|
593
1033
|
: undefined;
|
|
594
1034
|
const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
|
|
595
1035
|
.then(value => Array.isArray(value) ? value : [])
|
|
596
1036
|
.catch(() => []);
|
|
597
|
-
|
|
1037
|
+
// Özet kareden ÖNCE ve büyütmeden SONRA: ikisi de aynı yerleşmiş sayfayı anlatsın.
|
|
1038
|
+
const summaryRaw = request.summary === false
|
|
1039
|
+
? undefined
|
|
1040
|
+
: await page.evaluate(domSummaryScript(request.viewport.height)).then(value => ({ value })).catch(error => ({ error: error instanceof Error ? error.message : String(error) }));
|
|
1041
|
+
const summary = summaryRaw && 'value' in summaryRaw ? parseDomSummary(summaryRaw.value) : undefined;
|
|
1042
|
+
const summaryUnavailable = summaryRaw === undefined
|
|
1043
|
+
? undefined
|
|
1044
|
+
: 'error' in summaryRaw
|
|
1045
|
+
? summaryRaw.error
|
|
1046
|
+
: summary === undefined ? 'tarayıcı özet döndürmedi (beklenmeyen biçim)' : undefined;
|
|
1047
|
+
// Olgu doğrulamasında kare HİÇ çekilmez: maliyetin tamamı buradadır (S-5).
|
|
1048
|
+
const format = request.format ?? 'png';
|
|
1049
|
+
const png = request.image === 'none'
|
|
1050
|
+
? undefined
|
|
1051
|
+
: await page.screenshot({
|
|
1052
|
+
type: format,
|
|
1053
|
+
...(format === 'jpeg' && request.quality !== undefined ? { quality: request.quality } : {}),
|
|
1054
|
+
fullPage: false,
|
|
1055
|
+
...(clip === undefined ? {} : { clip })
|
|
1056
|
+
});
|
|
598
1057
|
const elapsedMs = Date.now() - startedAt;
|
|
599
|
-
logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · hazırlık ${readiness.state} (${readiness.waitedMs} ms) · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}${finalUrl === undefined ? '' : ` · yönlendi → ${finalUrl}`}`);
|
|
1058
|
+
logStep('tarayıcı ←', `${png === undefined ? 'kare YOK (image:none)' : `PNG ${png.byteLength} B`} · ${elapsedMs} ms · hazırlık ${readiness.state} (${readiness.waitedMs} ms) · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}${finalUrl === undefined ? '' : ` · yönlendi → ${finalUrl}`}`);
|
|
600
1059
|
if (runtimeErrors.length > 0) {
|
|
601
1060
|
logStep('akış hatası ✕', `${runtimeErrors.length} akış adımı hata bıraktı — sayfa yine 200 döndü`, runtimeErrors.map(error => `${error.step?.name ?? '?'}${error.element ? ` → ${error.element}.${error.property ?? '?'}` : ''}: ${error.message ?? ''}`));
|
|
602
1061
|
}
|
|
603
1062
|
return {
|
|
604
|
-
png,
|
|
1063
|
+
...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
|
|
1064
|
+
...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
|
|
1065
|
+
...(clip === undefined ? {} : { clip }),
|
|
1066
|
+
...(summary === undefined ? {} : { summary }),
|
|
1067
|
+
...(summaryUnavailable === undefined ? {} : { summaryUnavailable }),
|
|
605
1068
|
apiStatus,
|
|
606
1069
|
renderWaitTimedOut: apiStatus === undefined,
|
|
607
1070
|
elapsedMs,
|