@appaflytech/zeyrek-ai-mcp 0.0.24 → 0.0.26
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/.env.example +0 -15
- package/dist/config.js +0 -11
- package/dist/index.js +0 -2
- package/dist/knowledge/componentCatalog.js +25 -13
- package/dist/knowledge/componentEvents.js +6 -13
- package/dist/knowledge/normalize.js +17 -68
- package/dist/knowledge/rules.js +49 -137
- package/dist/knowledge/styleResolve.js +4 -50
- package/dist/knowledge/themeResolve.js +3 -3
- package/dist/observability/trace.js +1 -1
- package/dist/platform/diagnostics.js +4 -10
- package/dist/platform/domSummary.js +1 -1
- package/dist/platform/http.js +7 -23
- package/dist/platform/responses.js +2 -6
- package/dist/platform/{capture/capture.js → screenshot.js} +514 -52
- package/dist/platform/secrets.js +0 -9
- package/dist/platform/strictArguments.js +12 -68
- package/dist/platform/tokenVault.js +4 -187
- package/dist/resources/examples/workflow-http-step.json +2 -2
- package/dist/resources/guides/component.md +1 -6
- package/dist/resources/guides/expressions.md +2 -2
- package/dist/resources/guides/functions.md +1 -1
- package/dist/resources/guides/layout.md +7 -11
- package/dist/resources/guides/microfunction.md +28 -0
- package/dist/resources/guides/microservice.md +4 -10
- package/dist/resources/guides/middleware.md +1 -5
- package/dist/resources/guides/page.md +55 -215
- package/dist/resources/guides/theme.md +1 -4
- package/dist/resources/guides/translation.md +1 -1
- package/dist/resources/guides/workflow.md +9 -12
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/CreateWorkflowCommand.json +3 -0
- package/dist/resources/schemas/PreviewWorkflowCommand.json +3 -0
- package/dist/resources/schemas/StepDto.json +12 -9
- package/dist/resources/schemas/UpdateWorkflowCommand.json +3 -0
- package/dist/server.js +1 -3
- package/dist/tools/applications.js +3 -60
- package/dist/tools/components.js +10 -50
- package/dist/tools/datasources.js +1 -1
- package/dist/tools/designSchemaPatch.js +6 -16
- package/dist/tools/externalComponents.js +4 -25
- package/dist/tools/flows.js +6 -79
- package/dist/tools/layouts.js +12 -44
- package/dist/tools/microservices.js +10 -26
- package/dist/tools/pages.js +67 -303
- package/dist/tools/platformInfo.js +2 -6
- package/dist/tools/rows.js +1 -7
- package/dist/tools/schemaOps.js +1 -1
- package/dist/tools/screenWrite.js +5 -6
- package/dist/tools/screens.js +3 -90
- package/dist/tools/workflows.js +1 -1
- package/package.json +1 -1
- package/dist/knowledge/mobileComponentCatalog.js +0 -801
- package/dist/knowledge/mobileComponentEvents.js +0 -225
- package/dist/knowledge/platform/lint.js +0 -312
- package/dist/knowledge/platform/mobile.js +0 -78
- package/dist/knowledge/platform/mobileRules.js +0 -182
- package/dist/knowledge/platform/mobileTabs.js +0 -108
- package/dist/knowledge/platform/profile.js +0 -20
- package/dist/knowledge/platform/resolve.js +0 -122
- package/dist/knowledge/platform/web.js +0 -38
- package/dist/knowledge/styleResolveNative.js +0 -510
- package/dist/platform/argumentAliases.js +0 -128
- package/dist/platform/bodyFile.js +0 -103
- package/dist/platform/capture/actions.js +0 -333
- package/dist/platform/capture/browser.js +0 -1
- package/dist/platform/capture/console-log.js +0 -80
- package/dist/platform/capture/index.js +0 -11
- package/dist/platform/capture/readiness.js +0 -103
- package/dist/platform/capture/summary.js +0 -77
- package/dist/platform/capture/target.js +0 -5
- package/dist/platform/capture/targets/studio-mobile.js +0 -15
- package/dist/platform/capture/targets/studio-web.js +0 -5
- package/dist/platform/schemaReadMemory.js +0 -60
- package/dist/resources/examples/mobile-app-shell-drawer.json +0 -209
- package/dist/resources/examples/mobile-app-shell-stack.json +0 -123
- package/dist/resources/examples/mobile-app-shell-tab.json +0 -174
- package/dist/resources/examples/mobile-list-detail-form.json +0 -656
- package/dist/resources/guides/mobile.md +0 -419
- package/dist/tools/mobile.js +0 -203
- package/dist/tools/profileWrite.js +0 -45
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { PlatformApiError } from '
|
|
2
|
-
import { parseDomSummary } from '
|
|
3
|
-
import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '
|
|
4
|
-
import { logStep } from '
|
|
5
|
-
import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
|
|
6
|
-
import { domSummaryScript } from './summary.js';
|
|
7
|
-
import { createPageConsoleLog, describePageFailure, parseRenderErrors, renderErrorsScript } from './console-log.js';
|
|
8
|
-
import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
|
|
1
|
+
import { PlatformApiError } from './errors.js';
|
|
2
|
+
import { parseDomSummary } from './domSummary.js';
|
|
3
|
+
import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../knowledge/componentCatalog.js';
|
|
4
|
+
import { logStep } from '../observability/trace.js';
|
|
9
5
|
/**
|
|
10
6
|
* Ziyaretçinin tema tercihinin yaşadığı localStorage anahtarı — `theme-toggle` bunu yazar,
|
|
11
7
|
* `Theme` okur ve projenin kendi modunun önüne geçirir
|
|
@@ -13,17 +9,328 @@ import { decideReadiness, describeUnready, parseReadiness, readinessScript, runt
|
|
|
13
9
|
* DEĞİL tarayıcı başına tektir.
|
|
14
10
|
*/
|
|
15
11
|
const VIEWER_MODE_STORAGE_KEY = 'zeyrek-viewer-theme-mode';
|
|
12
|
+
/**
|
|
13
|
+
* G-24 — gateway sayfayı kurabiliyor, okuyabiliyor ve çizdirebiliyordu ama üzerinde OLAY
|
|
14
|
+
* TETİKLEYEMİYORDU: "filtre değişince veri yenileniyor" iddiası yalnız şema okumasıyla
|
|
15
|
+
* kanıtlanabiliyordu. 09-05 koşusunda üç olay bağının ikisi şemada "bağlı" görünüp gerçekte
|
|
16
|
+
* çalışmıyordu (form submit hiç doğmuyordu, `code` adımının gövdesi kaydedilmemişti) ve ikisi de
|
|
17
|
+
* yalnız gerçek tıklamayla görüldü.
|
|
18
|
+
*
|
|
19
|
+
* Değer yazarken NATIVE setter kullanılır ve `input`+`change` olayları bubbles ile atılır: DOM
|
|
20
|
+
* value'sunu düz atamak framework'ün duyduğu olayı üretmez, alan dolu görünür ama akış çalışmaz.
|
|
21
|
+
* `resource` sayacı "istek gerçekten gitti mi" sorusunu cevaplar — koşuda kusuru kesinleştiren
|
|
22
|
+
* gözlem tam olarak buydu (ağ kaydında hiç istek yoktu).
|
|
23
|
+
*/
|
|
24
|
+
export function actionScript(action) {
|
|
25
|
+
const selector = JSON.stringify(`[data-element-id="${String(action.target ?? '')}"]`);
|
|
26
|
+
const value = JSON.stringify(action.value ?? '');
|
|
27
|
+
if (action.action === 'click') {
|
|
28
|
+
return `(function () {
|
|
29
|
+
var count = performance.getEntriesByType('resource').length;
|
|
30
|
+
var node = document.querySelector(${selector});
|
|
31
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
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
|
+
}
|
|
38
|
+
target.click();
|
|
39
|
+
return { found: true, applied: true, requests: count };
|
|
40
|
+
})()`;
|
|
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.
|
|
206
|
+
return `(function () {
|
|
207
|
+
var count = performance.getEntriesByType('resource').length;
|
|
208
|
+
var node = document.querySelector(${selector});
|
|
209
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
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.
|
|
224
|
+
|
|
225
|
+
var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
|
|
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
|
+
|
|
232
|
+
combobox.click();
|
|
233
|
+
|
|
234
|
+
return new Promise(function (resolve) {
|
|
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 }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
waited += step;
|
|
276
|
+
setTimeout(attempt, step);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
attempt();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
|
|
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 });
|
|
285
|
+
var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
|
|
286
|
+
: (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
|
|
287
|
+
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
288
|
+
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
289
|
+
if (field.checked !== shouldCheck) field.click();
|
|
290
|
+
return finish({ found: true, applied: true, requests: count });
|
|
291
|
+
}
|
|
292
|
+
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
293
|
+
setter.call(field, ${value});
|
|
294
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
295
|
+
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
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;
|
|
327
|
+
})()`;
|
|
328
|
+
}
|
|
16
329
|
/**
|
|
17
330
|
* fullPage karesi Chromium'un viewport ötesi yakalamasıyla değil, viewport'u içerik boyuna
|
|
18
331
|
* büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
|
|
19
332
|
* ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
|
|
20
333
|
* Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
|
|
21
|
-
*
|
|
22
|
-
* İç kaydırıcı için gereken viewport `innerHeight + (scrollHeight − clientHeight)`'tir, kabın
|
|
23
|
-
* `scrollHeight`'i DEĞİL: kap viewport'la birlikte büyür ama üstündeki/altındaki sabit parçalar
|
|
24
|
-
* (mobil host'ta app bar + tab bar, ZM-23 f-06) büyümez. Yalnız `scrollHeight` itilince viewport
|
|
25
|
-
* içerik boyuna çıkıyor, kap hâlâ o parçalar kadar kısa kalıyor ve uzun mobil sayfanın altı
|
|
26
|
-
* karenin dışında kalıyordu. Kap viewport kadar yüksekse iki formül aynı sonucu verir.
|
|
27
334
|
*/
|
|
28
335
|
export const contentHeightScript = [
|
|
29
336
|
'(function () {',
|
|
@@ -34,7 +341,7 @@ export const contentHeightScript = [
|
|
|
34
341
|
' var nodes = document.querySelectorAll("*");',
|
|
35
342
|
' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
|
|
36
343
|
' var node = nodes[index];',
|
|
37
|
-
' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(
|
|
344
|
+
' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(node.scrollHeight);',
|
|
38
345
|
' }',
|
|
39
346
|
' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
|
|
40
347
|
'})()'
|
|
@@ -82,6 +389,80 @@ export function imageSize(buffer) {
|
|
|
82
389
|
}
|
|
83
390
|
return undefined;
|
|
84
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
|
+
}
|
|
85
466
|
/**
|
|
86
467
|
* Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
|
|
87
468
|
* çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
|
|
@@ -98,10 +479,6 @@ export const layoutReportScript = [
|
|
|
98
479
|
' };',
|
|
99
480
|
'})'
|
|
100
481
|
].join('\n');
|
|
101
|
-
/** Mobil çizicinin hata sınırı işaretleri; betik düşerse boş (çekimi düşürmez). */
|
|
102
|
-
async function readRenderErrors(page) {
|
|
103
|
-
return page.evaluate(renderErrorsScript).then(parseRenderErrors).catch(() => []);
|
|
104
|
-
}
|
|
105
482
|
/**
|
|
106
483
|
* F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
|
|
107
484
|
* patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
|
|
@@ -110,6 +487,67 @@ async function readRenderErrors(page) {
|
|
|
110
487
|
* flow-runtime-errors.ts); kare çekilirken bedavaya okunur.
|
|
111
488
|
*/
|
|
112
489
|
export const flowRuntimeErrorsScript = 'Array.isArray(window.__ngAppsFlowRuntimeErrors) ? window.__ngAppsFlowRuntimeErrors.slice(0, 50) : []';
|
|
490
|
+
/**
|
|
491
|
+
* Kare ne zaman hazır? (G-30 → B-6, 2026-09-07)
|
|
492
|
+
*
|
|
493
|
+
* `delayMs` TAHMİNDİ; 09-05'te runtime koşan akış sayacını yayınlamaya başladı (`@ng-apps/flow`
|
|
494
|
+
* → flow-runtime-errors.ts) ve bekleme "running sıfır, completed > 0" olunca bitiyordu. Bu şart
|
|
495
|
+
* mount akışı OLMAYAN sayfada (giriş ekranı) hiç sağlanmaz; akış tetiklemeyen `set` eylemi de
|
|
496
|
+
* `completed`'ı artırmaz. 09-06·2 koşusunda üç eylemli giriş denemesi bu yüzden 47 sn sürdü;
|
|
497
|
+
* sentetik ölçümde sayfa 0.3 sn'de hazırken aynı çağrı 48 sn'ydi — süre sayfanın değil, bu
|
|
498
|
+
* döngünün. Hazırlık artık TEK anlık görüntüden okunur: akış sayacı + Studio'nun yükleme
|
|
499
|
+
* iskeleti (`ViewDetail.svelte` → `.view-detail > .bx--skeleton__placeholder`) + runtime
|
|
500
|
+
* düğümleri (`[data-element-id]`) + hata/bulunamadı hâli + adres. Karar `decideReadiness`'ta.
|
|
501
|
+
*
|
|
502
|
+
* Yüklenmemiş kare ASLA dönmez: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla düşer
|
|
503
|
+
* (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
|
|
504
|
+
* neyin görüldüğünü zaten söyler.
|
|
505
|
+
*/
|
|
506
|
+
export const readinessScript = `(function () { /* readinessSnapshot */
|
|
507
|
+
var activity = window.__ngAppsFlowActivity;
|
|
508
|
+
return {
|
|
509
|
+
activity: activity && typeof activity.running === 'number' && typeof activity.completed === 'number' ? { running: activity.running, completed: activity.completed } : null,
|
|
510
|
+
runtimeNodes: document.querySelectorAll('[data-element-id]').length,
|
|
511
|
+
skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
|
|
512
|
+
viewDetail: !!document.querySelector('.view-detail'),
|
|
513
|
+
errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
|
|
514
|
+
readyState: String(document.readyState),
|
|
515
|
+
href: String(location.href)
|
|
516
|
+
};
|
|
517
|
+
})()`;
|
|
518
|
+
/** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
|
|
519
|
+
export function isFlowActivity(activity) {
|
|
520
|
+
if (!activity || typeof activity !== 'object')
|
|
521
|
+
return false;
|
|
522
|
+
const { running, completed } = activity;
|
|
523
|
+
return typeof running === 'number' && typeof completed === 'number';
|
|
524
|
+
}
|
|
525
|
+
/** Tarayıcıdan dönen değer anlık görüntü mü; yönlendirme sırasında evaluate düşer ve undefined kalır. */
|
|
526
|
+
export function parseReadiness(raw) {
|
|
527
|
+
if (!raw || typeof raw !== 'object')
|
|
528
|
+
return undefined;
|
|
529
|
+
const value = raw;
|
|
530
|
+
if (typeof value.runtimeNodes !== 'number' || typeof value.href !== 'string')
|
|
531
|
+
return undefined;
|
|
532
|
+
return {
|
|
533
|
+
activity: isFlowActivity(value.activity) ? { running: value.activity.running, completed: value.activity.completed } : null,
|
|
534
|
+
runtimeNodes: value.runtimeNodes,
|
|
535
|
+
skeleton: value.skeleton === true,
|
|
536
|
+
viewDetail: value.viewDetail === true,
|
|
537
|
+
errorState: value.errorState === true,
|
|
538
|
+
readyState: String(value.readyState ?? ''),
|
|
539
|
+
href: value.href
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
/** `/dev` ve `/view` aynı sayfadır (Studio `hooks.ts` reroute); yönlendirme kararı yalnız yoldan verilir. */
|
|
543
|
+
export function runtimePath(url) {
|
|
544
|
+
try {
|
|
545
|
+
return new URL(url).pathname.replace(/^\/dev\//, '/view/').replace(/\/+$/, '');
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return url;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
113
551
|
const maxCaptureHeight = 16384;
|
|
114
552
|
const navigationTimeoutMs = 20000;
|
|
115
553
|
const apiResponseTimeoutMs = 15000;
|
|
@@ -117,6 +555,12 @@ const settleMs = 600;
|
|
|
117
555
|
/** Akış bittikten sonra Svelte'in son render'ına bırakılan pay. */
|
|
118
556
|
const flowSettleGraceMs = 250;
|
|
119
557
|
const flowPollMs = 120;
|
|
558
|
+
/** "Hiç akış koşmadı" kararı için DOM'un kaç yoklama değişmemesi gerekir (~600 ms = eski settleMs). */
|
|
559
|
+
const quietPolls = 5;
|
|
560
|
+
/** Sayaç yayınlanmayan runtime'da tek kanıt DOM durulmasıdır; daha uzun beklenir (~1 sn). */
|
|
561
|
+
const legacyQuietPolls = 8;
|
|
562
|
+
/** Runtime dışına yönlendirme: hedef sayfanın kendi JS'i çizmeyi bitirsin (~1.5 sn). */
|
|
563
|
+
const redirectQuietPolls = 12;
|
|
120
564
|
const idleCloseMs = 60000;
|
|
121
565
|
/** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
|
|
122
566
|
const maxActionWaitMs = 15000;
|
|
@@ -136,6 +580,42 @@ const navigationAttempts = 2;
|
|
|
136
580
|
function isNavigationTimeout(message) {
|
|
137
581
|
return /timeout/i.test(message);
|
|
138
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Tek anlık görüntüden "kare alınabilir" kararı; iki uçta da yanlış beklememek için tek yerde.
|
|
585
|
+
* `quiet` görüntünün kaç yoklamadır değişmediğidir, `completedBefore` bu beklemenin başındaki
|
|
586
|
+
* bitmiş akış sayısı (sayfa değiştiyse çağıran sıfırlar). İskelet görünüyorsa hiçbir karar yok.
|
|
587
|
+
*/
|
|
588
|
+
export function decideReadiness(snapshot, quiet, completedBefore, baselineUrl) {
|
|
589
|
+
if (snapshot.skeleton)
|
|
590
|
+
return undefined;
|
|
591
|
+
const { activity } = snapshot;
|
|
592
|
+
const idle = activity !== null && activity.running === 0;
|
|
593
|
+
if (idle && activity.completed > completedBefore && snapshot.runtimeNodes > 0)
|
|
594
|
+
return 'ready';
|
|
595
|
+
if (idle && snapshot.runtimeNodes > 0 && quiet >= quietPolls)
|
|
596
|
+
return 'ready-no-flows';
|
|
597
|
+
if (activity === null && snapshot.runtimeNodes > 0 && quiet >= legacyQuietPolls)
|
|
598
|
+
return 'ready-no-counter';
|
|
599
|
+
if (idle && snapshot.errorState && quiet >= quietPolls)
|
|
600
|
+
return 'error';
|
|
601
|
+
if (!snapshot.viewDetail && snapshot.readyState === 'complete' && baselineUrl !== undefined && runtimePath(snapshot.href) !== runtimePath(baselineUrl) && quiet >= redirectQuietPolls)
|
|
602
|
+
return 'redirected';
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
/** Hata mesajı için: bütçe dolduğunda sayfada NE görüldü. */
|
|
606
|
+
export function describeUnready(readiness, apiNote) {
|
|
607
|
+
const snapshot = readiness.snapshot;
|
|
608
|
+
const suffix = apiNote === undefined ? '' : ` · ${apiNote}`;
|
|
609
|
+
if (!snapshot)
|
|
610
|
+
return `tarayıcı sayfayı hiç değerlendiremedi (sürekli yönlendirme ya da bağlam düştü)${suffix}`;
|
|
611
|
+
if (snapshot.skeleton)
|
|
612
|
+
return `sayfa hâlâ yükleme iskeletinde${suffix}`;
|
|
613
|
+
if (snapshot.activity && snapshot.activity.running > 0)
|
|
614
|
+
return `${snapshot.activity.running} akış hâlâ koşuyor (${snapshot.activity.completed} bitti)${suffix}`;
|
|
615
|
+
if (snapshot.runtimeNodes === 0)
|
|
616
|
+
return `runtime hiç render olmadı — data-element-id taşıyan düğüm yok${snapshot.viewDetail ? '' : ', .view-detail kabı da yok'}${suffix}`;
|
|
617
|
+
return `sayfa durulmadı (${snapshot.runtimeNodes} düğüm, sayaç ${snapshot.activity ? `${snapshot.activity.running} koşan / ${snapshot.activity.completed} biten` : 'yok'})${suffix}`;
|
|
618
|
+
}
|
|
139
619
|
async function measureContentHeight(page) {
|
|
140
620
|
const measured = await page.evaluate(contentHeightScript).catch(() => undefined);
|
|
141
621
|
const height = measured?.contentHeight;
|
|
@@ -275,7 +755,7 @@ export class StudioScreenshotter {
|
|
|
275
755
|
* işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
|
|
276
756
|
* yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
|
|
277
757
|
*/
|
|
278
|
-
async runActions(page, request
|
|
758
|
+
async runActions(page, request) {
|
|
279
759
|
if (!request.actions || request.actions.length === 0)
|
|
280
760
|
return undefined;
|
|
281
761
|
const outcomes = [];
|
|
@@ -287,7 +767,7 @@ export class StudioScreenshotter {
|
|
|
287
767
|
}
|
|
288
768
|
const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
289
769
|
const activityBefore = snapshotBefore?.activity ?? null;
|
|
290
|
-
const raw = await page.evaluate(actionScript(action
|
|
770
|
+
const raw = await page.evaluate(actionScript(action)).catch(error => ({
|
|
291
771
|
found: false,
|
|
292
772
|
applied: false,
|
|
293
773
|
note: error instanceof Error ? error.message : String(error),
|
|
@@ -298,11 +778,9 @@ export class StudioScreenshotter {
|
|
|
298
778
|
// iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
|
|
299
779
|
// kendisindedir, şemayı okumaya yönlendirilir (P1-3 Step 3).
|
|
300
780
|
if (before.found === false && !before.note && typeof action.target === 'string') {
|
|
301
|
-
before.note = action.target
|
|
302
|
-
? `'${action.target}' sayfada yok
|
|
303
|
-
: action.target.
|
|
304
|
-
? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Kolon rolleri kolonun SLUG'ından türer (başlığından değil); satır sırası 0 tabanlıdır ve sayfa değişince yeniden numaralanır.`
|
|
305
|
-
: `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
|
|
781
|
+
before.note = action.target.includes(INTERNAL_ROLE_SEPARATOR)
|
|
782
|
+
? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Kolon rolleri kolonun SLUG'ından türer (başlığından değil); satır sırası 0 tabanlıdır ve sayfa değişince yeniden numaralanır.`
|
|
783
|
+
: `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
|
|
306
784
|
}
|
|
307
785
|
// Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
|
|
308
786
|
// sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
|
|
@@ -317,8 +795,7 @@ export class StudioScreenshotter {
|
|
|
317
795
|
completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
|
|
318
796
|
});
|
|
319
797
|
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
320
|
-
|
|
321
|
-
throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}.${failure} Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
798
|
+
throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}. Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
322
799
|
}
|
|
323
800
|
const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
|
|
324
801
|
const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
|
|
@@ -440,7 +917,7 @@ export class StudioScreenshotter {
|
|
|
440
917
|
});
|
|
441
918
|
let context;
|
|
442
919
|
try {
|
|
443
|
-
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor:
|
|
920
|
+
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
|
|
444
921
|
}
|
|
445
922
|
catch {
|
|
446
923
|
this.browserPromise = this.launch();
|
|
@@ -448,7 +925,7 @@ export class StudioScreenshotter {
|
|
|
448
925
|
this.browserPromise = undefined;
|
|
449
926
|
throw error;
|
|
450
927
|
});
|
|
451
|
-
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor:
|
|
928
|
+
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
|
|
452
929
|
}
|
|
453
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}` : ''}`);
|
|
454
931
|
try {
|
|
@@ -471,11 +948,6 @@ export class StudioScreenshotter {
|
|
|
471
948
|
await context.addInitScript(`try { localStorage.setItem(${JSON.stringify(VIEWER_MODE_STORAGE_KEY)}, ${JSON.stringify(request.colorScheme)}) } catch (error) { /* localStorage kapalıysa proje varsayılanı çizilir */ }`);
|
|
472
949
|
}
|
|
473
950
|
const page = await context.newPage();
|
|
474
|
-
// Konsol ve istisna dinleyicileri navigasyondan ÖNCE takılır: modül yüklenirken patlayan
|
|
475
|
-
// bileşenin hatası ilk karede düşer ve sonradan takılan dinleyici onu kaçırırdı (g-06).
|
|
476
|
-
const consoleLog = createPageConsoleLog();
|
|
477
|
-
page.on?.('console', message => consoleLog.onConsole(message));
|
|
478
|
-
page.on?.('pageerror', error => consoleLog.onPageError(error));
|
|
479
951
|
// Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
|
|
480
952
|
// bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
|
|
481
953
|
// yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
|
|
@@ -516,8 +988,7 @@ export class StudioScreenshotter {
|
|
|
516
988
|
const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
|
|
517
989
|
const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
|
|
518
990
|
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
519
|
-
|
|
520
|
-
throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}.${failure} Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
991
|
+
throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}. Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
521
992
|
}
|
|
522
993
|
await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
|
|
523
994
|
const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
|
|
@@ -525,17 +996,14 @@ export class StudioScreenshotter {
|
|
|
525
996
|
: readiness.state === 'not-ready' ? false : undefined;
|
|
526
997
|
const finalHref = readiness.snapshot?.href;
|
|
527
998
|
const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
|
|
528
|
-
const actions = await this.runActions(page, request
|
|
999
|
+
const actions = await this.runActions(page, request);
|
|
529
1000
|
// Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
|
|
530
1001
|
// boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
|
|
531
1002
|
// tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
|
|
532
1003
|
// (S-1, 2026-09-16 raporu madde 6). Yalnız YÜKSEKLİK büyür, genişlik sabittir: `hideOn`
|
|
533
1004
|
// kırılım noktaları ve duyarlı yerleşim kareyi çekerken kaymaz (K4).
|
|
534
|
-
// Doku sınırı RASTER pikseldir: mobil kare @2-3 çekilir, CSS tavanı ölçekle bölünür (web'de
|
|
535
|
-
// ölçek ≤ 1, tavan 16384 kalır).
|
|
536
|
-
const heightLimit = Math.floor(maxCaptureHeight / Math.max(1, (request.dpr ?? 1) * (request.scale ?? 1)));
|
|
537
1005
|
const measured = request.fullPage ? await measureContentHeight(page) : undefined;
|
|
538
|
-
const captureHeight = measured === undefined ? undefined : Math.min(measured,
|
|
1006
|
+
const captureHeight = measured === undefined ? undefined : Math.min(measured, maxCaptureHeight);
|
|
539
1007
|
let viewportHeight = request.viewport.height;
|
|
540
1008
|
if (captureHeight !== undefined && captureHeight > viewportHeight) {
|
|
541
1009
|
await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
|
|
@@ -550,10 +1018,10 @@ export class StudioScreenshotter {
|
|
|
550
1018
|
clip = await resolveClip(page, request.clip);
|
|
551
1019
|
if (clip.y + clip.height > viewportHeight) {
|
|
552
1020
|
const wanted = Math.ceil(clip.y + clip.height) + 8;
|
|
553
|
-
if (wanted >
|
|
554
|
-
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
|
|
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.`);
|
|
555
1023
|
}
|
|
556
|
-
const grown = Math.min(wanted,
|
|
1024
|
+
const grown = Math.min(wanted, maxCaptureHeight);
|
|
557
1025
|
await page.setViewportSize({ width: request.viewport.width, height: grown });
|
|
558
1026
|
viewportHeight = grown;
|
|
559
1027
|
await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
|
|
@@ -566,9 +1034,6 @@ export class StudioScreenshotter {
|
|
|
566
1034
|
const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
|
|
567
1035
|
.then(value => Array.isArray(value) ? value : [])
|
|
568
1036
|
.catch(() => []);
|
|
569
|
-
const renderErrors = await readRenderErrors(page);
|
|
570
|
-
const consoleErrors = consoleLog.consoleErrors();
|
|
571
|
-
const pageErrors = consoleLog.pageErrors();
|
|
572
1037
|
// Özet kareden ÖNCE ve büyütmeden SONRA: ikisi de aynı yerleşmiş sayfayı anlatsın.
|
|
573
1038
|
const summaryRaw = request.summary === false
|
|
574
1039
|
? undefined
|
|
@@ -607,13 +1072,10 @@ export class StudioScreenshotter {
|
|
|
607
1072
|
readyAfterMs: readiness.waitedMs,
|
|
608
1073
|
...(finalUrl === undefined ? {} : { finalUrl }),
|
|
609
1074
|
...(measured === undefined ? {} : { contentHeight: measured }),
|
|
610
|
-
...(measured !== undefined && measured >
|
|
1075
|
+
...(measured !== undefined && measured > maxCaptureHeight ? { fullPageTruncated: true } : {}),
|
|
611
1076
|
...(layout === undefined ? {} : { layout }),
|
|
612
1077
|
...(actions === undefined ? {} : { actions }),
|
|
613
1078
|
...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
|
|
614
|
-
...(consoleErrors.length > 0 ? { consoleErrors } : {}),
|
|
615
|
-
...(pageErrors.length > 0 ? { pageErrors } : {}),
|
|
616
|
-
...(renderErrors.length > 0 ? { renderErrors } : {}),
|
|
617
1079
|
...(flowsSettled === undefined ? {} : { flowsSettled }),
|
|
618
1080
|
...(navigationRetried ? { navigationRetried: true } : {})
|
|
619
1081
|
};
|