pw-automation-framework 2.0.1

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.
Files changed (111) hide show
  1. package/README.md +93 -0
  2. package/bin/lexxit-automation-framework.js +427 -0
  3. package/dist/app.d.ts +2 -0
  4. package/dist/app.js +26 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/controllers/controller.d.ts +57 -0
  7. package/dist/controllers/controller.js +263 -0
  8. package/dist/controllers/controller.js.map +1 -0
  9. package/dist/core/BrowserManager.d.ts +46 -0
  10. package/dist/core/BrowserManager.js +377 -0
  11. package/dist/core/BrowserManager.js.map +1 -0
  12. package/dist/core/PlaywrightEngine.d.ts +16 -0
  13. package/dist/core/PlaywrightEngine.js +246 -0
  14. package/dist/core/PlaywrightEngine.js.map +1 -0
  15. package/dist/core/ScreenshotManager.d.ts +10 -0
  16. package/dist/core/ScreenshotManager.js +28 -0
  17. package/dist/core/ScreenshotManager.js.map +1 -0
  18. package/dist/core/TestData.d.ts +12 -0
  19. package/dist/core/TestData.js +29 -0
  20. package/dist/core/TestData.js.map +1 -0
  21. package/dist/core/TestExecutor.d.ts +16 -0
  22. package/dist/core/TestExecutor.js +355 -0
  23. package/dist/core/TestExecutor.js.map +1 -0
  24. package/dist/core/handlers/AllHandlers.d.ts +116 -0
  25. package/dist/core/handlers/AllHandlers.js +648 -0
  26. package/dist/core/handlers/AllHandlers.js.map +1 -0
  27. package/dist/core/handlers/BaseHandler.d.ts +16 -0
  28. package/dist/core/handlers/BaseHandler.js +27 -0
  29. package/dist/core/handlers/BaseHandler.js.map +1 -0
  30. package/dist/core/handlers/ClickHandler.d.ts +34 -0
  31. package/dist/core/handlers/ClickHandler.js +359 -0
  32. package/dist/core/handlers/ClickHandler.js.map +1 -0
  33. package/dist/core/handlers/CustomCodeHandler.d.ts +35 -0
  34. package/dist/core/handlers/CustomCodeHandler.js +102 -0
  35. package/dist/core/handlers/CustomCodeHandler.js.map +1 -0
  36. package/dist/core/handlers/DropdownHandler.d.ts +43 -0
  37. package/dist/core/handlers/DropdownHandler.js +304 -0
  38. package/dist/core/handlers/DropdownHandler.js.map +1 -0
  39. package/dist/core/handlers/InputHandler.d.ts +24 -0
  40. package/dist/core/handlers/InputHandler.js +197 -0
  41. package/dist/core/handlers/InputHandler.js.map +1 -0
  42. package/dist/core/registry/ActionRegistry.d.ts +8 -0
  43. package/dist/core/registry/ActionRegistry.js +35 -0
  44. package/dist/core/registry/ActionRegistry.js.map +1 -0
  45. package/dist/installer/frameworkLauncher.d.ts +31 -0
  46. package/dist/installer/frameworkLauncher.js +198 -0
  47. package/dist/installer/frameworkLauncher.js.map +1 -0
  48. package/dist/queue/ExecutionQueue.d.ts +52 -0
  49. package/dist/queue/ExecutionQueue.js +175 -0
  50. package/dist/queue/ExecutionQueue.js.map +1 -0
  51. package/dist/routes/api.routes.d.ts +2 -0
  52. package/dist/routes/api.routes.js +16 -0
  53. package/dist/routes/api.routes.js.map +1 -0
  54. package/dist/server.d.ts +1 -0
  55. package/dist/server.js +30 -0
  56. package/dist/server.js.map +1 -0
  57. package/dist/types/types.d.ts +135 -0
  58. package/dist/types/types.js +4 -0
  59. package/dist/types/types.js.map +1 -0
  60. package/dist/utils/elementHighlight.d.ts +35 -0
  61. package/dist/utils/elementHighlight.js +136 -0
  62. package/dist/utils/elementHighlight.js.map +1 -0
  63. package/dist/utils/locatorHelper.d.ts +7 -0
  64. package/dist/utils/locatorHelper.js +53 -0
  65. package/dist/utils/locatorHelper.js.map +1 -0
  66. package/dist/utils/logger.d.ts +12 -0
  67. package/dist/utils/logger.js +35 -0
  68. package/dist/utils/logger.js.map +1 -0
  69. package/dist/utils/response.d.ts +4 -0
  70. package/dist/utils/response.js +25 -0
  71. package/dist/utils/response.js.map +1 -0
  72. package/dist/utils/responseFormatter.d.ts +78 -0
  73. package/dist/utils/responseFormatter.js +123 -0
  74. package/dist/utils/responseFormatter.js.map +1 -0
  75. package/dist/utils/sseManager.d.ts +32 -0
  76. package/dist/utils/sseManager.js +122 -0
  77. package/dist/utils/sseManager.js.map +1 -0
  78. package/lexxit-automation-framework-2.0.0.tgz +0 -0
  79. package/npmignore +5 -0
  80. package/package.json +36 -0
  81. package/scripts/postinstall.js +52 -0
  82. package/src/app.ts +27 -0
  83. package/src/controllers/controller.ts +282 -0
  84. package/src/core/BrowserManager.ts +398 -0
  85. package/src/core/PlaywrightEngine.ts +371 -0
  86. package/src/core/ScreenshotManager.ts +25 -0
  87. package/src/core/TestData.ts +25 -0
  88. package/src/core/TestExecutor.ts +436 -0
  89. package/src/core/handlers/AllHandlers.ts +626 -0
  90. package/src/core/handlers/BaseHandler.ts +41 -0
  91. package/src/core/handlers/ClickHandler.ts +482 -0
  92. package/src/core/handlers/CustomCodeHandler.ts +123 -0
  93. package/src/core/handlers/DropdownHandler.ts +438 -0
  94. package/src/core/handlers/InputHandler.ts +192 -0
  95. package/src/core/registry/ActionRegistry.ts +31 -0
  96. package/src/installer/frameworkLauncher.ts +242 -0
  97. package/src/installer/install.sh +107 -0
  98. package/src/public/dashboard.html +540 -0
  99. package/src/public/queue-monitor.html +190 -0
  100. package/src/queue/ExecutionQueue.ts +200 -0
  101. package/src/routes/api.routes.ts +16 -0
  102. package/src/server.ts +29 -0
  103. package/src/types/types.ts +169 -0
  104. package/src/utils/elementHighlight.ts +174 -0
  105. package/src/utils/locatorHelper.ts +49 -0
  106. package/src/utils/logger.ts +40 -0
  107. package/src/utils/response.ts +27 -0
  108. package/src/utils/responseFormatter.ts +167 -0
  109. package/src/utils/sseManager.ts +127 -0
  110. package/tsconfig.json +18 -0
  111. package/videos/fb1b94b6-6639-4c9a-82bb-63572606f403/page@5bd5c6c8b62baa700e9810cdd64f5c49.webm +0 -0
