@shadowcoderr/context-graph 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/analyzers/a11y-extractor.d.ts +15 -0
- package/dist/analyzers/a11y-extractor.d.ts.map +1 -0
- package/dist/analyzers/a11y-extractor.js +148 -0
- package/dist/analyzers/a11y-extractor.js.map +1 -0
- package/dist/analyzers/dom-analyzer.d.ts +20 -0
- package/dist/analyzers/dom-analyzer.d.ts.map +1 -0
- package/dist/analyzers/dom-analyzer.js +126 -0
- package/dist/analyzers/dom-analyzer.js.map +1 -0
- package/dist/analyzers/locator-generator.d.ts +13 -0
- package/dist/analyzers/locator-generator.d.ts.map +1 -0
- package/dist/analyzers/locator-generator.js +381 -0
- package/dist/analyzers/locator-generator.js.map +1 -0
- package/dist/analyzers/network-logger.d.ts +15 -0
- package/dist/analyzers/network-logger.d.ts.map +1 -0
- package/dist/analyzers/network-logger.js +71 -0
- package/dist/analyzers/network-logger.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +155 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/defaults.d.ts +3 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +54 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +3 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +75 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +168 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +104 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/browser-adapter.d.ts +24 -0
- package/dist/core/browser-adapter.d.ts.map +1 -0
- package/dist/core/browser-adapter.js +208 -0
- package/dist/core/browser-adapter.js.map +1 -0
- package/dist/core/capture-engine.d.ts +52 -0
- package/dist/core/capture-engine.d.ts.map +1 -0
- package/dist/core/capture-engine.js +593 -0
- package/dist/core/capture-engine.js.map +1 -0
- package/dist/core/runtime.d.ts +38 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +648 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/prompts/init-prompt.d.ts +12 -0
- package/dist/prompts/init-prompt.d.ts.map +1 -0
- package/dist/prompts/init-prompt.js +128 -0
- package/dist/prompts/init-prompt.js.map +1 -0
- package/dist/registry/components-registry.d.ts +97 -0
- package/dist/registry/components-registry.d.ts.map +1 -0
- package/dist/registry/components-registry.js +469 -0
- package/dist/registry/components-registry.js.map +1 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +7 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/security/patterns.d.ts +4 -0
- package/dist/security/patterns.d.ts.map +1 -0
- package/dist/security/patterns.js +65 -0
- package/dist/security/patterns.js.map +1 -0
- package/dist/security/redactor.d.ts +26 -0
- package/dist/security/redactor.d.ts.map +1 -0
- package/dist/security/redactor.js +128 -0
- package/dist/security/redactor.js.map +1 -0
- package/dist/security/validator.d.ts +11 -0
- package/dist/security/validator.d.ts.map +1 -0
- package/dist/security/validator.js +62 -0
- package/dist/security/validator.js.map +1 -0
- package/dist/storage/engine.d.ts +45 -0
- package/dist/storage/engine.d.ts.map +1 -0
- package/dist/storage/engine.js +479 -0
- package/dist/storage/engine.js.map +1 -0
- package/dist/storage/manifest.d.ts +10 -0
- package/dist/storage/manifest.d.ts.map +1 -0
- package/dist/storage/manifest.js +98 -0
- package/dist/storage/manifest.js.map +1 -0
- package/dist/storage/serializer.d.ts +9 -0
- package/dist/storage/serializer.d.ts.map +1 -0
- package/dist/storage/serializer.js +22 -0
- package/dist/storage/serializer.js.map +1 -0
- package/dist/types/capture.d.ts +206 -0
- package/dist/types/capture.d.ts.map +1 -0
- package/dist/types/capture.js +3 -0
- package/dist/types/capture.js.map +1 -0
- package/dist/types/config.d.ts +63 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/registry.d.ts +94 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/registry.js +3 -0
- package/dist/types/registry.js.map +1 -0
- package/dist/types/storage.d.ts +57 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +3 -0
- package/dist/types/storage.js.map +1 -0
- package/dist/utils/hash.d.ts +3 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +26 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/logger.d.ts +20 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/pom-generator.d.ts +12 -0
- package/dist/utils/pom-generator.d.ts.map +1 -0
- package/dist/utils/pom-generator.js +83 -0
- package/dist/utils/pom-generator.js.map +1 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +51 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Browser, Page } from '@playwright/test';
|
|
2
|
+
import { Config } from '../types/config';
|
|
3
|
+
export declare enum RuntimeMode {
|
|
4
|
+
BROWSER = "browser",
|
|
5
|
+
RECORDER = "recorder"
|
|
6
|
+
}
|
|
7
|
+
export type StartRecorderOptions = {
|
|
8
|
+
captureArtifacts?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare class RuntimeController {
|
|
11
|
+
private config;
|
|
12
|
+
private mode;
|
|
13
|
+
private browserAdapter;
|
|
14
|
+
private captureEngine;
|
|
15
|
+
private storageEngine;
|
|
16
|
+
private componentsRegistry;
|
|
17
|
+
private sessionId;
|
|
18
|
+
private capturedPages;
|
|
19
|
+
private pendingCaptures;
|
|
20
|
+
constructor(config: Config, mode: RuntimeMode);
|
|
21
|
+
initialize(): Promise<void>;
|
|
22
|
+
startRecorder(url: string, options?: StartRecorderOptions): Promise<void>;
|
|
23
|
+
private captureFromRecordedScript;
|
|
24
|
+
private executeRecordedSpecSteps;
|
|
25
|
+
private extractTestBody;
|
|
26
|
+
launchBrowser(): Promise<Browser>;
|
|
27
|
+
createContext(browser: Browser): Promise<any>;
|
|
28
|
+
private setupRecorderMode;
|
|
29
|
+
setupPage(page: Page): Promise<void>;
|
|
30
|
+
private normalizeUrl;
|
|
31
|
+
private captureCurrentPage;
|
|
32
|
+
private isValidUrl;
|
|
33
|
+
shutdown(): Promise<void>;
|
|
34
|
+
onBrowserDisconnect(callback: () => void): void;
|
|
35
|
+
getSessionId(): string;
|
|
36
|
+
capturePageIfNeeded(page: Page): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/core/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAU,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAWzC,oBAAY,WAAW;IACrB,OAAO,YAAY;IACnB,QAAQ,aAAa;CACtB;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,kBAAkB,CAA0C;IAEpE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,aAAa,CAA0B;IAC/C,OAAO,CAAC,eAAe,CAAiC;gBAE5C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW;IAavC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;YAiFrE,yBAAyB;YAYzB,wBAAwB;IAiBtC,OAAO,CAAC,eAAe;IAmCjB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjC,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;YAiBrC,iBAAiB;IAmJzB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA8H1C,OAAO,CAAC,YAAY;YAgBN,kBAAkB;IAgGhC,OAAO,CAAC,UAAU;IASZ,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA0C/B,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAI/C,YAAY,IAAI,MAAM;IAIhB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAsBrD"}
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RuntimeController = exports.RuntimeMode = void 0;
|
|
37
|
+
// Developer: Shadow Coderr, Architect
|
|
38
|
+
const test_1 = require("@playwright/test");
|
|
39
|
+
const browser_adapter_1 = require("./browser-adapter");
|
|
40
|
+
const capture_engine_1 = require("./capture-engine");
|
|
41
|
+
const engine_1 = require("../storage/engine");
|
|
42
|
+
const components_registry_1 = require("../registry/components-registry");
|
|
43
|
+
const hash_1 = require("../utils/hash");
|
|
44
|
+
const logger_1 = require("../utils/logger");
|
|
45
|
+
const child_process_1 = require("child_process");
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
var RuntimeMode;
|
|
49
|
+
(function (RuntimeMode) {
|
|
50
|
+
RuntimeMode["BROWSER"] = "browser";
|
|
51
|
+
RuntimeMode["RECORDER"] = "recorder";
|
|
52
|
+
})(RuntimeMode || (exports.RuntimeMode = RuntimeMode = {}));
|
|
53
|
+
class RuntimeController {
|
|
54
|
+
config;
|
|
55
|
+
mode;
|
|
56
|
+
browserAdapter;
|
|
57
|
+
captureEngine;
|
|
58
|
+
storageEngine;
|
|
59
|
+
componentsRegistry = null;
|
|
60
|
+
sessionId;
|
|
61
|
+
capturedPages = new Set();
|
|
62
|
+
pendingCaptures = new Set();
|
|
63
|
+
constructor(config, mode) {
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.mode = mode;
|
|
66
|
+
this.browserAdapter = new browser_adapter_1.BrowserAdapter();
|
|
67
|
+
this.captureEngine = new capture_engine_1.CaptureEngine(config);
|
|
68
|
+
this.storageEngine = new engine_1.StorageEngine(config.storage.outputDir, config.storage.prettyJson, config.capture.forceCapture);
|
|
69
|
+
this.sessionId = (0, hash_1.generateSessionId)(new Date());
|
|
70
|
+
}
|
|
71
|
+
async initialize() {
|
|
72
|
+
await this.storageEngine.initialize();
|
|
73
|
+
// Initialize components registry manager if enabled
|
|
74
|
+
if (this.config.capture.components?.enabled) {
|
|
75
|
+
this.componentsRegistry = new components_registry_1.ComponentsRegistryManager(this.config.storage.outputDir, 'unknown', {
|
|
76
|
+
minOccurrences: this.config.capture.components.minOccurrences,
|
|
77
|
+
maxComponents: this.config.capture.components.maxComponents,
|
|
78
|
+
});
|
|
79
|
+
await this.componentsRegistry.initialize();
|
|
80
|
+
}
|
|
81
|
+
logger_1.logger.info(`Initialized runtime in ${this.mode} mode`);
|
|
82
|
+
}
|
|
83
|
+
async startRecorder(url, options = {}) {
|
|
84
|
+
const scriptPath = await this.storageEngine.getUniqueScriptPath(url);
|
|
85
|
+
logger_1.logger.info(`Starting Playwright Codegen for: ${url}`);
|
|
86
|
+
logger_1.logger.info(`Script will be saved to: ${scriptPath}`);
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
let cliPath;
|
|
89
|
+
try {
|
|
90
|
+
cliPath = require.resolve('playwright-core/cli');
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
try {
|
|
94
|
+
cliPath = require.resolve('playwright/cli');
|
|
95
|
+
}
|
|
96
|
+
catch (e2) {
|
|
97
|
+
cliPath = path.join(process.cwd(), 'node_modules', 'playwright-core', 'cli.js');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
logger_1.logger.debug(`Using Playwright CLI at: ${cliPath}`);
|
|
101
|
+
logger_1.logger.debug(`Using Playwright channel: msedge`);
|
|
102
|
+
const env = { ...process.env };
|
|
103
|
+
delete env.NODE_OPTIONS;
|
|
104
|
+
const spawnArgs = [
|
|
105
|
+
cliPath,
|
|
106
|
+
'codegen',
|
|
107
|
+
'-b', 'chromium',
|
|
108
|
+
'--channel', 'msedge',
|
|
109
|
+
'--output', scriptPath,
|
|
110
|
+
url,
|
|
111
|
+
];
|
|
112
|
+
logger_1.logger.debug(`Spawning recorder: ${process.execPath} ${spawnArgs.join(' ')}`);
|
|
113
|
+
const recorderProcess = (0, child_process_1.spawn)(process.execPath, spawnArgs, {
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
shell: false,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
cwd: process.cwd(),
|
|
118
|
+
env,
|
|
119
|
+
});
|
|
120
|
+
recorderProcess.on('error', (err) => {
|
|
121
|
+
logger_1.logger.error(`Failed to start recorder: ${err.message}`);
|
|
122
|
+
reject(err);
|
|
123
|
+
});
|
|
124
|
+
recorderProcess.on('close', (code) => {
|
|
125
|
+
if (code === 0) {
|
|
126
|
+
logger_1.logger.info(`Script saved to: ${scriptPath}`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// It's normal for codegen to return non-zero if closed via window X button sometimes
|
|
130
|
+
logger_1.logger.info(`Playwright Codegen session ended (code ${code})`);
|
|
131
|
+
}
|
|
132
|
+
this.storageEngine.mergeRecordedScript(url, scriptPath)
|
|
133
|
+
.then(async (mergedPath) => {
|
|
134
|
+
logger_1.logger.info(`Merged script saved to: ${mergedPath}`);
|
|
135
|
+
if (options.captureArtifacts) {
|
|
136
|
+
try {
|
|
137
|
+
await this.captureFromRecordedScript(mergedPath);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger_1.logger.warn(`Failed to capture artifacts from recorded script: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
resolve();
|
|
144
|
+
})
|
|
145
|
+
.catch((err) => {
|
|
146
|
+
logger_1.logger.warn(`Failed to merge recorded script: ${err.message}`);
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async captureFromRecordedScript(specPath) {
|
|
153
|
+
const browser = await this.launchBrowser();
|
|
154
|
+
const context = await this.createContext(browser);
|
|
155
|
+
const page = await context.newPage();
|
|
156
|
+
await this.setupPage(page);
|
|
157
|
+
await this.executeRecordedSpecSteps(specPath, page);
|
|
158
|
+
await this.shutdown();
|
|
159
|
+
}
|
|
160
|
+
async executeRecordedSpecSteps(specPath, page) {
|
|
161
|
+
const source = fs.readFileSync(specPath, 'utf8');
|
|
162
|
+
const body = this.extractTestBody(source);
|
|
163
|
+
if (!body.trim()) {
|
|
164
|
+
throw new Error(`No executable steps found in script: ${specPath}`);
|
|
165
|
+
}
|
|
166
|
+
const fn = new Function('page', 'expect', `"use strict"; return (async () => {\n${body}\n})();`);
|
|
167
|
+
await fn(page, test_1.expect);
|
|
168
|
+
}
|
|
169
|
+
extractTestBody(specSource) {
|
|
170
|
+
const lines = specSource.split(/\r?\n/);
|
|
171
|
+
const bodyLines = [];
|
|
172
|
+
let inTest = false;
|
|
173
|
+
let startedBody = false;
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
if (!inTest) {
|
|
176
|
+
if (/^\s*test\(/.test(line)) {
|
|
177
|
+
inTest = true;
|
|
178
|
+
if (line.includes('{')) {
|
|
179
|
+
startedBody = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!startedBody) {
|
|
185
|
+
if (line.includes('{')) {
|
|
186
|
+
startedBody = true;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (/^\s*\}\);\s*$/.test(line)) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
bodyLines.push(line);
|
|
194
|
+
}
|
|
195
|
+
return bodyLines.join('\n').trim();
|
|
196
|
+
}
|
|
197
|
+
async launchBrowser() {
|
|
198
|
+
return await this.browserAdapter.launchBrowser(this.config);
|
|
199
|
+
}
|
|
200
|
+
async createContext(browser) {
|
|
201
|
+
const context = await this.browserAdapter.createContext(browser, this.config);
|
|
202
|
+
// Attach page handler for newly-created pages (popups)
|
|
203
|
+
context.on('page', async (page) => {
|
|
204
|
+
await this.setupPage(page);
|
|
205
|
+
});
|
|
206
|
+
// Attach setup to any existing pages
|
|
207
|
+
const pages = context.pages();
|
|
208
|
+
for (const p of pages) {
|
|
209
|
+
await this.setupPage(p);
|
|
210
|
+
}
|
|
211
|
+
return context;
|
|
212
|
+
}
|
|
213
|
+
async setupRecorderMode(page) {
|
|
214
|
+
logger_1.logger.info('Setting up recorder mode');
|
|
215
|
+
// Inject script to capture user interactions
|
|
216
|
+
await page.addInitScript(() => {
|
|
217
|
+
// Store recorded interactions
|
|
218
|
+
window.__contextGraph = {
|
|
219
|
+
interactions: [],
|
|
220
|
+
startTime: Date.now()
|
|
221
|
+
};
|
|
222
|
+
// Helper to record interactions
|
|
223
|
+
const recordInteraction = (type, element, data) => {
|
|
224
|
+
const interaction = {
|
|
225
|
+
type,
|
|
226
|
+
timestamp: Date.now(),
|
|
227
|
+
element: {
|
|
228
|
+
tagName: element.tagName.toLowerCase(),
|
|
229
|
+
id: element.id,
|
|
230
|
+
className: element.className,
|
|
231
|
+
text: element.textContent?.slice(0, 50) || '',
|
|
232
|
+
xpath: getXPath(element),
|
|
233
|
+
cssSelector: getCssSelector(element)
|
|
234
|
+
},
|
|
235
|
+
data: data || {}
|
|
236
|
+
};
|
|
237
|
+
window.__contextGraph.interactions.push(interaction);
|
|
238
|
+
console.log('ContextGraph: Recorded', type, interaction);
|
|
239
|
+
};
|
|
240
|
+
// Helper functions
|
|
241
|
+
const getXPath = (element) => {
|
|
242
|
+
if (element.id)
|
|
243
|
+
return `//*[@id="${element.id}"]`;
|
|
244
|
+
if (element === document.body)
|
|
245
|
+
return '/html/body';
|
|
246
|
+
let ix = 0;
|
|
247
|
+
const siblings = element.parentNode.childNodes;
|
|
248
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
249
|
+
const sibling = siblings[i];
|
|
250
|
+
if (sibling === element) {
|
|
251
|
+
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
|
|
252
|
+
}
|
|
253
|
+
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
|
|
254
|
+
ix++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return '';
|
|
258
|
+
};
|
|
259
|
+
const getCssSelector = (element) => {
|
|
260
|
+
if (element.id)
|
|
261
|
+
return `#${element.id}`;
|
|
262
|
+
if (element.className) {
|
|
263
|
+
const classes = element.className.split(' ').filter((c) => c);
|
|
264
|
+
return `${element.tagName.toLowerCase()}.${classes.join('.')}`;
|
|
265
|
+
}
|
|
266
|
+
return element.tagName.toLowerCase();
|
|
267
|
+
};
|
|
268
|
+
// Record clicks
|
|
269
|
+
document.addEventListener('click', (e) => {
|
|
270
|
+
recordInteraction('click', e.target, {
|
|
271
|
+
button: e.button,
|
|
272
|
+
ctrlKey: e.ctrlKey,
|
|
273
|
+
shiftKey: e.shiftKey,
|
|
274
|
+
altKey: e.altKey
|
|
275
|
+
});
|
|
276
|
+
}, true);
|
|
277
|
+
// Record form inputs
|
|
278
|
+
document.addEventListener('input', (e) => {
|
|
279
|
+
const target = e.target;
|
|
280
|
+
recordInteraction('input', target, {
|
|
281
|
+
value: target.value.slice(0, 100), // Limit value length
|
|
282
|
+
inputType: target.type,
|
|
283
|
+
name: target.name
|
|
284
|
+
});
|
|
285
|
+
}, true);
|
|
286
|
+
// Record focus changes
|
|
287
|
+
document.addEventListener('focus', (e) => {
|
|
288
|
+
recordInteraction('focus', e.target);
|
|
289
|
+
}, true);
|
|
290
|
+
// Record scroll events
|
|
291
|
+
let scrollTimeout;
|
|
292
|
+
window.addEventListener('scroll', () => {
|
|
293
|
+
clearTimeout(scrollTimeout);
|
|
294
|
+
scrollTimeout = setTimeout(() => {
|
|
295
|
+
recordInteraction('scroll', document.body, {
|
|
296
|
+
scrollX: window.scrollX,
|
|
297
|
+
scrollY: window.scrollY
|
|
298
|
+
});
|
|
299
|
+
}, 100);
|
|
300
|
+
});
|
|
301
|
+
// Record navigation
|
|
302
|
+
const originalPushState = history.pushState;
|
|
303
|
+
history.pushState = function (data, unused, url) {
|
|
304
|
+
recordInteraction('navigation', document.body, {
|
|
305
|
+
url: url?.toString(),
|
|
306
|
+
method: 'pushState'
|
|
307
|
+
});
|
|
308
|
+
return originalPushState.apply(this, [data, unused, url]);
|
|
309
|
+
};
|
|
310
|
+
const originalReplaceState = history.replaceState;
|
|
311
|
+
history.replaceState = function (data, unused, url) {
|
|
312
|
+
recordInteraction('navigation', document.body, {
|
|
313
|
+
url: url?.toString(),
|
|
314
|
+
method: 'replaceState'
|
|
315
|
+
});
|
|
316
|
+
return originalReplaceState.apply(this, [data, unused, url]);
|
|
317
|
+
};
|
|
318
|
+
window.addEventListener('popstate', () => {
|
|
319
|
+
recordInteraction('navigation', document.body, {
|
|
320
|
+
url: location.href,
|
|
321
|
+
method: 'popstate'
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
// Set up periodic capture of interactions
|
|
326
|
+
const captureInteractions = async () => {
|
|
327
|
+
try {
|
|
328
|
+
const interactions = await page.evaluate(() => window.__contextGraph?.interactions || []);
|
|
329
|
+
if (interactions.length > 0) {
|
|
330
|
+
logger_1.logger.info(`Captured ${interactions.length} user interactions`);
|
|
331
|
+
// Save interactions to a separate file
|
|
332
|
+
await this.storageEngine.saveUserInteractions(page.url(), interactions);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
logger_1.logger.debug(`Error capturing interactions: ${error.message}`);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
// Capture interactions every 5 seconds
|
|
340
|
+
const interval = setInterval(captureInteractions, 5000);
|
|
341
|
+
// Clean up on page close
|
|
342
|
+
page.on('close', () => {
|
|
343
|
+
clearInterval(interval);
|
|
344
|
+
captureInteractions(); // Final capture
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async setupPage(page) {
|
|
348
|
+
try {
|
|
349
|
+
// Ensure the page is ready before proceeding
|
|
350
|
+
await page.waitForLoadState('domcontentloaded');
|
|
351
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
352
|
+
logger_1.logger.debug('Network idle state not reached, continuing...');
|
|
353
|
+
});
|
|
354
|
+
// Attach listeners
|
|
355
|
+
await this.browserAdapter.attachPageListeners(page);
|
|
356
|
+
await this.captureEngine.attachNetworkListeners(page);
|
|
357
|
+
// Set up recorder mode if enabled
|
|
358
|
+
if (this.mode === RuntimeMode.RECORDER) {
|
|
359
|
+
await this.setupRecorderMode(page);
|
|
360
|
+
}
|
|
361
|
+
// Prevent accidental closure using addInitScript instead of evaluateOnNewDocument
|
|
362
|
+
await page.addInitScript(() => {
|
|
363
|
+
window.addEventListener('beforeunload', (e) => {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
// @ts-ignore - TypeScript doesn't recognize returnValue on BeforeUnloadEvent
|
|
366
|
+
e.returnValue = '';
|
|
367
|
+
return '';
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
logger_1.logger.error(`Error in setupPage: ${error instanceof Error ? error.message : String(error)}`);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
// Set up page change detection
|
|
376
|
+
// Capture traditional load events
|
|
377
|
+
page.on('load', async () => {
|
|
378
|
+
const url = page.url();
|
|
379
|
+
logger_1.logger.info(`Page load detected: ${url}`);
|
|
380
|
+
const key = this.normalizeUrl(url);
|
|
381
|
+
if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
|
|
382
|
+
logger_1.logger.info(`New page detected (load event): ${url}`);
|
|
383
|
+
this.capturedPages.add(key);
|
|
384
|
+
await this.captureCurrentPage(page);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
logger_1.logger.debug(`Page already captured (load event): ${url}`);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
// Also capture DOMContentLoaded for faster detection
|
|
391
|
+
page.on('domcontentloaded', async () => {
|
|
392
|
+
const url = page.url();
|
|
393
|
+
const key = this.normalizeUrl(url);
|
|
394
|
+
if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
|
|
395
|
+
logger_1.logger.info(`New page detected (DOMContentLoaded): ${url}`);
|
|
396
|
+
this.capturedPages.add(key);
|
|
397
|
+
await this.captureCurrentPage(page);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
// Capture frame navigations (including main frame)
|
|
401
|
+
page.on('framenavigated', async (frame) => {
|
|
402
|
+
// Only capture main frame navigations to avoid duplicates
|
|
403
|
+
if (frame === page.mainFrame()) {
|
|
404
|
+
const url = frame.url();
|
|
405
|
+
logger_1.logger.info(`Frame navigated (main frame): ${url}`);
|
|
406
|
+
const key = this.normalizeUrl(url);
|
|
407
|
+
if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
|
|
408
|
+
logger_1.logger.info(`New page detected (framenavigated): ${url}`);
|
|
409
|
+
this.capturedPages.add(key);
|
|
410
|
+
await this.captureCurrentPage(page);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
logger_1.logger.debug(`Page already captured (framenavigated): ${url}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
// Capture SPA navigation via history API
|
|
418
|
+
try {
|
|
419
|
+
await page.exposeBinding('cc_onHistoryChange', async (_source, url) => {
|
|
420
|
+
logger_1.logger.info(`History change detected: ${url}`);
|
|
421
|
+
const key = this.normalizeUrl(url);
|
|
422
|
+
if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
|
|
423
|
+
logger_1.logger.info(`New page detected (history API): ${url}`);
|
|
424
|
+
this.capturedPages.add(key);
|
|
425
|
+
// Add a small delay to ensure page is ready
|
|
426
|
+
await new Promise(r => setTimeout(r, 300));
|
|
427
|
+
await this.captureCurrentPage(page);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
logger_1.logger.debug(`Page already captured (history API): ${url}`);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
await page.addInitScript(() => {
|
|
434
|
+
(function () {
|
|
435
|
+
const origPush = history.pushState;
|
|
436
|
+
history.pushState = function () {
|
|
437
|
+
const ret = origPush.apply(this, arguments);
|
|
438
|
+
try {
|
|
439
|
+
setTimeout(() => {
|
|
440
|
+
window['cc_onHistoryChange'] && window['cc_onHistoryChange'](location.href);
|
|
441
|
+
}, 100);
|
|
442
|
+
}
|
|
443
|
+
catch (e) { }
|
|
444
|
+
return ret;
|
|
445
|
+
};
|
|
446
|
+
const origReplace = history.replaceState;
|
|
447
|
+
history.replaceState = function () {
|
|
448
|
+
const ret = origReplace.apply(this, arguments);
|
|
449
|
+
try {
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
window['cc_onHistoryChange'] && window['cc_onHistoryChange'](location.href);
|
|
452
|
+
}, 100);
|
|
453
|
+
}
|
|
454
|
+
catch (e) { }
|
|
455
|
+
return ret;
|
|
456
|
+
};
|
|
457
|
+
window.addEventListener('popstate', function () {
|
|
458
|
+
try {
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
window['cc_onHistoryChange'] && window['cc_onHistoryChange'](location.href);
|
|
461
|
+
}, 100);
|
|
462
|
+
}
|
|
463
|
+
catch (e) { }
|
|
464
|
+
});
|
|
465
|
+
})();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
logger_1.logger.warn(`Failed to set up history API interception: ${error.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
normalizeUrl(url) {
|
|
473
|
+
try {
|
|
474
|
+
const u = new URL(url);
|
|
475
|
+
// Sort query params for consistent comparison
|
|
476
|
+
const sortedParams = Array.from(u.searchParams.entries())
|
|
477
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
478
|
+
const queryString = sortedParams.length > 0
|
|
479
|
+
? '?' + sortedParams.map(([k, v]) => `${k}=${v}`).join('&')
|
|
480
|
+
: '';
|
|
481
|
+
// Fix: Include hash so SPAs (e.g., /#/dashboard) are treated as unique pages
|
|
482
|
+
return `${u.origin}${u.pathname}${queryString}${u.hash}`;
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return url;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async captureCurrentPage(page) {
|
|
489
|
+
const captureWork = (async () => {
|
|
490
|
+
const maxAttempts = 3;
|
|
491
|
+
let attempt = 0;
|
|
492
|
+
const startingUrl = page.url();
|
|
493
|
+
while (attempt < maxAttempts) {
|
|
494
|
+
attempt++;
|
|
495
|
+
try {
|
|
496
|
+
if (page.isClosed()) {
|
|
497
|
+
logger_1.logger.warn('Skipping capture: page is already closed');
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
// If the page has navigated since this capture was scheduled, abort to avoid racing navigation.
|
|
501
|
+
const liveUrl = page.url();
|
|
502
|
+
if (this.normalizeUrl(liveUrl) !== this.normalizeUrl(startingUrl)) {
|
|
503
|
+
logger_1.logger.info(`Skipping capture: page navigated during capture scheduling (${startingUrl} -> ${liveUrl})`);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
// Wait for page to be stable
|
|
507
|
+
try {
|
|
508
|
+
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {
|
|
509
|
+
logger_1.logger.debug('Network idle timeout, proceeding with capture');
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
514
|
+
}
|
|
515
|
+
if (page.isClosed()) {
|
|
516
|
+
logger_1.logger.warn('Skipping capture: page closed while waiting for stability');
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
const currentUrl = page.url();
|
|
520
|
+
logger_1.logger.info(`Capturing page (attempt ${attempt}/${maxAttempts}): ${currentUrl}`);
|
|
521
|
+
const consoleMessages = this.browserAdapter.getConsoleMessages();
|
|
522
|
+
const snapshot = await this.captureEngine.capturePageSnapshot(page, this.config, consoleMessages);
|
|
523
|
+
await this.storageEngine.savePageSnapshot(snapshot);
|
|
524
|
+
// Update components registry
|
|
525
|
+
if (this.componentsRegistry && this.config.capture.components?.enabled) {
|
|
526
|
+
// Update registry domain if unknown
|
|
527
|
+
if (this.componentsRegistry.registry?.domain === 'unknown') {
|
|
528
|
+
this.componentsRegistry.registry.domain = snapshot.metadata.domain;
|
|
529
|
+
}
|
|
530
|
+
await this.componentsRegistry.processPage(snapshot);
|
|
531
|
+
}
|
|
532
|
+
this.browserAdapter.clearConsoleMessages();
|
|
533
|
+
await this.storageEngine.updateGlobalManifest({
|
|
534
|
+
captureId: snapshot.metadata.captureId,
|
|
535
|
+
url: snapshot.metadata.url,
|
|
536
|
+
title: snapshot.metadata.title,
|
|
537
|
+
timestamp: snapshot.metadata.timestamp,
|
|
538
|
+
sessionId: this.sessionId,
|
|
539
|
+
domain: snapshot.metadata.domain,
|
|
540
|
+
mode: this.mode,
|
|
541
|
+
});
|
|
542
|
+
logger_1.logger.info(`✓ Successfully captured page: ${snapshot.metadata.url} (saved as: ${snapshot.metadata.pageName})`);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
const msg = error.message || '';
|
|
547
|
+
logger_1.logger.warn(`Capture attempt ${attempt} failed: ${msg}`);
|
|
548
|
+
// Check for fatal browser closure errors - don't retry these
|
|
549
|
+
if (/Target page, context or browser has been closed|Execution context was destroyed|Navigation cancelled/i.test(msg)) {
|
|
550
|
+
logger_1.logger.warn(`Capture aborted due to navigation/page close: ${msg}`);
|
|
551
|
+
break; // don't retry these
|
|
552
|
+
}
|
|
553
|
+
// For other errors, retry if we haven't exhausted attempts
|
|
554
|
+
if (attempt < maxAttempts) {
|
|
555
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
logger_1.logger.error(`Failed to capture page: ${error}`);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
})();
|
|
563
|
+
this.pendingCaptures.add(captureWork);
|
|
564
|
+
try {
|
|
565
|
+
await captureWork;
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
this.pendingCaptures.delete(captureWork);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
isValidUrl(url) {
|
|
572
|
+
try {
|
|
573
|
+
const parsed = new URL(url);
|
|
574
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async shutdown() {
|
|
581
|
+
try {
|
|
582
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), 20000));
|
|
583
|
+
await Promise.race([
|
|
584
|
+
Promise.all(Array.from(this.pendingCaptures)),
|
|
585
|
+
timeout
|
|
586
|
+
]);
|
|
587
|
+
logger_1.logger.info('All pending captures completed');
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
const error = err;
|
|
591
|
+
if (error.message === 'Shutdown timeout') {
|
|
592
|
+
logger_1.logger.warn('Shutdown timeout - some captures may still be in progress');
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
logger_1.logger.warn('Error waiting for pending captures: ' + error.message);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Persist components registry and update manifest reference
|
|
599
|
+
try {
|
|
600
|
+
if (this.componentsRegistry && this.config.capture.components?.enabled) {
|
|
601
|
+
const domain = this.componentsRegistry.getRegistry().domain;
|
|
602
|
+
const domainName = (() => {
|
|
603
|
+
const parts = domain.split('.');
|
|
604
|
+
const filtered = parts.filter(p => p.toLowerCase() !== 'www');
|
|
605
|
+
if (filtered.length >= 2)
|
|
606
|
+
return filtered[filtered.length - 2];
|
|
607
|
+
return filtered[0] || parts[0];
|
|
608
|
+
})();
|
|
609
|
+
const registry = this.componentsRegistry.getRegistry();
|
|
610
|
+
await this.storageEngine.saveComponentsRegistry(registry, domainName);
|
|
611
|
+
await this.storageEngine.updateManifestWithComponents(domainName, registry.components.length);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
logger_1.logger.warn(`Failed to persist components registry: ${error.message}`);
|
|
616
|
+
}
|
|
617
|
+
await this.browserAdapter.close();
|
|
618
|
+
logger_1.logger.info('Runtime shutdown complete');
|
|
619
|
+
}
|
|
620
|
+
onBrowserDisconnect(callback) {
|
|
621
|
+
this.browserAdapter.onBrowserDisconnect(callback);
|
|
622
|
+
}
|
|
623
|
+
getSessionId() {
|
|
624
|
+
return this.sessionId;
|
|
625
|
+
}
|
|
626
|
+
async capturePageIfNeeded(page) {
|
|
627
|
+
try {
|
|
628
|
+
const url = page.url();
|
|
629
|
+
const key = this.normalizeUrl(url);
|
|
630
|
+
if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
|
|
631
|
+
logger_1.logger.info(`New page detected: ${url}`);
|
|
632
|
+
// Ensure page is stable before capturing
|
|
633
|
+
await page.waitForLoadState('domcontentloaded');
|
|
634
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
635
|
+
logger_1.logger.debug('Network idle state not reached, continuing with capture...');
|
|
636
|
+
});
|
|
637
|
+
await this.captureCurrentPage(page);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
642
|
+
logger_1.logger.error(`Error in capturePageIfNeeded: ${errorMessage}`);
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
exports.RuntimeController = RuntimeController;
|
|
648
|
+
//# sourceMappingURL=runtime.js.map
|