@appaflytech/zeyrek-ai-mcp 0.0.35 → 0.0.37
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 +15 -0
- package/README.md +5 -5
- package/dist/config.js +37 -0
- package/dist/index.js +2 -0
- package/dist/knowledge/access.js +802 -0
- package/dist/knowledge/backendConstraints.js +18 -1
- package/dist/knowledge/case.js +41 -0
- package/dist/knowledge/componentCatalog.js +655 -424
- package/dist/knowledge/componentEvents.js +140 -23
- package/dist/knowledge/connectDataBind.js +843 -0
- package/dist/knowledge/connectDataDefinition.js +344 -0
- package/dist/knowledge/connectDataExecute.js +93 -0
- package/dist/knowledge/connectDataRules.js +128 -0
- package/dist/knowledge/decision.js +121 -0
- package/dist/knowledge/expressionPrelude.js +59 -0
- package/dist/knowledge/layoutSummary.js +140 -2
- package/dist/knowledge/microserviceParameters.js +58 -0
- package/dist/knowledge/mining.js +94 -0
- package/dist/knowledge/mobileComponentCatalog.js +864 -0
- package/dist/knowledge/mobileComponentEvents.js +239 -0
- package/dist/knowledge/mobileStore.js +321 -0
- package/dist/knowledge/normalize.js +235 -84
- package/dist/knowledge/outlines.js +36 -0
- package/dist/knowledge/platform/lint.js +385 -0
- package/dist/knowledge/platform/mobile.js +142 -0
- package/dist/knowledge/platform/mobileLayoutLint.js +324 -0
- package/dist/knowledge/platform/mobileRules.js +231 -0
- package/dist/knowledge/platform/mobileSpacing.js +82 -0
- package/dist/knowledge/platform/mobileTabs.js +108 -0
- package/dist/knowledge/platform/nativeBoxModel.js +44 -0
- package/dist/knowledge/platform/profile.js +25 -0
- package/dist/knowledge/platform/resolve.js +122 -0
- package/dist/knowledge/platform/web.js +39 -0
- package/dist/knowledge/policy.js +18 -4
- package/dist/knowledge/process.js +510 -0
- package/dist/knowledge/reservedTables.js +79 -0
- package/dist/knowledge/rowFilter.js +43 -0
- package/dist/knowledge/rules.js +423 -37
- package/dist/knowledge/schemaGuard.js +11 -0
- package/dist/knowledge/styleResolve.js +53 -19
- package/dist/knowledge/styleResolveNative.js +527 -0
- package/dist/knowledge/themeResolve.js +4 -4
- package/dist/knowledge/themeSchema.js +1 -2
- package/dist/observability/trace.js +6 -3
- package/dist/platform/argumentAliases.js +128 -0
- package/dist/platform/bodyFile.js +103 -0
- package/dist/platform/capture/actions.js +398 -0
- package/dist/platform/capture/browser.js +1 -0
- package/dist/platform/capture/capture.js +683 -0
- package/dist/platform/capture/console-log.js +122 -0
- package/dist/platform/capture/index.js +11 -0
- package/dist/platform/capture/readiness.js +103 -0
- package/dist/platform/capture/summary.js +77 -0
- package/dist/platform/capture/target.js +5 -0
- package/dist/platform/capture/targets/studio-mobile.js +15 -0
- package/dist/platform/capture/targets/studio-web.js +5 -0
- package/dist/platform/diagnostics.js +10 -4
- package/dist/platform/domSummary.js +1 -1
- package/dist/platform/errors.js +163 -10
- package/dist/platform/http.js +29 -8
- package/dist/platform/impersonation.js +45 -0
- package/dist/platform/jsonArray.js +24 -0
- package/dist/platform/responses.js +39 -5
- package/dist/platform/schemaReadMemory.js +60 -0
- package/dist/platform/secrets.js +19 -1
- package/dist/platform/strictArguments.js +68 -12
- package/dist/platform/tokenVault.js +187 -4
- package/dist/resources/examples/bpm-gorev-kontrol.json +294 -0
- package/dist/resources/examples/bpm-iso-dokuman.json +228 -0
- package/dist/resources/examples/bpm-satinalma.json +376 -0
- package/dist/resources/examples/connect-data-convert-verify.json +107 -0
- package/dist/resources/examples/connect-data-dropdown-roles.json +66 -0
- package/dist/resources/examples/connect-data-edit-m2m.json +73 -0
- package/dist/resources/examples/connect-data-query-params.json +50 -0
- package/dist/resources/examples/connect-data-table-entity.json +147 -0
- package/dist/resources/examples/connect-data-workflow-microservice.json +65 -0
- package/dist/resources/examples/middleware-authenticate.json +1 -1
- package/dist/resources/examples/mobile-app-shell-drawer.json +244 -0
- package/dist/resources/examples/mobile-app-shell-stack.json +157 -0
- package/dist/resources/examples/mobile-app-shell-tab.json +208 -0
- package/dist/resources/examples/mobile-form-screen.json +365 -0
- package/dist/resources/examples/mobile-list-detail-form.json +703 -0
- package/dist/resources/examples/page-detail-profile.json +2 -1
- package/dist/resources/examples/page-dynamic-form.json +3 -2
- package/dist/resources/examples/page-flex-sections.json +1 -1
- package/dist/resources/examples/page-html-embed.json +1 -1
- package/dist/resources/examples/workflow-basic.json +8 -2
- package/dist/resources/guides/access.md +810 -0
- package/dist/resources/guides/bpm.md +691 -0
- package/dist/resources/guides/case.md +153 -0
- package/dist/resources/guides/component.md +93 -6
- package/dist/resources/guides/decision.md +185 -0
- package/dist/resources/guides/entity.md +50 -8
- package/dist/resources/guides/expressions.md +14 -5
- package/dist/resources/guides/functions.md +1 -1
- package/dist/resources/guides/getting-started.md +38 -1
- package/dist/resources/guides/layout.md +11 -7
- package/dist/resources/guides/merge.md +37 -3
- package/dist/resources/guides/microservice.md +87 -2
- package/dist/resources/guides/middleware.md +68 -4
- package/dist/resources/guides/mobile-layout.md +193 -0
- package/dist/resources/guides/mobile-publish.md +124 -0
- package/dist/resources/guides/mobile.md +549 -0
- package/dist/resources/guides/page.md +463 -83
- package/dist/resources/guides/theme.md +8 -3
- package/dist/resources/guides/translation.md +1 -1
- package/dist/resources/guides/workflow.md +58 -1
- package/dist/resources/read.js +41 -0
- package/dist/resources/rules.js +11 -5
- package/dist/resources/schema-fingerprint.json +7 -5
- package/dist/resources/schemas/AccessExplanationDto.json +144 -0
- package/dist/resources/schemas/AccessMatrixDto.json +170 -0
- package/dist/resources/schemas/AccessPermissionDto.json +57 -0
- package/dist/resources/schemas/AccessPolicyDto.json +223 -0
- package/dist/resources/schemas/AccessResourceDto.json +86 -0
- package/dist/resources/schemas/AppSessionDto.json +53 -0
- package/dist/resources/schemas/ConnectDataDescribeDto.json +344 -0
- package/dist/resources/schemas/ConnectDataRequestModel.json +202 -0
- package/dist/resources/schemas/ConnectDataResultDto.json +421 -0
- package/dist/resources/schemas/ConnectorCredentialDto.json +70 -0
- package/dist/resources/schemas/ConvertConnectDataToMicroserviceCommand.json +37 -0
- package/dist/resources/schemas/CreateAccessPolicyCommand.json +209 -0
- package/dist/resources/schemas/CreateDecisionCommand.json +221 -0
- package/dist/resources/schemas/CreateEntityCommand.json +118 -0
- package/dist/resources/schemas/CreateMicroserviceCommand.json +13 -1
- package/dist/resources/schemas/CreateProcessDefinitionCommand.json +1337 -0
- package/dist/resources/schemas/CreateQueryCommand.json +4 -0
- package/dist/resources/schemas/CreateWorkflowCommand.json +146 -1
- package/dist/resources/schemas/DataJobDto.json +6 -1
- package/dist/resources/schemas/DecisionDto.json +302 -0
- package/dist/resources/schemas/DetachConnectDataMicroserviceCommand.json +28 -0
- package/dist/resources/schemas/ExplainAccessQuery.json +33 -0
- package/dist/resources/schemas/GenerateProcessDocumentCommand.json +116 -0
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +25 -1
- package/dist/resources/schemas/MicroserviceDto.json +15 -3
- package/dist/resources/schemas/OrganizationProjectDto.json +335 -0
- package/dist/resources/schemas/OrganizationSchemaDto.json +196 -0
- package/dist/resources/schemas/OrganizationSchemaVersionDto.json +207 -0
- package/dist/resources/schemas/PatchProcessDefinitionCommand.json +76 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +10 -1
- package/dist/resources/schemas/PreviewWorkflowCommand.json +25 -1
- package/dist/resources/schemas/ProcessDefinitionDto.json +1480 -0
- package/dist/resources/schemas/ProcessSimulationDto.json +96 -0
- package/dist/resources/schemas/SaveConnectorCredentialCommand.json +90 -0
- package/dist/resources/schemas/SaveProcessEventSourceCommand.json +61 -0
- package/dist/resources/schemas/SaveProcessMailSourceCommand.json +89 -0
- package/dist/resources/schemas/SignProcessTaskCommand.json +73 -0
- package/dist/resources/schemas/SimulateProcessDefinitionQuery.json +92 -0
- package/dist/resources/schemas/StartMobileBuildCommand.json +54 -0
- package/dist/resources/schemas/StepDto.json +25 -1
- package/dist/resources/schemas/UpdateAccessPolicyCommand.json +209 -0
- package/dist/resources/schemas/UpdateDecisionCommand.json +216 -0
- package/dist/resources/schemas/UpdateEntityCommand.json +118 -0
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +13 -1
- package/dist/resources/schemas/UpdateProcessDefinitionCommand.json +1357 -0
- package/dist/resources/schemas/UpdateQueryCommand.json +4 -0
- package/dist/resources/schemas/UpdateViewCommand.json +0 -2
- package/dist/resources/schemas/UpdateWorkflowCommand.json +146 -1
- package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +274 -0
- package/dist/resources/schemas/ValidateProcessDefinitionQuery.json +1339 -0
- package/dist/server.js +31 -1
- package/dist/tools/access.js +369 -2
- package/dist/tools/accessOperations.js +135 -0
- package/dist/tools/accessPolicies.js +170 -0
- package/dist/tools/accessRuntime.js +203 -0
- package/dist/tools/applications.js +60 -3
- package/dist/tools/bpm.js +343 -0
- package/dist/tools/bpmGovernance.js +266 -0
- package/dist/tools/bpmOperations.js +230 -0
- package/dist/tools/bpmReporting.js +154 -0
- package/dist/tools/bpmRouting.js +150 -0
- package/dist/tools/bpmRuntime.js +396 -0
- package/dist/tools/case.js +178 -0
- package/dist/tools/caseRuntime.js +89 -0
- package/dist/tools/common.js +10 -1
- package/dist/tools/components.js +118 -14
- package/dist/tools/connectData.js +450 -0
- package/dist/tools/connectors.js +193 -0
- package/dist/tools/datasources.js +3 -3
- package/dist/tools/decisions.js +206 -0
- package/dist/tools/dependencies.js +1 -1
- package/dist/tools/deployments.js +38 -3
- package/dist/tools/designSchemaPatch.js +34 -9
- package/dist/tools/dtos.js +36 -5
- package/dist/tools/entities.js +79 -12
- package/dist/tools/externalComponents.js +26 -5
- package/dist/tools/flows.js +119 -17
- package/dist/tools/folders.js +1 -1
- package/dist/tools/imports.js +5 -2
- package/dist/tools/layouts.js +50 -17
- package/dist/tools/merges.js +2 -2
- package/dist/tools/microservices.js +77 -25
- package/dist/tools/mobile.js +280 -0
- package/dist/tools/mobileStore.js +556 -0
- package/dist/tools/modules.js +48 -0
- package/dist/tools/organizationStructure.js +137 -0
- package/dist/tools/pages.js +465 -98
- package/dist/tools/platformInfo.js +28 -8
- package/dist/tools/profileWrite.js +92 -0
- package/dist/tools/projects.js +71 -9
- package/dist/tools/rows.js +69 -13
- package/dist/tools/schemaOps.js +61 -1
- package/dist/tools/screenWrite.js +36 -7
- package/dist/tools/screens.js +95 -5
- package/dist/tools/templates.js +26 -1
- package/dist/tools/translations.js +1 -1
- package/dist/tools/variables.js +11 -5
- package/dist/tools/workflows.js +10 -5
- package/package.json +2 -2
- package/dist/platform/screenshot.js +0 -1162
|
@@ -1,1162 +0,0 @@
|
|
|
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';
|
|
5
|
-
/**
|
|
6
|
-
* Ziyaretçinin tema tercihinin yaşadığı localStorage anahtarı — `theme-toggle` bunu yazar,
|
|
7
|
-
* `Theme` okur ve projenin kendi modunun önüne geçirir
|
|
8
|
-
* (`zeyrek-frontend/packages/ng-apps-ui/src/shared/theme/stores.ts`). Anahtar uygulama başına
|
|
9
|
-
* DEĞİL tarayıcı başına tektir.
|
|
10
|
-
*/
|
|
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
|
-
|
|
85
|
-
// SA-12 — autocomplete girdisi de role="combobox" taşır ama açılır kutu değildir: listesi yalnız
|
|
86
|
-
// input olayıyla açılır (tıklama hiçbir şey yapmaz), öneri satırı role="option" ve data-value
|
|
87
|
-
// TAŞIMAZ, seçim satırın mousedown'ındadır (Autocomplete.svelte on:mousedown|preventDefault;
|
|
88
|
-
// click ölçüldü, seçmedi). Dropdown yoluna girdiğinde 3 sn bekleyip "kutu devre dışı" diyordu.
|
|
89
|
-
// İstenen metin yazılır ve GÖRÜNEN METNİ eşleşen satıra mousedown atılır; value ile seçim yoktur.
|
|
90
|
-
var suggest = combobox && combobox.closest('.ng-autocomplete');
|
|
91
|
-
if (suggest) {
|
|
92
|
-
// Vurgu satırı [metin, ' ', <strong>, ' ', metin] düğümleriyle basılır (Svelte derleyicisi):
|
|
93
|
-
// textContent 'Ist anbul' olur. Yalnız-boşluk düğümleri atlanınca etiket geri gelir.
|
|
94
|
-
var labelOf = function (option) {
|
|
95
|
-
var label = option.querySelector('.ng-autocomplete__item-label');
|
|
96
|
-
if (label) return Array.prototype.map.call(label.childNodes, function (part) { return part.nodeType === 3 && !/\\S/.test(part.textContent) ? '' : part.textContent; }).join('').trim();
|
|
97
|
-
var caption = option.querySelector('.ng-autocomplete__item-caption');
|
|
98
|
-
var text = option.textContent || '';
|
|
99
|
-
return (caption ? text.replace(caption.textContent || '', '') : text).trim();
|
|
100
|
-
};
|
|
101
|
-
var same = function (left, right) { return left.replace(/\\s+/g, ' ').trim().toLowerCase() === right.replace(/\\s+/g, ' ').trim().toLowerCase(); };
|
|
102
|
-
|
|
103
|
-
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(combobox, wanted);
|
|
104
|
-
combobox.dispatchEvent(new Event('input', { bubbles: true }));
|
|
105
|
-
|
|
106
|
-
return new Promise(function (resolve) {
|
|
107
|
-
var waited = 0;
|
|
108
|
-
var step = 100;
|
|
109
|
-
var limit = 3000;
|
|
110
|
-
|
|
111
|
-
var attempt = function () {
|
|
112
|
-
var options = Array.prototype.slice.call(suggest.querySelectorAll('[role="option"]'));
|
|
113
|
-
var match = options.filter(function (option) { return same(labelOf(option), wanted); })[0];
|
|
114
|
-
|
|
115
|
-
if (match) {
|
|
116
|
-
match.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
117
|
-
resolve(finish({ found: true, applied: true, matchedBy: 'text', optionsSeen: options.length, waitedMs: waited, requests: count }));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (waited >= limit) {
|
|
122
|
-
var empty = suggest.querySelector('.ng-autocomplete__empty');
|
|
123
|
-
var seen = options.slice(0, 10).map(labelOf).join(' · ');
|
|
124
|
-
var shown = options.length > 0
|
|
125
|
-
? "görülen " + options.length + " öneri: " + seen
|
|
126
|
-
: empty
|
|
127
|
-
? "liste: '" + (empty.textContent || '').trim() + "'"
|
|
128
|
-
: "öneri listesi hiç basılmadı — yazılan metin minChars eşiğinin altında ya da alan kilitli olabilir (page_runtime_get ile minChars/isDisabled/isReadonly)";
|
|
129
|
-
var result = { found: true, applied: false, waitedMs: waited, requests: count, note: "öneri listesinde '" + wanted + "' yok (" + waited + " ms) — " + shown + ". Yazılan metin ('" + wanted + "') alanda kaldı. Autocomplete yalnız görünen metinle seçer, value ile değil; öneriler bir sorgudan geliyorsa query_preview ile doğrulayın." };
|
|
130
|
-
// optionsSeen: 0 yazılmaz — page_screenshot sıfır seçenekli 'select' için açılır kutuya
|
|
131
|
-
// ait "devre dışı ya da salt okunur olabilir" notunu basar (tools/pages.ts emptyOptions);
|
|
132
|
-
// bu dalın nedeni yukarıdaki nottadır.
|
|
133
|
-
if (options.length > 0) result.optionsSeen = options.length;
|
|
134
|
-
resolve(finish(result));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
waited += step;
|
|
139
|
-
setTimeout(attempt, step);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
attempt();
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (combobox) {
|
|
147
|
-
// Liste BU kutunun kabının içindedir. Sorgular belge kapsamlı kaldığında açık duran başka
|
|
148
|
-
// bir kutunun (multiselect seçimden sonra kapanmıyor) seçeneğine tıklanabiliyordu ve adım
|
|
149
|
-
// "uygulandı" diyordu; komşunun yükleme göstergesi de bu kutunun kararını blokluyordu (B-5).
|
|
150
|
-
var scope = combobox.closest('.ng-select, .ng-multiselect') || node;
|
|
151
|
-
|
|
152
|
-
combobox.click();
|
|
153
|
-
|
|
154
|
-
return new Promise(function (resolve) {
|
|
155
|
-
var waited = 0;
|
|
156
|
-
var step = 100;
|
|
157
|
-
var limit = 3000;
|
|
158
|
-
|
|
159
|
-
var attempt = function () {
|
|
160
|
-
var loading = scope.querySelector('.ng-select__loading, .ng-multiselect__loading');
|
|
161
|
-
var options = Array.prototype.slice.call(scope.querySelectorAll('[role="treeitem"][data-value]'));
|
|
162
|
-
var byValue = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0];
|
|
163
|
-
var byText = options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
|
|
164
|
-
var match = byValue || byText;
|
|
165
|
-
|
|
166
|
-
if (match) {
|
|
167
|
-
var button = match.querySelector('button');
|
|
168
|
-
// Seçilemeyen seçeneğe tıklamak hiçbir şey yapmaz; "uygulandı" demek yanlış olurdu (B-6).
|
|
169
|
-
if (match.getAttribute('aria-disabled') === 'true' || (button && button.disabled)) {
|
|
170
|
-
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: "seçenek devre dışı (aria-disabled): " + wanted }));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
(button || match).click();
|
|
175
|
-
resolve(finish({ found: true, applied: true, matchedBy: byValue ? 'value' : 'text', optionsSeen: options.length, waitedMs: waited, requests: count }));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
var settled = !loading && (options.length > 0 || !!scope.querySelector('.ng-select__empty, .ng-multiselect__empty'));
|
|
180
|
-
|
|
181
|
-
if (settled || waited >= limit) {
|
|
182
|
-
var seen = options.slice(0, 10).map(function (option) { return (option.textContent || '').trim(); }).join(' · ');
|
|
183
|
-
// Liste HİÇ açılmadıysa kusur sorguda değil kutunun kendisinde olabilir:
|
|
184
|
-
// handleContainerClick kilitli/salt-okunur/yükleniyor kutuda erken döner.
|
|
185
|
-
var locked = !scope.querySelector('.ng-select__list, .ng-multiselect__list');
|
|
186
|
-
var note = options.length > 0
|
|
187
|
-
? "açılır kutuda bu değere karşılık gelen seçenek yok: " + wanted + " — görülen " + options.length + " seçenek: " + seen
|
|
188
|
-
: locked
|
|
189
|
-
? "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"
|
|
190
|
-
: "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";
|
|
191
|
-
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: note }));
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
waited += step;
|
|
196
|
-
setTimeout(attempt, step);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
attempt();
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
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 };
|
|
203
|
-
})()`;
|
|
204
|
-
}
|
|
205
|
-
// S-4 — form gönderme. Gönder butonu olmayan ya da Enter ile gönderilen form başsız
|
|
206
|
-
// doğrulanamıyordu; `requestSubmit` formun kendi submit olayını doğduğu gibi doğurur.
|
|
207
|
-
if (action.action === 'submit') {
|
|
208
|
-
return `(function () {
|
|
209
|
-
var count = performance.getEntriesByType('resource').length;
|
|
210
|
-
var node = document.querySelector(${selector});
|
|
211
|
-
if (!node) return { found: false, applied: false, requests: count };
|
|
212
|
-
var form = node.matches('form') ? node : (node.querySelector('form') || node.closest('form'));
|
|
213
|
-
|
|
214
|
-
if (form) {
|
|
215
|
-
if (typeof form.requestSubmit === 'function') {
|
|
216
|
-
form.requestSubmit();
|
|
217
|
-
return { found: true, applied: true, submitVia: 'requestSubmit', requests: count };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
221
|
-
return { found: true, applied: true, submitVia: 'dispatch', requests: count };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
var button = node.querySelector('button[type="submit"]');
|
|
225
|
-
|
|
226
|
-
if (button) {
|
|
227
|
-
button.click();
|
|
228
|
-
return { found: true, applied: true, submitVia: 'button', requests: count };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
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 };
|
|
232
|
-
})()`;
|
|
233
|
-
}
|
|
234
|
-
// G-09 — kanban kartı ve takvim etkinliği HTML5 DnD ile taşınır. Olaylar sentetik atılır ve
|
|
235
|
-
// TEK DataTransfer'i paylaşır; koordinat bırakma hedefinin merkezidir, çünkü takvimin saat
|
|
236
|
-
// kolonu bırakılan saati `clientY`'den hesaplar (slot hedefi o saatin ortasına düşer).
|
|
237
|
-
// Playwright `dragTo` gerçek fare sürer ama ScreenshotPage bu yeteneği taşımaz; betik yolu
|
|
238
|
-
// öteki eylemlerle aynı ölçüm döngüsünden (requests · flowRuns) geçer.
|
|
239
|
-
if (action.action === 'drag') {
|
|
240
|
-
const destination = JSON.stringify(`[data-element-id="${String(action.to ?? '')}"]`);
|
|
241
|
-
const destinationName = JSON.stringify(String(action.to ?? ''));
|
|
242
|
-
return `(function () {
|
|
243
|
-
var count = performance.getEntriesByType('resource').length;
|
|
244
|
-
var node = document.querySelector(${selector});
|
|
245
|
-
if (!node) return { found: false, applied: false, requests: count };
|
|
246
|
-
var drop = document.querySelector(${destination});
|
|
247
|
-
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 };
|
|
248
|
-
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 };
|
|
249
|
-
var transfer = new DataTransfer();
|
|
250
|
-
var center = function (element) { var rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; };
|
|
251
|
-
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 })); };
|
|
252
|
-
var from = center(node);
|
|
253
|
-
var to = center(drop);
|
|
254
|
-
fire(node, 'dragstart', from);
|
|
255
|
-
fire(drop, 'dragenter', to);
|
|
256
|
-
fire(drop, 'dragover', to);
|
|
257
|
-
fire(drop, 'drop', to);
|
|
258
|
-
fire(node, 'dragend', to);
|
|
259
|
-
return { found: true, applied: true, requests: count };
|
|
260
|
-
})()`;
|
|
261
|
-
}
|
|
262
|
-
const target = String(action.target ?? '');
|
|
263
|
-
const root = rootElementId(target);
|
|
264
|
-
// G-14 — `applied` yalnız "DOM'a yazıldı" demekti: tarih seçici olayı dinlemiyordu, kutu dolu
|
|
265
|
-
// görünüyor, prop boş kalıyordu. Kökün prop parmak izi eylemden ÖNCE alınır; runActions
|
|
266
|
-
// yerleşmeden sonra yeniden okuyup `propWritten`'ı çıkarır. Çok alanlı bileşenin kökünde tek
|
|
267
|
-
// değer yoktur — ilk kutuya yazmak yarım değeri "uygulandı" diye raporluyordu, kök reddedilir.
|
|
268
|
-
return `(function () {
|
|
269
|
-
var count = performance.getEntriesByType('resource').length;
|
|
270
|
-
var node = document.querySelector(${selector});
|
|
271
|
-
if (!node) return { found: false, applied: false, requests: count };
|
|
272
|
-
var wanted = String(${value});
|
|
273
|
-
var propsBefore = ${runtimePropsScript(root)};
|
|
274
|
-
var finish = function (result) { if (propsBefore !== undefined) result.propsBefore = propsBefore; return result; };
|
|
275
|
-
|
|
276
|
-
if (${JSON.stringify(root === target)}) {
|
|
277
|
-
var parts = Array.prototype.slice.call(node.querySelectorAll(${JSON.stringify(`[data-element-id^="${root}${INTERNAL_ROLE_SEPARATOR}"]`)}))
|
|
278
|
-
.filter(function (part) { return part.querySelector('input, select, textarea'); })
|
|
279
|
-
.map(function (part) { return part.getAttribute('data-element-id'); });
|
|
280
|
-
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 });
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak hiçbir şey
|
|
284
|
-
// yapmaz, seçeneğe TIKLANMASI gerekir. 'set' bu noktada 'select' fiiliyle AYNI yola girer —
|
|
285
|
-
// iki kopya seçim mantığı tutulmaz.
|
|
286
|
-
//
|
|
287
|
-
// SA-12 — autocomplete girdisi de role="combobox" taşır ama serbest metin alanıdır: listesi
|
|
288
|
-
// tıklamayla değil input olayıyla açılır. Native yola düşer; öneri seçmek 'select'in işi.
|
|
289
|
-
|
|
290
|
-
var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
|
|
291
|
-
if (combobox && !combobox.closest('.ng-autocomplete')) {
|
|
292
|
-
// Liste BU kutunun kabının içindedir. Sorgular belge kapsamlı kaldığında açık duran başka
|
|
293
|
-
// bir kutunun (multiselect seçimden sonra kapanmıyor) seçeneğine tıklanabiliyordu ve adım
|
|
294
|
-
// "uygulandı" diyordu; komşunun yükleme göstergesi de bu kutunun kararını blokluyordu (B-5).
|
|
295
|
-
var scope = combobox.closest('.ng-select, .ng-multiselect') || node;
|
|
296
|
-
|
|
297
|
-
combobox.click();
|
|
298
|
-
|
|
299
|
-
return new Promise(function (resolve) {
|
|
300
|
-
var waited = 0;
|
|
301
|
-
var step = 100;
|
|
302
|
-
var limit = 3000;
|
|
303
|
-
|
|
304
|
-
var attempt = function () {
|
|
305
|
-
var loading = scope.querySelector('.ng-select__loading, .ng-multiselect__loading');
|
|
306
|
-
var options = Array.prototype.slice.call(scope.querySelectorAll('[role="treeitem"][data-value]'));
|
|
307
|
-
var byValue = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0];
|
|
308
|
-
var byText = options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
|
|
309
|
-
var match = byValue || byText;
|
|
310
|
-
|
|
311
|
-
if (match) {
|
|
312
|
-
var button = match.querySelector('button');
|
|
313
|
-
// Seçilemeyen seçeneğe tıklamak hiçbir şey yapmaz; "uygulandı" demek yanlış olurdu (B-6).
|
|
314
|
-
if (match.getAttribute('aria-disabled') === 'true' || (button && button.disabled)) {
|
|
315
|
-
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: "seçenek devre dışı (aria-disabled): " + wanted }));
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
(button || match).click();
|
|
320
|
-
resolve(finish({ found: true, applied: true, matchedBy: byValue ? 'value' : 'text', optionsSeen: options.length, waitedMs: waited, requests: count }));
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
var settled = !loading && (options.length > 0 || !!scope.querySelector('.ng-select__empty, .ng-multiselect__empty'));
|
|
325
|
-
|
|
326
|
-
if (settled || waited >= limit) {
|
|
327
|
-
var seen = options.slice(0, 10).map(function (option) { return (option.textContent || '').trim(); }).join(' · ');
|
|
328
|
-
// Liste HİÇ açılmadıysa kusur sorguda değil kutunun kendisinde olabilir:
|
|
329
|
-
// handleContainerClick kilitli/salt-okunur/yükleniyor kutuda erken döner.
|
|
330
|
-
var locked = !scope.querySelector('.ng-select__list, .ng-multiselect__list');
|
|
331
|
-
var note = options.length > 0
|
|
332
|
-
? "açılır kutuda bu değere karşılık gelen seçenek yok: " + wanted + " — görülen " + options.length + " seçenek: " + seen
|
|
333
|
-
: locked
|
|
334
|
-
? "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"
|
|
335
|
-
: "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";
|
|
336
|
-
resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: note }));
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
waited += step;
|
|
341
|
-
setTimeout(attempt, step);
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
attempt();
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
|
|
349
|
-
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 });
|
|
350
|
-
var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
|
|
351
|
-
: (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
|
|
352
|
-
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
353
|
-
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
354
|
-
if (field.checked !== shouldCheck) field.click();
|
|
355
|
-
return finish({ found: true, applied: true, requests: count });
|
|
356
|
-
}
|
|
357
|
-
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
358
|
-
setter.call(field, ${value});
|
|
359
|
-
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
360
|
-
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
361
|
-
return finish({ found: true, applied: true, requests: count });
|
|
362
|
-
})()`;
|
|
363
|
-
}
|
|
364
|
-
/** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */
|
|
365
|
-
export function rootElementId(target) {
|
|
366
|
-
const separator = target.indexOf(INTERNAL_ROLE_SEPARATOR);
|
|
367
|
-
return separator > 0 ? target.slice(0, separator) : target;
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Düğümün runtime prop'larının parmak izi (`@ng-apps/flow` → flow-runtime-props.ts,
|
|
371
|
-
* `window.__ngAppsReadProps`). `undefined` okuyucu yayınlanmıyor (eski runtime) ya da prop'lar
|
|
372
|
-
* serileşmiyor; `null` hiçbir makine düğümü tanımıyor; dize karşılaştırılabilir iz.
|
|
373
|
-
*/
|
|
374
|
-
export function runtimePropsScript(elementId) {
|
|
375
|
-
return `(function () { /* runtimePropsSnapshot */
|
|
376
|
-
var read = window.__ngAppsReadProps;
|
|
377
|
-
if (typeof read !== 'function') return undefined;
|
|
378
|
-
try { var props = read(${JSON.stringify(elementId)}); return props === null ? null : JSON.stringify(props); } catch (error) { return undefined; }
|
|
379
|
-
})()`;
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Hangi sekme AÇIK? `tab` adımı tıklamanın işe yarayıp yaramadığını ancak bununla kanıtlar:
|
|
383
|
-
* tıklama "uygulandı" dese de bileşen seçimi kendi `selected` prop'una yazmamış olabilir.
|
|
384
|
-
* `undefined` düğüm yok, `null` sekme grubu var ama hiçbiri seçili değil.
|
|
385
|
-
*/
|
|
386
|
-
export function activeTabScript(elementId) {
|
|
387
|
-
return `(function () { /* activeTabSnapshot */
|
|
388
|
-
var root = document.querySelector(${JSON.stringify(`[data-element-id="${elementId}"]`)});
|
|
389
|
-
if (!root) return undefined;
|
|
390
|
-
var active = root.querySelector('[role="tab"][aria-selected="true"]');
|
|
391
|
-
return active ? String(active.textContent || '').replace(/\\s+/g, ' ').trim() : null;
|
|
392
|
-
})()`;
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* fullPage karesi Chromium'un viewport ötesi yakalamasıyla değil, viewport'u içerik boyuna
|
|
396
|
-
* büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
|
|
397
|
-
* ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
|
|
398
|
-
* Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
|
|
399
|
-
*/
|
|
400
|
-
export const contentHeightScript = [
|
|
401
|
-
'(function () {',
|
|
402
|
-
' var doc = document.documentElement;',
|
|
403
|
-
' var body = document.body;',
|
|
404
|
-
' var viewportHeight = window.innerHeight;',
|
|
405
|
-
' var heights = [doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0];',
|
|
406
|
-
' var nodes = document.querySelectorAll("*");',
|
|
407
|
-
' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
|
|
408
|
-
' var node = nodes[index];',
|
|
409
|
-
' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(node.scrollHeight);',
|
|
410
|
-
' }',
|
|
411
|
-
' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
|
|
412
|
-
'})()'
|
|
413
|
-
].join('\n');
|
|
414
|
-
/**
|
|
415
|
-
* Kırpma kutusu CSS pikselinde okunur; kare çekilirken Chromium onu `scale` ile çarpar
|
|
416
|
-
* (ölçüldü 2026-09-16: viewport 1280x800 · scale 0.5 · clip 300x100 → 150x50 raster).
|
|
417
|
-
* `null` düğüm yok demektir — tool bunu HATAYA çevirir, boş kare döndürmez.
|
|
418
|
-
*/
|
|
419
|
-
export function clipRectScript(elementId) {
|
|
420
|
-
return `(function () { /* clipRect */
|
|
421
|
-
var node = document.querySelector(${JSON.stringify(`[data-element-id="${elementId}"]`)});
|
|
422
|
-
if (!node) return null;
|
|
423
|
-
var rect = node.getBoundingClientRect();
|
|
424
|
-
var x = rect.x + (window.scrollX || 0);
|
|
425
|
-
var y = rect.y + (window.scrollY || 0);
|
|
426
|
-
// 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.
|
|
427
|
-
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) };
|
|
428
|
-
})()`;
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Karenin raster boyu başlıktan okunur: PNG'de IHDR, JPEG'de ilk SOF işaretçisi. Ölçeğin
|
|
432
|
-
* gerçekten uygulandığı yanıtta SAYIYLA görünsün diye var — "yarı çözünürlük istedim" ile
|
|
433
|
-
* "yarı çözünürlük aldım" arasındaki farkı başka hiçbir alan söylemiyor.
|
|
434
|
-
*/
|
|
435
|
-
export function imageSize(buffer) {
|
|
436
|
-
if (buffer.length > 24 && buffer.subarray(1, 4).toString() === 'PNG') {
|
|
437
|
-
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
438
|
-
}
|
|
439
|
-
if (buffer.length > 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
440
|
-
let offset = 2;
|
|
441
|
-
while (offset + 9 < buffer.length) {
|
|
442
|
-
if (buffer[offset] !== 0xff) {
|
|
443
|
-
offset += 1;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const marker = buffer[offset + 1] ?? 0;
|
|
447
|
-
const length = buffer.readUInt16BE(offset + 2);
|
|
448
|
-
// SOF0-SOF3 · SOF5-SOF7 · SOF9-SOF11 · SOF13-SOF15 boyutu taşır; DHT/DRI gibi işaretçiler atlanır.
|
|
449
|
-
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
450
|
-
return { height: buffer.readUInt16BE(offset + 5), width: buffer.readUInt16BE(offset + 7) };
|
|
451
|
-
}
|
|
452
|
-
offset += 2 + length;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return undefined;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* NE GÖRÜNÜYOR — resimsiz (S-5). Aynı sayfa yüklemesinden çıkan üçüncü çıktı: her KÖK runtime
|
|
459
|
-
* düğümünün görünürlüğü, metni, satır sayısı, açık sekmesi, değeri ve hata metinleri.
|
|
460
|
-
*
|
|
461
|
-
* Kök düğüm = kimliğinde `__` OLMAYAN düğüm; iç roller (`tbl__row-3`) satır sayısına dönüşür,
|
|
462
|
-
* listeye girmez. Tavan 300 düğüm, metin 80 karakter (K3) — özet bir kareyi değiştirmeli, onun
|
|
463
|
-
* kadar pahalı olmamalı. Tip `data-component-type`'tan gelir (frontend f-02); runtime basmıyorsa
|
|
464
|
-
* alan YAZILMAZ ve `typed: false` döner — `layoutReportUnavailable` ile aynı dürüstlük kuralı.
|
|
465
|
-
*/
|
|
466
|
-
export function domSummaryScript(viewportHeight) {
|
|
467
|
-
return `(function () { /* domSummary */
|
|
468
|
-
var MAX = 300;
|
|
469
|
-
var CUT = 80;
|
|
470
|
-
// Görünür alanın GERÇEK boyu: fullPage ve clip yolları viewport'u içerik boyuna büyütür ve
|
|
471
|
-
// o anda window.innerHeight = içerik yüksekliği olur — her düğüm "ekran içinde" görünürdü ve
|
|
472
|
-
// "kart katlanmanın altında mı" sorusu HER ZAMAN 'visible' cevabı alırdı (B-10).
|
|
473
|
-
var VH = ${Math.max(Math.round(viewportHeight), 1)};
|
|
474
|
-
var clean = function (value) { return String(value || '').replace(/\\s+/g, ' ').trim(); };
|
|
475
|
-
var ROOT_SELECTOR = '[data-element-id]:not([data-element-id*="__"])';
|
|
476
|
-
var all = Array.prototype.slice.call(document.querySelectorAll('[data-element-id]'));
|
|
477
|
-
var roots = all.filter(function (node) { return String(node.getAttribute('data-element-id') || '').indexOf('__') < 0; });
|
|
478
|
-
var typed = false;
|
|
479
|
-
var nodes = roots.slice(0, MAX).map(function (node) {
|
|
480
|
-
var id = node.getAttribute('data-element-id');
|
|
481
|
-
var type = node.getAttribute('data-component-type');
|
|
482
|
-
if (type) typed = true;
|
|
483
|
-
// Bir olgu, ONU TAŞIYAN düğüme yazılır: en yakın kök ata bu düğüm değilse o olgu
|
|
484
|
-
// çocuğundur ve orada zaten listelenecektir (B-8). Aksi hâlde aynı değer üç satırda çıkar.
|
|
485
|
-
var owned = function (element) { return !!element && element.closest(ROOT_SELECTOR) === node; };
|
|
486
|
-
var rect = node.getBoundingClientRect();
|
|
487
|
-
var visible = typeof node.checkVisibility === 'function'
|
|
488
|
-
? !!node.checkVisibility({ checkVisibilityCSS: true, checkOpacity: true })
|
|
489
|
-
: (rect.width > 0 && rect.height > 0);
|
|
490
|
-
var inViewport = rect.bottom > 0 && rect.right > 0 && rect.top < VH && rect.left < (window.innerWidth || 0);
|
|
491
|
-
// Kap düğümün innerText'i ÇOCUKLARININ metnidir; metin yalnız YAPRAK köklerde yazılır.
|
|
492
|
-
var nested = Array.prototype.slice.call(node.querySelectorAll('[data-element-id]'))
|
|
493
|
-
.some(function (child) { return String(child.getAttribute('data-element-id') || '').indexOf('__') < 0; });
|
|
494
|
-
var text = nested ? '' : clean(node.innerText);
|
|
495
|
-
if (text.length > CUT) text = text.slice(0, CUT) + '…';
|
|
496
|
-
var rows = document.querySelectorAll('[data-element-id^="' + id + '__row-"]').length;
|
|
497
|
-
var tab = node.querySelector('[role="tab"][aria-selected="true"]');
|
|
498
|
-
var field = node.querySelector('input, select, textarea');
|
|
499
|
-
var chosen = node.querySelector('.selected-item');
|
|
500
|
-
var holder = field || chosen;
|
|
501
|
-
var value = !owned(holder) ? '' : (field ? (field.type === 'password' ? '***' : clean(field.value)) : clean(chosen.textContent));
|
|
502
|
-
var media = node.querySelector('img, svg, canvas, video, iframe');
|
|
503
|
-
// İç içe <li> hem grubu hem alt mesajları sayıyordu: tek grup + 2 alt hata 3 görünüyordu (B-7).
|
|
504
|
-
var errorItems = Array.prototype.slice.call(node.querySelectorAll('.form-validation-errors li:not(:has(li))'))
|
|
505
|
-
.filter(owned).map(function (item) { return clean(item.textContent); });
|
|
506
|
-
// aria-invalid hem köke hem input'a yazılabiliyor (Password.svelte); yalnız ALANLAR sayılır.
|
|
507
|
-
var invalid = Array.prototype.slice.call(node.querySelectorAll('input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"]')).filter(owned).length;
|
|
508
|
-
var summary = { elementId: id, visible: !!visible, inViewport: !!inViewport };
|
|
509
|
-
if (type) summary.type = type;
|
|
510
|
-
if (text) summary.text = text;
|
|
511
|
-
if (rows > 0) summary.rows = rows;
|
|
512
|
-
if (owned(tab)) summary.activeTab = clean(tab.textContent);
|
|
513
|
-
if (value) summary.value = value;
|
|
514
|
-
if (visible && !text && !media && !nested) summary.empty = true;
|
|
515
|
-
if (errorItems.length > 0) summary.errors = errorItems;
|
|
516
|
-
else if (invalid > 0) summary.errors = ['aria-invalid: ' + invalid];
|
|
517
|
-
return summary;
|
|
518
|
-
});
|
|
519
|
-
var messages = [];
|
|
520
|
-
Array.prototype.slice.call(document.querySelectorAll('.ng-toast')).forEach(function (toast) {
|
|
521
|
-
var name = String(toast.className || '');
|
|
522
|
-
var kind = name.indexOf('ng-toast--error') >= 0 ? 'toast/error' : (name.indexOf('ng-toast--success') >= 0 ? 'toast/success' : 'toast');
|
|
523
|
-
messages.push({ kind: kind, text: clean(toast.innerText).slice(0, 160) });
|
|
524
|
-
});
|
|
525
|
-
Array.prototype.slice.call(document.querySelectorAll('.view-detail__error, .error-page')).forEach(function (node) {
|
|
526
|
-
messages.push({ kind: 'error', text: clean(node.innerText).slice(0, 160) });
|
|
527
|
-
});
|
|
528
|
-
return { nodes: nodes, messages: messages, truncated: roots.length > MAX, typed: typed };
|
|
529
|
-
})()`;
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
|
|
533
|
-
* çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
|
|
534
|
-
* (F-04 dev'e çıkana dek) liste boş döner — tool uydurmaz, layoutReportUnavailable der.
|
|
535
|
-
*/
|
|
536
|
-
export const layoutReportScript = [
|
|
537
|
-
'Array.from(document.querySelectorAll("[data-element-id]")).slice(0, 500).map(function (node) {',
|
|
538
|
-
' var rect = node.getBoundingClientRect();',
|
|
539
|
-
' return {',
|
|
540
|
-
' elementId: node.getAttribute("data-element-id"),',
|
|
541
|
-
' x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height),',
|
|
542
|
-
' overflowX: node.scrollWidth > node.clientWidth + 1,',
|
|
543
|
-
' overflowY: node.scrollHeight > node.clientHeight + 1',
|
|
544
|
-
' };',
|
|
545
|
-
'})'
|
|
546
|
-
].join('\n');
|
|
547
|
-
/**
|
|
548
|
-
* F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
|
|
549
|
-
* patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
|
|
550
|
-
* yazılmıyor ve tek belirti yığında kaybolan bir bildirim oluyordu. Runtime artık
|
|
551
|
-
* yapısal hata defterini bu global anahtarda yayınlıyor (`@ng-apps/flow` →
|
|
552
|
-
* flow-runtime-errors.ts); kare çekilirken bedavaya okunur.
|
|
553
|
-
*/
|
|
554
|
-
export const flowRuntimeErrorsScript = 'Array.isArray(window.__ngAppsFlowRuntimeErrors) ? window.__ngAppsFlowRuntimeErrors.slice(0, 50) : []';
|
|
555
|
-
/**
|
|
556
|
-
* Kare ne zaman hazır? (G-30 → B-6, 2026-09-07)
|
|
557
|
-
*
|
|
558
|
-
* `delayMs` TAHMİNDİ; 09-05'te runtime koşan akış sayacını yayınlamaya başladı (`@ng-apps/flow`
|
|
559
|
-
* → flow-runtime-errors.ts) ve bekleme "running sıfır, completed > 0" olunca bitiyordu. Bu şart
|
|
560
|
-
* mount akışı OLMAYAN sayfada (giriş ekranı) hiç sağlanmaz; akış tetiklemeyen `set` eylemi de
|
|
561
|
-
* `completed`'ı artırmaz. 09-06·2 koşusunda üç eylemli giriş denemesi bu yüzden 47 sn sürdü;
|
|
562
|
-
* sentetik ölçümde sayfa 0.3 sn'de hazırken aynı çağrı 48 sn'ydi — süre sayfanın değil, bu
|
|
563
|
-
* döngünün. Hazırlık artık TEK anlık görüntüden okunur: akış sayacı + Studio'nun yükleme
|
|
564
|
-
* iskeleti (`ViewDetail.svelte` → `.view-detail > .bx--skeleton__placeholder`) + runtime
|
|
565
|
-
* düğümleri (`[data-element-id]`) + hata/bulunamadı hâli + adres. Karar `decideReadiness`'ta.
|
|
566
|
-
*
|
|
567
|
-
* Yüklenmemiş kare ASLA dönmez: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla düşer
|
|
568
|
-
* (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
|
|
569
|
-
* neyin görüldüğünü zaten söyler.
|
|
570
|
-
*/
|
|
571
|
-
export const readinessScript = `(function () { /* readinessSnapshot */
|
|
572
|
-
var activity = window.__ngAppsFlowActivity;
|
|
573
|
-
return {
|
|
574
|
-
activity: activity && typeof activity.running === 'number' && typeof activity.completed === 'number' ? { running: activity.running, completed: activity.completed } : null,
|
|
575
|
-
runtimeNodes: document.querySelectorAll('[data-element-id]').length,
|
|
576
|
-
skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
|
|
577
|
-
viewDetail: !!document.querySelector('.view-detail'),
|
|
578
|
-
errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
|
|
579
|
-
readyState: String(document.readyState),
|
|
580
|
-
href: String(location.href)
|
|
581
|
-
};
|
|
582
|
-
})()`;
|
|
583
|
-
/** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
|
|
584
|
-
export function isFlowActivity(activity) {
|
|
585
|
-
if (!activity || typeof activity !== 'object')
|
|
586
|
-
return false;
|
|
587
|
-
const { running, completed } = activity;
|
|
588
|
-
return typeof running === 'number' && typeof completed === 'number';
|
|
589
|
-
}
|
|
590
|
-
/** 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. */
|
|
591
|
-
export function parseReadiness(raw) {
|
|
592
|
-
if (!raw || typeof raw !== 'object')
|
|
593
|
-
return undefined;
|
|
594
|
-
const value = raw;
|
|
595
|
-
if (typeof value.runtimeNodes !== 'number' || typeof value.href !== 'string')
|
|
596
|
-
return undefined;
|
|
597
|
-
return {
|
|
598
|
-
activity: isFlowActivity(value.activity) ? { running: value.activity.running, completed: value.activity.completed } : null,
|
|
599
|
-
runtimeNodes: value.runtimeNodes,
|
|
600
|
-
skeleton: value.skeleton === true,
|
|
601
|
-
viewDetail: value.viewDetail === true,
|
|
602
|
-
errorState: value.errorState === true,
|
|
603
|
-
readyState: String(value.readyState ?? ''),
|
|
604
|
-
href: value.href
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
/** `/dev` ve `/view` aynı sayfadır (Studio `hooks.ts` reroute); yönlendirme kararı yalnız yoldan verilir. */
|
|
608
|
-
export function runtimePath(url) {
|
|
609
|
-
try {
|
|
610
|
-
return new URL(url).pathname.replace(/^\/dev\//, '/view/').replace(/\/+$/, '');
|
|
611
|
-
}
|
|
612
|
-
catch {
|
|
613
|
-
return url;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const maxCaptureHeight = 16384;
|
|
617
|
-
const navigationTimeoutMs = 20000;
|
|
618
|
-
const apiResponseTimeoutMs = 15000;
|
|
619
|
-
const settleMs = 600;
|
|
620
|
-
/** Akış bittikten sonra Svelte'in son render'ına bırakılan pay. */
|
|
621
|
-
const flowSettleGraceMs = 250;
|
|
622
|
-
const flowPollMs = 120;
|
|
623
|
-
/** "Hiç akış koşmadı" kararı için DOM'un kaç yoklama değişmemesi gerekir (~600 ms = eski settleMs). */
|
|
624
|
-
const quietPolls = 5;
|
|
625
|
-
/** Sayaç yayınlanmayan runtime'da tek kanıt DOM durulmasıdır; daha uzun beklenir (~1 sn). */
|
|
626
|
-
const legacyQuietPolls = 8;
|
|
627
|
-
/** Runtime dışına yönlendirme: hedef sayfanın kendi JS'i çizmeyi bitirsin (~1.5 sn). */
|
|
628
|
-
const redirectQuietPolls = 12;
|
|
629
|
-
const idleCloseMs = 60000;
|
|
630
|
-
/** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
|
|
631
|
-
const maxActionWaitMs = 15000;
|
|
632
|
-
/** Viewport büyütmesinden sonra yeniden yerleşme için tanınan üst süre (B-4). */
|
|
633
|
-
const resizeSettleBudgetMs = 3000;
|
|
634
|
-
/**
|
|
635
|
-
* Navigasyon zaman aşımında toplam deneme sayısı. Soğuk Studio'da ilk `page.goto` Vite
|
|
636
|
-
* derlemesini ISITIR; ikinci deneme neredeyse her zaman geçer (09-08 S-8: aynı kare üç
|
|
637
|
-
* çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
|
|
638
|
-
*/
|
|
639
|
-
const navigationAttempts = 2;
|
|
640
|
-
/**
|
|
641
|
-
* Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
|
|
642
|
-
* hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
|
|
643
|
-
* çağırana yanlış ipucu verir.
|
|
644
|
-
*/
|
|
645
|
-
function isNavigationTimeout(message) {
|
|
646
|
-
return /timeout/i.test(message);
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Tek anlık görüntüden "kare alınabilir" kararı; iki uçta da yanlış beklememek için tek yerde.
|
|
650
|
-
* `quiet` görüntünün kaç yoklamadır değişmediğidir, `completedBefore` bu beklemenin başındaki
|
|
651
|
-
* bitmiş akış sayısı (sayfa değiştiyse çağıran sıfırlar). İskelet görünüyorsa hiçbir karar yok.
|
|
652
|
-
*/
|
|
653
|
-
export function decideReadiness(snapshot, quiet, completedBefore, baselineUrl) {
|
|
654
|
-
if (snapshot.skeleton)
|
|
655
|
-
return undefined;
|
|
656
|
-
const { activity } = snapshot;
|
|
657
|
-
const idle = activity !== null && activity.running === 0;
|
|
658
|
-
if (idle && activity.completed > completedBefore && snapshot.runtimeNodes > 0)
|
|
659
|
-
return 'ready';
|
|
660
|
-
if (idle && snapshot.runtimeNodes > 0 && quiet >= quietPolls)
|
|
661
|
-
return 'ready-no-flows';
|
|
662
|
-
if (activity === null && snapshot.runtimeNodes > 0 && quiet >= legacyQuietPolls)
|
|
663
|
-
return 'ready-no-counter';
|
|
664
|
-
if (idle && snapshot.errorState && quiet >= quietPolls)
|
|
665
|
-
return 'error';
|
|
666
|
-
if (!snapshot.viewDetail && snapshot.readyState === 'complete' && baselineUrl !== undefined && runtimePath(snapshot.href) !== runtimePath(baselineUrl) && quiet >= redirectQuietPolls)
|
|
667
|
-
return 'redirected';
|
|
668
|
-
return undefined;
|
|
669
|
-
}
|
|
670
|
-
/** Hata mesajı için: bütçe dolduğunda sayfada NE görüldü. */
|
|
671
|
-
export function describeUnready(readiness, apiNote) {
|
|
672
|
-
const snapshot = readiness.snapshot;
|
|
673
|
-
const suffix = apiNote === undefined ? '' : ` · ${apiNote}`;
|
|
674
|
-
if (!snapshot)
|
|
675
|
-
return `tarayıcı sayfayı hiç değerlendiremedi (sürekli yönlendirme ya da bağlam düştü)${suffix}`;
|
|
676
|
-
if (snapshot.skeleton)
|
|
677
|
-
return `sayfa hâlâ yükleme iskeletinde${suffix}`;
|
|
678
|
-
if (snapshot.activity && snapshot.activity.running > 0)
|
|
679
|
-
return `${snapshot.activity.running} akış hâlâ koşuyor (${snapshot.activity.completed} bitti)${suffix}`;
|
|
680
|
-
if (snapshot.runtimeNodes === 0)
|
|
681
|
-
return `runtime hiç render olmadı — data-element-id taşıyan düğüm yok${snapshot.viewDetail ? '' : ', .view-detail kabı da yok'}${suffix}`;
|
|
682
|
-
return `sayfa durulmadı (${snapshot.runtimeNodes} düğüm, sayaç ${snapshot.activity ? `${snapshot.activity.running} koşan / ${snapshot.activity.completed} biten` : 'yok'})${suffix}`;
|
|
683
|
-
}
|
|
684
|
-
async function measureContentHeight(page) {
|
|
685
|
-
const measured = await page.evaluate(contentHeightScript).catch(() => undefined);
|
|
686
|
-
const height = measured?.contentHeight;
|
|
687
|
-
return typeof height === 'number' && Number.isFinite(height) && height > 0 ? Math.ceil(height) : undefined;
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
|
-
* Kırpma kutusunu çözer. Bulunamayan ya da çizilmemiş düğüm BOŞ KARE değil HATA döndürür:
|
|
691
|
-
* "yan sütunu kırptım" deyip bomboş bir PNG almak sessiz yanlıştır.
|
|
692
|
-
*/
|
|
693
|
-
async function resolveClip(page, clip) {
|
|
694
|
-
if (!('element' in clip))
|
|
695
|
-
return clip;
|
|
696
|
-
const raw = await page.evaluate(clipRectScript(clip.element)).catch(() => null);
|
|
697
|
-
if (raw === null || raw === undefined) {
|
|
698
|
-
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.`);
|
|
699
|
-
}
|
|
700
|
-
if (!(raw.width > 0 && raw.height > 0)) {
|
|
701
|
-
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.`);
|
|
702
|
-
}
|
|
703
|
-
return { x: Math.round(raw.x), y: Math.round(raw.y), width: Math.round(raw.width), height: Math.round(raw.height) };
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
|
|
707
|
-
* 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
|
|
708
|
-
* (çağrı #204/#205, 64 sn kayıp, ajan yeniden giriş yapmak zorunda kaldı — `S-3`). Kök neden
|
|
709
|
-
* iki yerdeydi: (1) paylaşılan tarayıcı üstünde eşzamanlı bağlam açma, (2) her çekimin `finally`
|
|
710
|
-
* bloğunda ÖNCEKİ boşta-kapatma zamanlayıcısı temizlenmeden yenisinin kurulması — kaçan
|
|
711
|
-
* zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu.
|
|
712
|
-
*
|
|
713
|
-
* Çözüm bir seri kuyruk: çekimler tek tek koşar, bekleyen çağrı `queuedMs` ile ne kadar
|
|
714
|
-
* beklediğini söyler. Sıcak tarayıcı havuzu bu işin dışıdır (Ç2 · tur ekonomisi).
|
|
715
|
-
*/
|
|
716
|
-
export class StudioScreenshotter {
|
|
717
|
-
config;
|
|
718
|
-
launcher;
|
|
719
|
-
browserPromise;
|
|
720
|
-
idleTimer;
|
|
721
|
-
queue = Promise.resolve();
|
|
722
|
-
pending = 0;
|
|
723
|
-
recycleNext = false;
|
|
724
|
-
constructor(config, launcher) {
|
|
725
|
-
this.config = config;
|
|
726
|
-
this.launcher = launcher;
|
|
727
|
-
}
|
|
728
|
-
/**
|
|
729
|
-
* Kuyruk sınırsız birikmesin: bekleme üst sınırı aşılırsa çağrı tarayıcıya HİÇ girmeden
|
|
730
|
-
* anlaşılır bir hatayla döner ve sırasını öndekine zincirleyip çıkar, böylece kuyruğun geri
|
|
731
|
-
* kalanı serileşmiş kalır (Kısıt: tek yavaş çekimin arkasında kuyruk şişmemeli).
|
|
732
|
-
*/
|
|
733
|
-
async capture(request) {
|
|
734
|
-
const queuedAt = Date.now();
|
|
735
|
-
// Sayaç SENKRON artar: `Promise.all` ile aynı turda gelen iki çağrıda ikincisi beklediğini
|
|
736
|
-
// ancak böyle bilebilir (async bir bayrak o turda henüz kurulmamış olurdu).
|
|
737
|
-
const waited = this.pending > 0;
|
|
738
|
-
const previous = this.queue;
|
|
739
|
-
this.pending += 1;
|
|
740
|
-
let release;
|
|
741
|
-
this.queue = new Promise(resolve => {
|
|
742
|
-
release = resolve;
|
|
743
|
-
});
|
|
744
|
-
let timer;
|
|
745
|
-
const capExceeded = await Promise.race([
|
|
746
|
-
previous.then(() => false, () => false),
|
|
747
|
-
new Promise(resolve => {
|
|
748
|
-
timer = setTimeout(() => resolve(true), this.config.screenshotQueueWaitMs);
|
|
749
|
-
timer.unref?.();
|
|
750
|
-
})
|
|
751
|
-
]);
|
|
752
|
-
if (timer)
|
|
753
|
-
clearTimeout(timer);
|
|
754
|
-
if (capExceeded) {
|
|
755
|
-
this.pending -= 1;
|
|
756
|
-
void previous.then(release, release);
|
|
757
|
-
throw new PlatformApiError(`Ekran görüntüsü kuyruğunda ${Date.now() - queuedAt} ms beklendi ve üst sınır (${this.config.screenshotQueueWaitMs} ms) aşıldı — çekim başlatılmadı. Çekimleri seri çağırın ya da SCREENSHOT_QUEUE_WAIT_MS ile sınırı yükseltin.`, 503);
|
|
758
|
-
}
|
|
759
|
-
const queuedMs = Date.now() - queuedAt;
|
|
760
|
-
const recycled = this.recycleNext;
|
|
761
|
-
this.recycleNext = false;
|
|
762
|
-
if (waited)
|
|
763
|
-
logStep('tarayıcı ⏸', `çekim kuyrukta ${queuedMs} ms bekledi — eşzamanlı çekimler seri koşar`);
|
|
764
|
-
try {
|
|
765
|
-
const result = await this.captureOne(request);
|
|
766
|
-
return {
|
|
767
|
-
...result,
|
|
768
|
-
...(waited ? { queuedMs: Math.max(queuedMs, 1) } : {}),
|
|
769
|
-
...(recycled ? { contextRecycled: true } : {})
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
catch (error) {
|
|
773
|
-
// Bağlam/tarayıcı bozulduysa bir sonraki çağrı TEMİZ bir tarayıcıyla başlasın; hata
|
|
774
|
-
// kuyruktaki komşuya bulaşmasın.
|
|
775
|
-
if (/browser has been closed|target (page|closed)|context.*closed/i.test(error instanceof Error ? error.message : String(error))) {
|
|
776
|
-
this.recycleNext = true;
|
|
777
|
-
const expired = this.browserPromise;
|
|
778
|
-
this.browserPromise = undefined;
|
|
779
|
-
expired?.then(instance => instance.close()).catch(() => undefined);
|
|
780
|
-
}
|
|
781
|
-
throw error;
|
|
782
|
-
}
|
|
783
|
-
finally {
|
|
784
|
-
this.pending -= 1;
|
|
785
|
-
release();
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
async launch() {
|
|
789
|
-
if (this.launcher)
|
|
790
|
-
return this.launcher();
|
|
791
|
-
let chromium;
|
|
792
|
-
try {
|
|
793
|
-
chromium = (await import('playwright-core')).chromium;
|
|
794
|
-
}
|
|
795
|
-
catch {
|
|
796
|
-
throw new PlatformApiError('playwright-core paketi yüklü değil — zeyrek-mcp dizininde `npm install playwright-core` çalıştırın, ardından tarayıcı için `npx playwright-core install chromium`.');
|
|
797
|
-
}
|
|
798
|
-
logStep('tarayıcı →', 'chromium başlatılıyor (ilk çağrı — sonraki çağrılar önbellekteki tarayıcıyı kullanır)');
|
|
799
|
-
try {
|
|
800
|
-
return await chromium.launch({
|
|
801
|
-
headless: true,
|
|
802
|
-
...(this.config.screenshotBrowserPath ? { executablePath: this.config.screenshotBrowserPath } : {})
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
catch (firstError) {
|
|
806
|
-
try {
|
|
807
|
-
return await chromium.launch({ headless: true, channel: 'chrome' });
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
const message = firstError instanceof Error ? firstError.message : String(firstError);
|
|
811
|
-
throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Bekleme stratejisi bilinçli olarak `networkidle` DEĞİL: Studio'nun kök layout'u
|
|
817
|
-
* office-lite.zeyrek.dev'den dış bir script yükler ve ağ hiç durulmayabilir. SSR kabuğu yalnız
|
|
818
|
-
* spinner bastığı için domcontentloaded da tek başına yetmez. Bunun yerine sayfanın kendi veri
|
|
819
|
-
* isteği (`/{app}/p/{slug}`) gözlenir; yanıt hiç gelmezse bu hata SAYILMAZ — renderWaitTimedOut
|
|
820
|
-
* işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
|
|
821
|
-
* yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
|
|
822
|
-
*/
|
|
823
|
-
async runActions(page, request) {
|
|
824
|
-
if (!request.actions || request.actions.length === 0)
|
|
825
|
-
return undefined;
|
|
826
|
-
const outcomes = [];
|
|
827
|
-
for (const [index, action] of request.actions.entries()) {
|
|
828
|
-
if (action.action === 'wait') {
|
|
829
|
-
await page.waitForTimeout(Math.min(Math.max(Number(action.ms ?? settleMs), 0), maxActionWaitMs));
|
|
830
|
-
outcomes.push({ index, action: 'wait', found: true, applied: true, requests: 0 });
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
834
|
-
const activityBefore = snapshotBefore?.activity ?? null;
|
|
835
|
-
const raw = await page.evaluate(actionScript(action)).catch(error => ({
|
|
836
|
-
found: false,
|
|
837
|
-
applied: false,
|
|
838
|
-
note: error instanceof Error ? error.message : String(error),
|
|
839
|
-
requests: 0
|
|
840
|
-
}));
|
|
841
|
-
const before = (raw ?? {});
|
|
842
|
-
// Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
|
|
843
|
-
// iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
|
|
844
|
-
// kendisindedir, şemayı okumaya yönlendirilir (P1-3 Step 3).
|
|
845
|
-
if (before.found === false && !before.note && typeof action.target === 'string') {
|
|
846
|
-
before.note = action.target.includes(INTERNAL_ROLE_SEPARATOR)
|
|
847
|
-
? `'${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.`
|
|
848
|
-
: `'${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).`;
|
|
849
|
-
}
|
|
850
|
-
// Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
|
|
851
|
-
// sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
|
|
852
|
-
//
|
|
853
|
-
// Bekleme ana yükleme döngüsüyle AYNI hazırlık kararıdır: akış koştuysa bitince, hiç akış
|
|
854
|
-
// tetiklenmediyse (metin alanına `set`) DOM durulunca, eylem sayfayı değiştirdiyse yeni
|
|
855
|
-
// sayfa hazır olunca çıkar. Eskiden akış tetiklemeyen her eylem `settleMs + delayMs`'in
|
|
856
|
-
// tamamını bekliyordu: 09-06·2 koşusunda üç eylemli giriş denemesi 47 sn (B-6).
|
|
857
|
-
const readiness = await this.waitForReady(page, {
|
|
858
|
-
budgetMs: apiResponseTimeoutMs + request.delayMs,
|
|
859
|
-
baselineUrl: snapshotBefore?.href ?? request.url,
|
|
860
|
-
completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
|
|
861
|
-
});
|
|
862
|
-
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
863
|
-
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);
|
|
864
|
-
}
|
|
865
|
-
const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
|
|
866
|
-
const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
|
|
867
|
-
// Yerel bir akış (yalnız prop yazan setter) hiç uca gitmez ama işini YAPAR. Sayaç bu ayrımı
|
|
868
|
-
// veren tek kanıttır; sayaç yoksa flowRuns bilinmez kalır. Eylem sayfayı değiştirdiyse
|
|
869
|
-
// yeni sayfanın sayacı sıfırdan başlar, orada biten her akış bu eyleme yazılır.
|
|
870
|
-
const activityAfter = readiness.snapshot?.activity ?? null;
|
|
871
|
-
const navigated = snapshotBefore !== undefined && readiness.snapshot !== undefined && runtimePath(readiness.snapshot.href) !== runtimePath(snapshotBefore.href);
|
|
872
|
-
const flowRuns = activityAfter === null
|
|
873
|
-
? undefined
|
|
874
|
-
: navigated
|
|
875
|
-
? activityAfter.completed
|
|
876
|
-
: activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
|
|
877
|
-
// `tab` adımının KANITI tıklama değil, sonuçta hangi sekmenin açık olduğudur: bileşen
|
|
878
|
-
// seçimi kendi prop'una yazmadıysa kare hâlâ eski sekmeyi gösterir ve rapor bunu söyler.
|
|
879
|
-
const activeTab = action.action === 'tab' && typeof action.target === 'string'
|
|
880
|
-
? await page.evaluate(activeTabScript(action.target)).catch(() => undefined)
|
|
881
|
-
: undefined;
|
|
882
|
-
// G-14 — DOM'a yazmak değerin prop'a ulaştığını kanıtlamaz. İz yalnız okuyucu yayınlanıyor ve
|
|
883
|
-
// makine düğümü tanıyorsa (dize) karşılaştırılır; aksi hâlde propWritten bilinmez kalır.
|
|
884
|
-
const propsAfter = typeof before.propsBefore === 'string' && before.applied === true && !navigated
|
|
885
|
-
? await page.evaluate(runtimePropsScript(rootElementId(String(action.target ?? '')))).catch(() => undefined)
|
|
886
|
-
: undefined;
|
|
887
|
-
const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
|
|
888
|
-
const applied = before.applied === true && propWritten !== false;
|
|
889
|
-
const note = propWritten === false
|
|
890
|
-
? `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).`
|
|
891
|
-
: before.note;
|
|
892
|
-
outcomes.push({
|
|
893
|
-
index,
|
|
894
|
-
action: String(action.action),
|
|
895
|
-
...(action.target === undefined ? {} : { target: action.target }),
|
|
896
|
-
...(action.to === undefined ? {} : { to: action.to }),
|
|
897
|
-
found: before.found === true,
|
|
898
|
-
applied,
|
|
899
|
-
...(propWritten === undefined ? {} : { propWritten }),
|
|
900
|
-
...(note === undefined ? {} : { note }),
|
|
901
|
-
requests,
|
|
902
|
-
...(flowRuns === undefined ? {} : { flowRuns }),
|
|
903
|
-
...(before.matchedBy === undefined ? {} : { matchedBy: before.matchedBy }),
|
|
904
|
-
...(typeof activeTab === 'string' ? { activeTab } : {}),
|
|
905
|
-
...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
|
|
906
|
-
...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
|
|
907
|
-
...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
|
|
908
|
-
pageState: readiness.state
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
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`));
|
|
912
|
-
return outcomes;
|
|
913
|
-
}
|
|
914
|
-
/**
|
|
915
|
-
* Sayfa hazır olana kadar yoklar; hazır kararını `decideReadiness` verir. Karar verildikten
|
|
916
|
-
* sonra kısa bir pay bırakılır ve görüntü BİR KEZ DAHA doğrulanır: giriş akışının son adımı
|
|
917
|
-
* yönlendirmeyse "bitti" sinyali yeni sayfa açılırken gelir ve doğrulama olmadan kare o
|
|
918
|
-
* geçişte alınırdı. Adres değiştiyse sayaç yeni sayfada sıfırdan başlar; baz da sıfırlanır.
|
|
919
|
-
*
|
|
920
|
-
* Döngü hem süreyle hem TUR sayısıyla sınırlıdır: sahte saatle koşan testte `Date.now()`
|
|
921
|
-
* ilerlemez ve yalnız süreye bakan bir döngü hiç bitmezdi.
|
|
922
|
-
*/
|
|
923
|
-
async waitForReady(page, options) {
|
|
924
|
-
const startedAt = Date.now();
|
|
925
|
-
const deadline = startedAt + options.budgetMs;
|
|
926
|
-
const maxPolls = Math.ceil(options.budgetMs / flowPollMs) + 1;
|
|
927
|
-
let completedBefore = options.completedBefore;
|
|
928
|
-
let previousKey;
|
|
929
|
-
let quiet = 0;
|
|
930
|
-
let last;
|
|
931
|
-
for (let poll = 0; poll < maxPolls; poll++) {
|
|
932
|
-
const snapshot = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
933
|
-
if (snapshot === undefined) {
|
|
934
|
-
// Yönlendirme sırasında evaluate düşer: sessizlik sayacı sıfırlanır, yoklama sürer.
|
|
935
|
-
previousKey = undefined;
|
|
936
|
-
quiet = 0;
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
last = snapshot;
|
|
940
|
-
if (runtimePath(snapshot.href) !== runtimePath(options.baselineUrl))
|
|
941
|
-
completedBefore = 0;
|
|
942
|
-
const key = JSON.stringify(snapshot);
|
|
943
|
-
quiet = key === previousKey ? quiet + 1 : 0;
|
|
944
|
-
previousKey = key;
|
|
945
|
-
const state = decideReadiness(snapshot, quiet, completedBefore, options.baselineUrl);
|
|
946
|
-
if (state !== undefined) {
|
|
947
|
-
await page.waitForTimeout(flowSettleGraceMs);
|
|
948
|
-
const confirmed = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
|
|
949
|
-
if (confirmed !== undefined && !confirmed.skeleton && confirmed.href === snapshot.href && (confirmed.activity === null || confirmed.activity.running === 0)) {
|
|
950
|
-
return { state, waitedMs: Date.now() - startedAt, snapshot: confirmed };
|
|
951
|
-
}
|
|
952
|
-
previousKey = undefined;
|
|
953
|
-
quiet = 0;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
if (Date.now() >= deadline || poll === maxPolls - 1)
|
|
957
|
-
break;
|
|
958
|
-
await page.waitForTimeout(flowPollMs);
|
|
959
|
-
}
|
|
960
|
-
return { state: 'not-ready', waitedMs: Date.now() - startedAt, ...(last === undefined ? {} : { snapshot: last }) };
|
|
961
|
-
}
|
|
962
|
-
/**
|
|
963
|
-
* Viewport büyütmesinden sonra sayfanın YENİDEN YERLEŞMESİNİ bekler. Eski sırada (ölç → büyüt →
|
|
964
|
-
* eylemler) büyütme sonrası render'ı her eylemin kendi hazırlık döngüsü soğuruyordu; sıra
|
|
965
|
-
* eylemlerden sonraya alınınca geriye yalnız 600 ms kalmıştı ve `onResize`'a bağlı ya da
|
|
966
|
-
* sanallaştırılmış bir tablo o pay içinde yeni satırlarını çizmeyebilirdi — özet ve kare
|
|
967
|
-
* büyütme ÖNCESİ gerçeği anlatırdı (B-4).
|
|
968
|
-
*/
|
|
969
|
-
async settleAfterResize(page, baselineUrl) {
|
|
970
|
-
await page.waitForTimeout(settleMs);
|
|
971
|
-
// Yeni akış beklenmiyor: karar DOM durulmasından çıkar (`ready-no-flows` / `ready-no-counter`).
|
|
972
|
-
await this.waitForReady(page, { budgetMs: resizeSettleBudgetMs, baselineUrl, completedBefore: Number.POSITIVE_INFINITY });
|
|
973
|
-
}
|
|
974
|
-
async captureOne(request) {
|
|
975
|
-
const startedAt = Date.now();
|
|
976
|
-
if (this.idleTimer)
|
|
977
|
-
clearTimeout(this.idleTimer);
|
|
978
|
-
this.browserPromise ??= this.launch();
|
|
979
|
-
let browser = await this.browserPromise.catch(error => {
|
|
980
|
-
this.browserPromise = undefined;
|
|
981
|
-
throw error;
|
|
982
|
-
});
|
|
983
|
-
let context;
|
|
984
|
-
try {
|
|
985
|
-
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
|
|
986
|
-
}
|
|
987
|
-
catch {
|
|
988
|
-
this.browserPromise = this.launch();
|
|
989
|
-
browser = await this.browserPromise.catch(error => {
|
|
990
|
-
this.browserPromise = undefined;
|
|
991
|
-
throw error;
|
|
992
|
-
});
|
|
993
|
-
context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
|
|
994
|
-
}
|
|
995
|
-
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}` : ''}`);
|
|
996
|
-
try {
|
|
997
|
-
const cookies = new Map();
|
|
998
|
-
if (request.token)
|
|
999
|
-
cookies.set('NG_AccessToken', request.token);
|
|
1000
|
-
for (const cookie of request.cookies ?? [])
|
|
1001
|
-
cookies.set(cookie.name, cookie.value);
|
|
1002
|
-
if (cookies.size > 0) {
|
|
1003
|
-
await context.addCookies([...cookies].map(([name, value]) => ({ name, value, url: request.cookieUrl, httpOnly: false })));
|
|
1004
|
-
}
|
|
1005
|
-
// Tema tercihi çerezde değil localStorage'da yaşar ve tema store'u değerini modül İLK
|
|
1006
|
-
// değerlendirildiğinde okur: sayfa açıldıktan sonra yazmak bir sonraki yüklemeye kadar
|
|
1007
|
-
// etkisizdir. Bu yüzden tercih, sayfanın script'lerinden önce koşan bağlam script'iyle
|
|
1008
|
-
// ekilir. Yeteneği olmayan bağlamda çağrı SESSİZCE açık temada kare almaz, hata verir.
|
|
1009
|
-
if (request.colorScheme) {
|
|
1010
|
-
if (!context.addInitScript) {
|
|
1011
|
-
throw new PlatformApiError(`colorScheme='${request.colorScheme}' uygulanamadı: tarayıcı bağlamı addInitScript desteklemiyor. Tema tercihi sayfa açılmadan ekilmek zorunda; desteksiz bağlamda kare projenin varsayılan modunda çıkardı ve bu sessizce yanlış olurdu.`);
|
|
1012
|
-
}
|
|
1013
|
-
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 */ }`);
|
|
1014
|
-
}
|
|
1015
|
-
const page = await context.newPage();
|
|
1016
|
-
// Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
|
|
1017
|
-
// bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
|
|
1018
|
-
// yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
|
|
1019
|
-
const readyBudgetMs = apiResponseTimeoutMs + request.delayMs;
|
|
1020
|
-
const watchApiResponse = () => page
|
|
1021
|
-
.waitForResponse(response => response.url().includes(request.apiPathMarker), { timeout: readyBudgetMs })
|
|
1022
|
-
.then(response => response.status(), () => undefined);
|
|
1023
|
-
// `delayMs` navigasyon bütçesini de uzatır. Eskiden sabit 20 sn'ydi ve soğuk Studio'da
|
|
1024
|
-
// ajanın elinde tek çıkış çağrıyı tekrarlamaktı (09-08 S-8) — "sabrım bu kadar" demenin
|
|
1025
|
-
// tek anlamı olsun diye iki bütçe de aynı düğmeye bağlı.
|
|
1026
|
-
const navigationTimeout = navigationTimeoutMs + request.delayMs;
|
|
1027
|
-
let observedApiStatus = watchApiResponse();
|
|
1028
|
-
let navigationRetried = false;
|
|
1029
|
-
for (let attempt = 1;; attempt++) {
|
|
1030
|
-
try {
|
|
1031
|
-
await page.goto(request.url, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });
|
|
1032
|
-
break;
|
|
1033
|
-
}
|
|
1034
|
-
catch (error) {
|
|
1035
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1036
|
-
const timedOut = isNavigationTimeout(message);
|
|
1037
|
-
if (!timedOut || attempt >= navigationAttempts) {
|
|
1038
|
-
const attemptNote = attempt > 1 ? ` (${attempt} deneme)` : '';
|
|
1039
|
-
const budgetNote = timedOut
|
|
1040
|
-
? ` Navigasyon bütçesi ${navigationTimeout} ms'ydi; soğuk Studio'da delayMs'i büyütün — o değer artık navigasyonu da uzatır.`
|
|
1041
|
-
: '';
|
|
1042
|
-
throw new PlatformApiError(`Studio'ya ulaşılamadı${attemptNote}: ${request.url} — ${message}.${budgetNote} STUDIO_BASE_URL doğru mu, arayüz ayakta mı? (yerelde zeyrek-frontend/apps/studio içinde \`pnpm dev\`)`);
|
|
1043
|
-
}
|
|
1044
|
-
navigationRetried = true;
|
|
1045
|
-
// Veri isteği gözlemi de sıfırlanır: ilk denemeyi bekleyen sayaç çoktan dolmuş olur ve
|
|
1046
|
-
// yeniden denenen navigasyonun yanıtını kaçırıp boş loader karesi damgalardı.
|
|
1047
|
-
observedApiStatus = watchApiResponse();
|
|
1048
|
-
logStep('tarayıcı ⟳', `navigasyon ${navigationTimeout} ms'de düştü, bir kez daha deneniyor — ilk deneme soğuk derlemeyi ısıttı`);
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
const readiness = await this.waitForReady(page, { budgetMs: readyBudgetMs, baselineUrl: request.url, completedBefore: 0 });
|
|
1052
|
-
// Gözlendiyse dolu, hâlâ bekleniyorsa (yönlendirme, hata) undefined — ikinci vaat hemen çözülür.
|
|
1053
|
-
const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
|
|
1054
|
-
const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
|
|
1055
|
-
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
1056
|
-
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);
|
|
1057
|
-
}
|
|
1058
|
-
await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
|
|
1059
|
-
const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
|
|
1060
|
-
? true
|
|
1061
|
-
: readiness.state === 'not-ready' ? false : undefined;
|
|
1062
|
-
const finalHref = readiness.snapshot?.href;
|
|
1063
|
-
const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
|
|
1064
|
-
const actions = await this.runActions(page, request);
|
|
1065
|
-
// Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
|
|
1066
|
-
// boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
|
|
1067
|
-
// tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
|
|
1068
|
-
// (S-1, 2026-09-16 raporu madde 6). Yalnız YÜKSEKLİK büyür, genişlik sabittir: `hideOn`
|
|
1069
|
-
// kırılım noktaları ve duyarlı yerleşim kareyi çekerken kaymaz (K4).
|
|
1070
|
-
const measured = request.fullPage ? await measureContentHeight(page) : undefined;
|
|
1071
|
-
const captureHeight = measured === undefined ? undefined : Math.min(measured, maxCaptureHeight);
|
|
1072
|
-
let viewportHeight = request.viewport.height;
|
|
1073
|
-
if (captureHeight !== undefined && captureHeight > viewportHeight) {
|
|
1074
|
-
await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
|
|
1075
|
-
viewportHeight = captureHeight;
|
|
1076
|
-
await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
|
|
1077
|
-
}
|
|
1078
|
-
// K7 — kırpma kutusu katlanmış alanın ALTINDAYSA kare boş çıkardı: viewport kutuyu
|
|
1079
|
-
// kapsayana kadar büyütülür (fullPage ile aynı yol) ve kutu YENİDEN okunur, çünkü
|
|
1080
|
-
// büyüyen viewport yerleşimi kaydırabilir.
|
|
1081
|
-
let clip;
|
|
1082
|
-
if (request.clip) {
|
|
1083
|
-
clip = await resolveClip(page, request.clip);
|
|
1084
|
-
if (clip.y + clip.height > viewportHeight) {
|
|
1085
|
-
const wanted = Math.ceil(clip.y + clip.height) + 8;
|
|
1086
|
-
if (wanted > maxCaptureHeight) {
|
|
1087
|
-
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.`);
|
|
1088
|
-
}
|
|
1089
|
-
const grown = Math.min(wanted, maxCaptureHeight);
|
|
1090
|
-
await page.setViewportSize({ width: request.viewport.width, height: grown });
|
|
1091
|
-
viewportHeight = grown;
|
|
1092
|
-
await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
|
|
1093
|
-
clip = await resolveClip(page, request.clip);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
const layout = request.layoutReport === true
|
|
1097
|
-
? await page.evaluate(layoutReportScript).then(value => Array.isArray(value) ? value : []).catch(() => [])
|
|
1098
|
-
: undefined;
|
|
1099
|
-
const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
|
|
1100
|
-
.then(value => Array.isArray(value) ? value : [])
|
|
1101
|
-
.catch(() => []);
|
|
1102
|
-
// Özet kareden ÖNCE ve büyütmeden SONRA: ikisi de aynı yerleşmiş sayfayı anlatsın.
|
|
1103
|
-
const summaryRaw = request.summary === false
|
|
1104
|
-
? undefined
|
|
1105
|
-
: await page.evaluate(domSummaryScript(request.viewport.height)).then(value => ({ value })).catch(error => ({ error: error instanceof Error ? error.message : String(error) }));
|
|
1106
|
-
const summary = summaryRaw && 'value' in summaryRaw ? parseDomSummary(summaryRaw.value) : undefined;
|
|
1107
|
-
const summaryUnavailable = summaryRaw === undefined
|
|
1108
|
-
? undefined
|
|
1109
|
-
: 'error' in summaryRaw
|
|
1110
|
-
? summaryRaw.error
|
|
1111
|
-
: summary === undefined ? 'tarayıcı özet döndürmedi (beklenmeyen biçim)' : undefined;
|
|
1112
|
-
// Olgu doğrulamasında kare HİÇ çekilmez: maliyetin tamamı buradadır (S-5).
|
|
1113
|
-
const format = request.format ?? 'png';
|
|
1114
|
-
const png = request.image === 'none'
|
|
1115
|
-
? undefined
|
|
1116
|
-
: await page.screenshot({
|
|
1117
|
-
type: format,
|
|
1118
|
-
...(format === 'jpeg' && request.quality !== undefined ? { quality: request.quality } : {}),
|
|
1119
|
-
fullPage: false,
|
|
1120
|
-
...(clip === undefined ? {} : { clip })
|
|
1121
|
-
});
|
|
1122
|
-
const elapsedMs = Date.now() - startedAt;
|
|
1123
|
-
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}`}`);
|
|
1124
|
-
if (runtimeErrors.length > 0) {
|
|
1125
|
-
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 ?? ''}`));
|
|
1126
|
-
}
|
|
1127
|
-
return {
|
|
1128
|
-
...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
|
|
1129
|
-
...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
|
|
1130
|
-
...(clip === undefined ? {} : { clip }),
|
|
1131
|
-
...(summary === undefined ? {} : { summary }),
|
|
1132
|
-
...(summaryUnavailable === undefined ? {} : { summaryUnavailable }),
|
|
1133
|
-
apiStatus,
|
|
1134
|
-
renderWaitTimedOut: apiStatus === undefined,
|
|
1135
|
-
elapsedMs,
|
|
1136
|
-
pageState: readiness.state,
|
|
1137
|
-
readyAfterMs: readiness.waitedMs,
|
|
1138
|
-
...(finalUrl === undefined ? {} : { finalUrl }),
|
|
1139
|
-
...(measured === undefined ? {} : { contentHeight: measured }),
|
|
1140
|
-
...(measured !== undefined && measured > maxCaptureHeight ? { fullPageTruncated: true } : {}),
|
|
1141
|
-
...(layout === undefined ? {} : { layout }),
|
|
1142
|
-
...(actions === undefined ? {} : { actions }),
|
|
1143
|
-
...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
|
|
1144
|
-
...(flowsSettled === undefined ? {} : { flowsSettled }),
|
|
1145
|
-
...(navigationRetried ? { navigationRetried: true } : {})
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
finally {
|
|
1149
|
-
await context.close().catch(() => undefined);
|
|
1150
|
-
// Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
|
|
1151
|
-
// kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
|
|
1152
|
-
if (this.idleTimer)
|
|
1153
|
-
clearTimeout(this.idleTimer);
|
|
1154
|
-
this.idleTimer = setTimeout(() => {
|
|
1155
|
-
const expired = this.browserPromise;
|
|
1156
|
-
this.browserPromise = undefined;
|
|
1157
|
-
expired?.then(instance => instance.close()).catch(() => undefined);
|
|
1158
|
-
}, idleCloseMs);
|
|
1159
|
-
this.idleTimer.unref?.();
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
}
|