@@ -0,0 +1,190 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>PW — Queue Monitor</title>
6
+ <style>
7
+ *{box-sizing:border-box;margin:0;padding:0}
8
+ :root{--bg:#0a0d14;--s:#111827;--s2:#1a2236;--b:#1e2d45;--t:#e2e8f0;--m:#64748b;
9
+ --pass:#22c55e;--fail:#ef4444;--skip:#f59e0b;--run:#3b82f6;--q:#a855f7;--cancel:#a855f7;--r:10px}
10
+ body{background:var(--bg);color:var(--t);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh}
11
+ header{background:var(--s);border-bottom:1px solid var(--b);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;gap:12px}
12
+ h1{font-size:.95rem;font-weight:700;display:flex;align-items:center;gap:8px}
13
+ .hbar{display:flex;align-items:center;gap:12px}
14
+ .sb-item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--m)}
15
+ .sb-dot{width:7px;height:7px;border-radius:50%}
16
+ .btn{padding:5px 14px;border-radius:99px;font-size:.72rem;font-weight:600;cursor:pointer;border:1px solid;transition:all .2s}
17
+ .btn-danger{background:#2d0a0a;color:var(--fail);border-color:#7f1d1d}
18
+ .btn-danger:hover{background:#3d0a0a}
19
+ .btn-danger:disabled{opacity:.4;cursor:not-allowed}
20
+ .btn-refresh{background:var(--s2);color:var(--m);border-color:var(--b)}
21
+ .btn-refresh:hover{color:var(--t);border-color:#2d3f5e}
22
+
23
+ main{padding:20px;max-width:1100px;margin:0 auto}
24
+ .sec-title{font-size:.65rem;font-weight:700;color:var(--m);text-transform:uppercase;letter-spacing:.1em;margin-bottom:10px}
25
+ .jobs{display:flex;flex-direction:column;gap:7px}
26
+
27
+ .job{background:var(--s2);border:1px solid var(--b);border-left:3px solid var(--b);border-radius:var(--r);padding:12px 16px;display:grid;grid-template-columns:auto 1fr auto;gap:14px;align-items:center}
28
+ .job.queued {border-left-color:var(--q)}
29
+ .job.running {border-left-color:var(--run)}
30
+ .job.done {border-left-color:var(--pass)}
31
+ .job.failed {border-left-color:var(--fail)}
32
+ .job.cancelled{border-left-color:var(--cancel)}
33
+
34
+ .jbadge{padding:3px 10px;border-radius:99px;font-size:.65rem;font-weight:700;flex-shrink:0}
35
+ .jb-queued {background:#1a0a2e;color:var(--q);border:1px solid #4a1d7e}
36
+ .jb-running {background:#0c1f3d;color:var(--run);border:1px solid #1e3a6e}
37
+ .jb-done {background:#052e16;color:var(--pass);border:1px solid #14532d}
38
+ .jb-failed {background:#2d0a0a;color:var(--fail);border:1px solid #7f1d1d}
39
+ .jb-cancelled{background:#1a0a2e;color:var(--cancel);border:1px solid #4a1d7e}
40
+
41
+ .jid{font-family:monospace;font-size:.7rem;color:var(--m)}
42
+ .jmeta{font-size:.72rem;color:#94a3b8;margin-top:2px}
43
+ .jprog-wrap{height:3px;background:var(--b);border-radius:99px;overflow:hidden;margin-top:6px;width:180px}
44
+ .jprog-fill{height:100%;background:var(--run);border-radius:99px;transition:width .4s}
45
+ .jprog-fill.done{background:var(--pass)}
46
+ .jprog-fill.failed{background:var(--fail)}
47
+ .jprog-fill.cancelled{background:var(--cancel)}
48
+ .jactions{display:flex;align-items:center;gap:8px}
49
+ .btn-sm{padding:3px 10px;border-radius:6px;font-size:.67rem;font-weight:600;cursor:pointer;border:1px solid;transition:all .15s}
50
+ .btn-view{background:var(--s2);color:var(--run);border-color:var(--b);text-decoration:none}
51
+ .btn-view:hover{border-color:var(--run)}
52
+ .btn-stop{background:#2d0a0a;color:var(--fail);border-color:#7f1d1d}
53
+ .btn-stop:hover{background:#3d0a0a}
54
+ .btn-stop:disabled{opacity:.4;cursor:not-allowed}
55
+
56
+ .empty{text-align:center;padding:40px;color:var(--m);font-size:.85rem}
57
+ .anim{animation:pulse .9s ease-in-out infinite}
58
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <header>
63
+ <h1>🎭 Execution Queue Monitor</h1>
64
+ <div class="hbar">
65
+ <div class="sb-item"><span class="sb-dot anim" style="background:var(--run)"></span><span id="sqRunning">0 running</span></div>
66
+ <div class="sb-item"><span class="sb-dot" style="background:var(--q)"></span><span id="sqQueued">0 queued</span></div>
67
+ <div class="sb-item"><span class="sb-dot" style="background:var(--pass)"></span><span id="sqDone">0 passed</span></div>
68
+ <div class="sb-item"><span class="sb-dot" style="background:var(--fail)"></span><span id="sqFailed">0 failed</span></div>
69
+ <div class="sb-item"><span class="sb-dot" style="background:var(--cancel)"></span><span id="sqCancelled">0 cancelled</span></div>
70
+ <button class="btn btn-refresh" onclick="refresh()">↻ Refresh</button>
71
+ <button class="btn btn-danger" id="cancelAllBtn" onclick="cancelAll()">⏹ Cancel All</button>
72
+ </div>
73
+ </header>
74
+
75
+ <main>
76
+ <div class="sec-title">All Executions (newest first)</div>
77
+ <div class="jobs" id="jobsGrid"><div class="empty">Loading…</div></div>
78
+ </main>
79
+
80
+ <script>
81
+ const $ = id => document.getElementById(id);
82
+
83
+ function fmt(ms) {
84
+ if (!ms && ms !== 0) return '—';
85
+ if (ms < 1000) return ms + 'ms';
86
+ return (ms / 1000).toFixed(1) + 's';
87
+ }
88
+
89
+ function timeSince(iso) {
90
+ if (!iso) return '';
91
+ const diff = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
92
+ if (diff < 60) return diff + 's ago';
93
+ if (diff < 3600) return Math.round(diff / 60) + 'm ago';
94
+ return Math.round(diff / 3600) + 'h ago';
95
+ }
96
+
97
+ function pct(p) {
98
+ const t = p.total_steps;
99
+ return t > 0 ? Math.round((p.completed_steps / t) * 100) : 0;
100
+ }
101
+
102
+ async function cancelJob(id, btn) {
103
+ if (!confirm('Cancel this execution?')) return;
104
+ btn.disabled = true;
105
+ try {
106
+ const r = await fetch(`/api/cancel/${id}`, { method: 'DELETE' });
107
+ const d = await r.json();
108
+ alert(d.message);
109
+ refresh();
110
+ } catch(e) {
111
+ alert('Failed: ' + e.message);
112
+ btn.disabled = false;
113
+ }
114
+ }
115
+
116
+ async function cancelAll() {
117
+ if (!confirm('Cancel ALL running and queued executions?')) return;
118
+ $('cancelAllBtn').disabled = true;
119
+ try {
120
+ const r = await fetch('/api/cancel-all', { method: 'DELETE' });
121
+ const d = await r.json();
122
+ alert(`Cancelled ${d.cancelled} execution(s)`);
123
+ refresh();
124
+ } catch(e) {
125
+ alert('Failed: ' + e.message);
126
+ } finally {
127
+ $('cancelAllBtn').disabled = false;
128
+ }
129
+ }
130
+
131
+ function renderJobs(jobs) {
132
+ const grid = $('jobsGrid');
133
+ if (!jobs?.length) { grid.innerHTML = '<div class="empty">No executions yet.</div>'; return; }
134
+
135
+ grid.innerHTML = jobs.map(j => {
136
+ const p = pct(j.progress);
137
+ const since = timeSince(j.queuedAt);
138
+ const dur = j.completedAt && j.startedAt
139
+ ? fmt(new Date(j.completedAt) - new Date(j.startedAt))
140
+ : j.startedAt ? fmt(Date.now() - new Date(j.startedAt)) : '—';
141
+ const canCancel = j.status === 'queued' || j.status === 'running';
142
+
143
+ return `
144
+ <div class="job ${j.status}">
145
+ <span class="jbadge jb-${j.status}">${j.status.toUpperCase()}</span>
146
+ <div>
147
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:4px">
148
+ <span class="jid">${j.executionId.slice(0,8)}…</span>
149
+ <span class="jmeta">${j.progress.total_scripts} script(s) · ${j.progress.total_steps} steps · ${since} · ${dur}</span>
150
+ </div>
151
+ <div style="display:flex;align-items:center;gap:8px">
152
+ <div class="jprog-wrap">
153
+ <div class="jprog-fill ${j.status}" style="width:${p}%"></div>
154
+ </div>
155
+ <span style="font-size:.63rem;color:var(--m)">${p}% · ✅${j.progress.passed_steps} ❌${j.progress.failed_steps} ⏭${j.progress.skipped_steps}</span>
156
+ </div>
157
+ </div>
158
+ <div class="jactions">
159
+ <a class="btn-sm btn-view" href="/dashboard/${j.executionId}" target="_blank">View →</a>
160
+ <button class="btn-sm btn-stop" ${canCancel ? '' : 'disabled'} onclick="cancelJob('${j.executionId}', this)">⏹ Stop</button>
161
+ </div>
162
+ </div>`;
163
+ }).join('');
164
+ }
165
+
166
+ function updateStats(data) {
167
+ $('sqQueued').textContent = data.queued + ' queued';
168
+ $('sqRunning').textContent = data.running + ' running';
169
+ $('sqDone').textContent = data.done + ' passed';
170
+ $('sqFailed').textContent = data.failed + ' failed';
171
+ $('sqCancelled').textContent= (data.cancelled||0) + ' cancelled';
172
+
173
+ const hasActive = data.queued > 0 || data.running > 0;
174
+ $('cancelAllBtn').disabled = !hasActive;
175
+ }
176
+
177
+ async function refresh() {
178
+ try {
179
+ const r = await fetch('/api/queue');
180
+ const data = await r.json();
181
+ updateStats(data);
182
+ renderJobs(data.jobs);
183
+ } catch { /* offline */ }
184
+ }
185
+
186
+ refresh();
187
+ setInterval(refresh, 2000);
188
+ </script>
189
+ </body>
190
+ </html>
@@ -0,0 +1,200 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { QueueJob, RequestPayload, ExecutionResult, QueueStatus } from "../types/types";
3
+ import { TestExecutor } from "../core/TestExecutor";
4
+ import { sseManager } from "../utils/sseManager";
5
+ import { Logger } from "../utils/logger";
6
+
7
+ const log = Logger.create("ExecutionQueue");
8
+
9
+ export class ExecutionQueue {
10
+ private jobs: Map<string, QueueJob> = new Map();
11
+ private running: Set<string> = new Set();
12
+ private maxConcurrent: number;
13
+
14
+ constructor(maxConcurrent: number = 3) {
15
+ this.maxConcurrent = maxConcurrent;
16
+ }
17
+
18
+ // ─── Enqueue ───────────────────────────────────────────────────────────────
19
+
20
+ enqueue(payload: RequestPayload): string {
21
+ const executionId = uuidv4();
22
+ const job: QueueJob = {
23
+ executionId,
24
+ status: "queued",
25
+ payload,
26
+ queuedAt: new Date().toISOString(),
27
+ cancelRequested: false,
28
+ progress: {
29
+ total_scripts: payload.scripts.length,
30
+ completed_scripts: 0,
31
+ total_steps: payload.scripts.reduce((s, sc) => s + sc.steps.length, 0),
32
+ completed_steps: 0,
33
+ passed_steps: 0,
34
+ failed_steps: 0,
35
+ skipped_steps: 0,
36
+ },
37
+ };
38
+
39
+ this.jobs.set(executionId, job);
40
+ log.info(`Queued: ${executionId} | scripts=${payload.scripts.length}`);
41
+ this.broadcastQueueUpdate();
42
+ this.processNext();
43
+ return executionId;
44
+ }
45
+
46
+ // ─── Cancel ────────────────────────────────────────────────────────────────
47
+
48
+ cancel(executionId: string): { success: boolean; message: string } {
49
+ const job = this.jobs.get(executionId);
50
+ if (!job) return { success: false, message: "Execution not found" };
51
+
52
+ if (job.status === "done" || job.status === "failed" || job.status === "cancelled") {
53
+ return { success: false, message: `Cannot cancel — already ${job.status}` };
54
+ }
55
+
56
+ job.cancelRequested = true;
57
+
58
+ if (job.status === "queued") {
59
+ // Not started yet — cancel immediately
60
+ job.status = "cancelled";
61
+ job.cancelledAt = new Date().toISOString();
62
+ sseManager.emit(executionId, "execution_cancelled", {
63
+ message: "Execution cancelled before it started",
64
+ executionId,
65
+ });
66
+ this.broadcastQueueUpdate();
67
+ log.info(`Cancelled (queued): ${executionId}`);
68
+ return { success: true, message: "Execution cancelled" };
69
+ }
70
+
71
+ // Running — signal the executor to stop after current step
72
+ log.info(`Cancel requested (running): ${executionId}`);
73
+ sseManager.emit(executionId, "log", {
74
+ message: "⏹ Cancellation requested — stopping after current step...",
75
+ });
76
+ return { success: true, message: "Cancellation requested — will stop after current step" };
77
+ }
78
+
79
+ // ─── Cancel All ────────────────────────────────────────────────────────────
80
+
81
+ cancelAll(): { cancelled: number } {
82
+ let count = 0;
83
+ this.jobs.forEach((job) => {
84
+ if (job.status === "queued" || job.status === "running") {
85
+ this.cancel(job.executionId);
86
+ count++;
87
+ }
88
+ });
89
+ return { cancelled: count };
90
+ }
91
+
92
+ // ─── Process ───────────────────────────────────────────────────────────────
93
+
94
+ private processNext(): void {
95
+ if (this.running.size >= this.maxConcurrent) return;
96
+
97
+ const next = Array.from(this.jobs.values()).find(j => j.status === "queued");
98
+ if (!next) return;
99
+
100
+ this.running.add(next.executionId);
101
+ next.status = "running";
102
+ next.startedAt = new Date().toISOString();
103
+ this.broadcastQueueUpdate();
104
+
105
+ this.runJob(next).catch((err) => {
106
+ log.error(`Job crashed: ${next.executionId} — ${err.message}`);
107
+ next.status = "failed";
108
+ next.error = err.message;
109
+ this.finalize(next.executionId);
110
+ });
111
+ }
112
+
113
+ private async runJob(job: QueueJob): Promise<void> {
114
+ log.info(`Starting: ${job.executionId}`);
115
+ try {
116
+ const result = await TestExecutor.run(
117
+ job.payload,
118
+ job.executionId,
119
+ // Progress callback
120
+ (update) => {
121
+ Object.assign(job.progress, update);
122
+ this.broadcastQueueUpdate();
123
+ },
124
+ // Cancel check — TestExecutor polls this
125
+ () => job.cancelRequested
126
+ );
127
+
128
+ if (job.cancelRequested) {
129
+ job.status = "cancelled";
130
+ job.cancelledAt = new Date().toISOString();
131
+ } else {
132
+ job.result = result;
133
+ job.status = result.overall_status === "PASS" ? "done" : "failed";
134
+ }
135
+ job.completedAt = new Date().toISOString();
136
+ } catch (error: any) {
137
+ job.status = "failed";
138
+ job.error = error.message;
139
+ job.completedAt = new Date().toISOString();
140
+ sseManager.emit(job.executionId, "error", { message: error.message });
141
+ }
142
+
143
+ this.finalize(job.executionId);
144
+ }
145
+
146
+ private finalize(executionId: string): void {
147
+ this.running.delete(executionId);
148
+ this.broadcastQueueUpdate();
149
+ log.info(`Finalized: ${executionId} | running=${this.running.size}/${this.maxConcurrent}`);
150
+ // Keep jobs in memory 1 hour for replay
151
+ setTimeout(() => this.jobs.delete(executionId), 60 * 60 * 1000);
152
+ this.processNext();
153
+ }
154
+
155
+ // ─── Query ─────────────────────────────────────────────────────────────────
156
+
157
+ getJob(executionId: string): QueueJob | undefined {
158
+ return this.jobs.get(executionId);
159
+ }
160
+
161
+ isCancelRequested(executionId: string): boolean {
162
+ return this.jobs.get(executionId)?.cancelRequested ?? false;
163
+ }
164
+
165
+ getAllJobs(): QueueJob[] {
166
+ return Array.from(this.jobs.values())
167
+ .sort((a, b) => new Date(b.queuedAt).getTime() - new Date(a.queuedAt).getTime());
168
+ }
169
+
170
+ getQueueStats() {
171
+ const jobs = this.getAllJobs();
172
+ return {
173
+ total: jobs.length,
174
+ queued: jobs.filter(j => j.status === "queued").length,
175
+ running: jobs.filter(j => j.status === "running").length,
176
+ done: jobs.filter(j => j.status === "done").length,
177
+ failed: jobs.filter(j => j.status === "failed").length,
178
+ cancelled: jobs.filter(j => j.status === "cancelled").length,
179
+ maxConcurrent: this.maxConcurrent,
180
+ jobs: jobs.map(j => ({
181
+ executionId: j.executionId,
182
+ status: j.status,
183
+ queuedAt: j.queuedAt,
184
+ startedAt: j.startedAt,
185
+ completedAt: j.completedAt,
186
+ cancelledAt: j.cancelledAt,
187
+ progress: j.progress,
188
+ error: j.error,
189
+ overall_status: j.result?.overall_status,
190
+ cancelRequested: j.cancelRequested,
191
+ })),
192
+ };
193
+ }
194
+
195
+ private broadcastQueueUpdate(): void {
196
+ sseManager.broadcast("queue_update", this.getQueueStats());
197
+ }
198
+ }
199
+
200
+ export const executionQueue = new ExecutionQueue(3);
@@ -0,0 +1,16 @@
1
+ import { Router } from "express";
2
+ import { ApiController } from "../controllers/controller";
3
+
4
+ const router = Router();
5
+
6
+ router.post ("/run-test", ApiController.runTest);
7
+ router.get ("/stream/:executionId", ApiController.stream);
8
+ router.get ("/result/:executionId", ApiController.getResult); // ← full formatted result
9
+ router.get ("/status/:executionId", ApiController.getStatus); // ← lightweight status
10
+ router.delete("/cancel/:executionId", ApiController.cancelExecution);
11
+ router.delete("/cancel-all", ApiController.cancelAll);
12
+ router.get ("/queue", ApiController.getQueue);
13
+ router.get ("/actions", ApiController.listActions);
14
+ router.get ("/health", ApiController.health);
15
+
16
+ export default router;
package/src/server.ts ADDED
@@ -0,0 +1,29 @@
1
+ import "dotenv/config";
2
+ import app from "./app";
3
+
4
+ const PORT = parseInt(process.env.PORT || "3000", 10);
5
+
6
+ const server = app.listen(PORT, () => {
7
+ console.log("\n" + "═".repeat(60));
8
+ console.log(" 🎭 Playwright Automation Framework v2.0");
9
+ console.log("═".repeat(60));
10
+ console.log(` API : http://localhost:${PORT}/api`);
11
+ console.log(` Queue Monitor: http://localhost:${PORT}/queue-monitor`);
12
+ console.log(` Health : http://localhost:${PORT}/api/health`);
13
+ console.log(` Actions : http://localhost:${PORT}/api/actions`);
14
+ console.log("═".repeat(60) + "\n");
15
+ });
16
+
17
+ server.keepAliveTimeout = 65000;
18
+ server.headersTimeout = 66000;
19
+
20
+ const shutdown = (signal: string) => {
21
+ console.log(`\n[SHUTDOWN] ${signal} received`);
22
+ server.close(() => process.exit(0));
23
+ setTimeout(() => process.exit(1), 10000);
24
+ };
25
+
26
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
27
+ process.on("SIGINT", () => shutdown("SIGINT"));
28
+ process.on("uncaughtException", (e) => { console.error("[FATAL]", e); shutdown("uncaughtException"); });
29
+ process.on("unhandledRejection", (r) => console.error("[UNHANDLED]", r));
@@ -0,0 +1,169 @@
1
+ // ─── Enums / Literals ────────────────────────────────────────────────────────
2
+
3
+ export type ScreenshotMode = "always" | "on_failure" | "never";
4
+ export type StepStatus = "PASS" | "FAIL" | "SKIP" | "RUNNING" | "PENDING";
5
+ export type ScriptStatus = "PASS" | "FAIL" | "RUNNING" | "PENDING";
6
+ export type QueueStatus = "queued" | "running" | "done" | "failed" | "cancelled";
7
+ export type SSEEventType =
8
+ | "execution_start" | "script_start" | "step_start"
9
+ | "step_complete" | "script_complete"| "execution_complete"
10
+ | "execution_cancelled" | "queue_update" | "log" | "error" | "health";
11
+
12
+ // ─── TestData ─────────────────────────────────────────────────────────────────
13
+
14
+ export interface ITestData {
15
+ set(key: string, value: unknown): void;
16
+ get(key: string): unknown;
17
+ has(key: string): boolean;
18
+ resolve(value: unknown): unknown;
19
+ getAll(): Record<string, unknown>;
20
+ }
21
+
22
+ // ─── Config ───────────────────────────────────────────────────────────────────
23
+
24
+ export interface TestCaseConfig {
25
+ testData: ITestData;
26
+ screenshot_mode: ScreenshotMode;
27
+ record_video: boolean;
28
+ headless: boolean;
29
+ executionId: string;
30
+ timeout: number;
31
+ slowMo?: number;
32
+ }
33
+
34
+ // ─── Payload ──────────────────────────────────────────────────────────────────
35
+
36
+ export interface RequestPayload {
37
+ scripts: Script[];
38
+ stop_on_failure?: boolean;
39
+ parallel?: boolean;
40
+ max_parallel?: number;
41
+ }
42
+
43
+ export interface Script {
44
+ test_script_uid: string;
45
+ test_case_name?: string;
46
+ app_id?: string;
47
+ headless?: boolean;
48
+ screenshot_mode?: ScreenshotMode;
49
+ record_video?: boolean;
50
+ stop_on_failure?: boolean;
51
+ steps: Step[];
52
+ browser?: string;
53
+ }
54
+
55
+ export interface Step {
56
+ uid?: string;
57
+ step_name: string;
58
+ step_script: string;
59
+ label?: string;
60
+ obj_uid?: string;
61
+ page_uid?: string | null;
62
+ value?: string;
63
+ continue_on_failure?: boolean;
64
+ }
65
+
66
+ // ─── Results ──────────────────────────────────────────────────────────────────
67
+
68
+ export interface StepResult {
69
+ uid?: string;
70
+ obj_uid?: string;
71
+ step_name: string;
72
+ step_script?: string;
73
+ status: StepStatus;
74
+ page_uid?: string | null;
75
+ comments: string;
76
+ screenshot?: string;
77
+ duration_ms: number;
78
+ start_time: string;
79
+ expected_result?: string;
80
+ skip_reason?: string;
81
+ label?: string;
82
+ value?: string;
83
+ comparison_code?: any;
84
+ }
85
+
86
+ export interface ScriptResult {
87
+ test_script_uid: string;
88
+ test_case_name: string;
89
+ step_results: StepResult[];
90
+ overall_status: ScriptStatus;
91
+ app_id?: string;
92
+ duration_ms: number;
93
+ start_time: string;
94
+ browser?: string;
95
+ passed_steps: number;
96
+ failed_steps: number;
97
+ skipped_steps: number;
98
+ total_steps: number;
99
+ }
100
+
101
+ export interface ExecutionResult {
102
+ executionId: string;
103
+ overall_status: "PASS" | "FAIL" | "CANCELLED";
104
+ total_scripts: number;
105
+ passed: number;
106
+ failed: number;
107
+ duration_ms: number;
108
+ results: ScriptResult[];
109
+ }
110
+
111
+ // ─── Queue ────────────────────────────────────────────────────────────────────
112
+
113
+ export interface QueueJob {
114
+ executionId: string;
115
+ status: QueueStatus;
116
+ payload: RequestPayload;
117
+ queuedAt: string;
118
+ startedAt?: string;
119
+ completedAt?: string;
120
+ cancelledAt?: string;
121
+ result?: ExecutionResult;
122
+ error?: string;
123
+ // Cancellation token — set to true to request abort
124
+ cancelRequested: boolean;
125
+ progress: {
126
+ total_scripts: number;
127
+ completed_scripts: number;
128
+ total_steps: number;
129
+ completed_steps: number;
130
+ passed_steps: number;
131
+ failed_steps: number;
132
+ skipped_steps: number;
133
+ };
134
+ }
135
+ export interface LogEntry {
136
+ level: LogLevel;
137
+ message: string;
138
+ timestamp?: string;
139
+ }
140
+
141
+ // ─── Actions ──────────────────────────────────────────────────────────────────
142
+
143
+ export interface ActionResponse {
144
+ status: "Pass" | "Fail";
145
+ comments: string;
146
+ screenshot?: string;
147
+ data?: any;
148
+ }
149
+
150
+ export interface ParsedStep {
151
+ action: string;
152
+ args: string[];
153
+ }
154
+
155
+ export type ActionHandler = (args: string[]) => Promise<ActionResponse | undefined>;
156
+ export interface IActionHandler {
157
+ getActions(): Record<string, ActionHandler>;
158
+ }
159
+
160
+ // ─── SSE ─────────────────────────────────────────────────────────────────────
161
+
162
+ export interface SSEEvent {
163
+ type: SSEEventType;
164
+ executionId: string;
165
+ timestamp: string;
166
+ data: any;
167
+ }
168
+
169
+ export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";