testblocks 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli/executor.d.ts +32 -0
  4. package/dist/cli/executor.js +517 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +411 -0
  7. package/dist/cli/reporters.d.ts +62 -0
  8. package/dist/cli/reporters.js +451 -0
  9. package/dist/client/assets/index-4hbFPUhP.js +2087 -0
  10. package/dist/client/assets/index-4hbFPUhP.js.map +1 -0
  11. package/dist/client/assets/index-Dnk1ti7l.css +1 -0
  12. package/dist/client/index.html +25 -0
  13. package/dist/core/blocks/api.d.ts +2 -0
  14. package/dist/core/blocks/api.js +610 -0
  15. package/dist/core/blocks/data-driven.d.ts +2 -0
  16. package/dist/core/blocks/data-driven.js +245 -0
  17. package/dist/core/blocks/index.d.ts +15 -0
  18. package/dist/core/blocks/index.js +71 -0
  19. package/dist/core/blocks/lifecycle.d.ts +2 -0
  20. package/dist/core/blocks/lifecycle.js +199 -0
  21. package/dist/core/blocks/logic.d.ts +2 -0
  22. package/dist/core/blocks/logic.js +357 -0
  23. package/dist/core/blocks/playwright.d.ts +2 -0
  24. package/dist/core/blocks/playwright.js +764 -0
  25. package/dist/core/blocks/procedures.d.ts +5 -0
  26. package/dist/core/blocks/procedures.js +321 -0
  27. package/dist/core/index.d.ts +5 -0
  28. package/dist/core/index.js +44 -0
  29. package/dist/core/plugins.d.ts +66 -0
  30. package/dist/core/plugins.js +118 -0
  31. package/dist/core/types.d.ts +153 -0
  32. package/dist/core/types.js +2 -0
  33. package/dist/server/codegenManager.d.ts +54 -0
  34. package/dist/server/codegenManager.js +259 -0
  35. package/dist/server/codegenParser.d.ts +17 -0
  36. package/dist/server/codegenParser.js +598 -0
  37. package/dist/server/executor.d.ts +37 -0
  38. package/dist/server/executor.js +672 -0
  39. package/dist/server/globals.d.ts +85 -0
  40. package/dist/server/globals.js +273 -0
  41. package/dist/server/index.d.ts +2 -0
  42. package/dist/server/index.js +361 -0
  43. package/dist/server/plugins.d.ts +55 -0
  44. package/dist/server/plugins.js +206 -0
  45. package/package.json +103 -0
@@ -0,0 +1,153 @@
1
+ export interface TestFile {
2
+ version: string;
3
+ name: string;
4
+ description?: string;
5
+ variables?: Record<string, VariableDefinition>;
6
+ beforeAll?: TestStep[];
7
+ afterAll?: TestStep[];
8
+ beforeEach?: TestStep[];
9
+ afterEach?: TestStep[];
10
+ procedures?: Record<string, ProcedureDefinition>;
11
+ tests: TestCase[];
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+ export interface VariableDefinition {
15
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array';
16
+ default?: unknown;
17
+ description?: string;
18
+ }
19
+ export interface ProcedureDefinition {
20
+ name: string;
21
+ description?: string;
22
+ params?: ProcedureParam[];
23
+ returnType?: string;
24
+ steps: TestStep[];
25
+ }
26
+ export interface ProcedureParam {
27
+ name: string;
28
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
29
+ default?: unknown;
30
+ description?: string;
31
+ }
32
+ export interface TestCase {
33
+ id: string;
34
+ name: string;
35
+ description?: string;
36
+ data?: TestDataSet[];
37
+ dataFile?: string;
38
+ beforeEach?: TestStep[];
39
+ afterEach?: TestStep[];
40
+ steps: TestStep[];
41
+ tags?: string[];
42
+ }
43
+ export interface TestDataSet {
44
+ name?: string;
45
+ values: Record<string, unknown>;
46
+ }
47
+ export interface TestStep {
48
+ id: string;
49
+ type: string;
50
+ params: Record<string, unknown>;
51
+ children?: TestStep[];
52
+ }
53
+ export interface BlockDefinition {
54
+ type: string;
55
+ category: string;
56
+ color: string;
57
+ tooltip?: string;
58
+ helpUrl?: string;
59
+ inputs: BlockInput[];
60
+ output?: BlockOutput;
61
+ previousStatement?: boolean;
62
+ nextStatement?: boolean;
63
+ execute: (params: Record<string, unknown>, context: ExecutionContext) => Promise<unknown>;
64
+ }
65
+ export interface BlockInput {
66
+ name: string;
67
+ type: 'value' | 'statement' | 'field';
68
+ fieldType?: 'text' | 'number' | 'dropdown' | 'checkbox' | 'variable' | 'multiline';
69
+ options?: Array<[string, string]>;
70
+ check?: string | string[];
71
+ default?: unknown;
72
+ required?: boolean;
73
+ }
74
+ export interface BlockOutput {
75
+ type: string | string[];
76
+ }
77
+ export interface ExecutionContext {
78
+ variables: Map<string, unknown>;
79
+ results: TestResult[];
80
+ browser?: unknown;
81
+ page?: unknown;
82
+ apiClient?: unknown;
83
+ logger: Logger;
84
+ plugins: Map<string, Plugin>;
85
+ abortSignal?: AbortSignal;
86
+ procedures?: Map<string, ProcedureDefinition>;
87
+ currentData?: TestDataSet;
88
+ dataIndex?: number;
89
+ testIdAttribute?: string;
90
+ }
91
+ export interface Logger {
92
+ info(message: string, data?: unknown): void;
93
+ warn(message: string, data?: unknown): void;
94
+ error(message: string, data?: unknown): void;
95
+ debug(message: string, data?: unknown): void;
96
+ }
97
+ export interface TestResult {
98
+ testId: string;
99
+ testName: string;
100
+ status: 'passed' | 'failed' | 'skipped' | 'error';
101
+ duration: number;
102
+ steps: StepResult[];
103
+ error?: ErrorInfo;
104
+ startedAt: string;
105
+ finishedAt: string;
106
+ dataIteration?: {
107
+ index: number;
108
+ name?: string;
109
+ data: Record<string, unknown>;
110
+ };
111
+ isLifecycle?: boolean;
112
+ lifecycleType?: 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';
113
+ }
114
+ export interface StepResult {
115
+ stepId: string;
116
+ stepType: string;
117
+ status: 'passed' | 'failed' | 'skipped' | 'error';
118
+ duration: number;
119
+ output?: unknown;
120
+ error?: ErrorInfo;
121
+ screenshot?: string;
122
+ }
123
+ export interface ErrorInfo {
124
+ message: string;
125
+ stack?: string;
126
+ code?: string;
127
+ }
128
+ export interface Plugin {
129
+ name: string;
130
+ version: string;
131
+ description?: string;
132
+ blocks: BlockDefinition[];
133
+ hooks?: PluginHooks;
134
+ }
135
+ export interface PluginHooks {
136
+ beforeAll?: (context: ExecutionContext) => Promise<void>;
137
+ afterAll?: (context: ExecutionContext) => Promise<void>;
138
+ beforeTest?: (context: ExecutionContext, test: TestCase) => Promise<void>;
139
+ afterTest?: (context: ExecutionContext, test: TestCase, result: TestResult) => Promise<void>;
140
+ beforeStep?: (context: ExecutionContext, step: TestStep) => Promise<void>;
141
+ afterStep?: (context: ExecutionContext, step: TestStep, result: StepResult) => Promise<void>;
142
+ }
143
+ export interface CLIConfig {
144
+ testFiles: string[];
145
+ outputDir?: string;
146
+ reporter?: 'console' | 'json' | 'junit' | 'html';
147
+ parallel?: number;
148
+ timeout?: number;
149
+ headless?: boolean;
150
+ baseUrl?: string;
151
+ variables?: Record<string, unknown>;
152
+ plugins?: string[];
153
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Playwright Codegen Manager
3
+ *
4
+ * Manages the lifecycle of Playwright codegen recording sessions.
5
+ */
6
+ import { ChildProcess } from 'child_process';
7
+ import { TestStep } from '../core';
8
+ export interface CodegenSession {
9
+ id: string;
10
+ process: ChildProcess | null;
11
+ outputFile: string;
12
+ url: string;
13
+ status: 'running' | 'completed' | 'error';
14
+ error?: string;
15
+ startedAt: number;
16
+ testIdAttribute?: string;
17
+ }
18
+ /**
19
+ * Manages Playwright codegen sessions
20
+ */
21
+ export declare class CodegenManager {
22
+ private sessions;
23
+ /**
24
+ * Generate a unique session ID
25
+ */
26
+ private generateSessionId;
27
+ /**
28
+ * Start a new recording session
29
+ */
30
+ startRecording(url: string, options?: {
31
+ testIdAttribute?: string;
32
+ }): Promise<string>;
33
+ /**
34
+ * Stop a recording session and get the recorded steps
35
+ */
36
+ stopRecording(sessionId: string): Promise<TestStep[]>;
37
+ /**
38
+ * Get the status of a session
39
+ */
40
+ getStatus(sessionId: string): CodegenSession | undefined;
41
+ /**
42
+ * Clean up a session (remove temp file)
43
+ */
44
+ cleanup(sessionId: string): void;
45
+ /**
46
+ * Clean up all sessions (call on server shutdown)
47
+ */
48
+ cleanupAll(): void;
49
+ /**
50
+ * Get all active sessions
51
+ */
52
+ getActiveSessions(): CodegenSession[];
53
+ }
54
+ export declare const codegenManager: CodegenManager;
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ /**
3
+ * Playwright Codegen Manager
4
+ *
5
+ * Manages the lifecycle of Playwright codegen recording sessions.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.codegenManager = exports.CodegenManager = void 0;
42
+ const child_process_1 = require("child_process");
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const codegenParser_1 = require("./codegenParser");
47
+ /**
48
+ * Manages Playwright codegen sessions
49
+ */
50
+ class CodegenManager {
51
+ constructor() {
52
+ this.sessions = new Map();
53
+ }
54
+ /**
55
+ * Generate a unique session ID
56
+ */
57
+ generateSessionId() {
58
+ return `rec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
59
+ }
60
+ /**
61
+ * Start a new recording session
62
+ */
63
+ async startRecording(url, options) {
64
+ const sessionId = this.generateSessionId();
65
+ const outputFile = path.join(os.tmpdir(), `testblocks-codegen-${sessionId}.js`);
66
+ console.log(`[CodegenManager] Starting recording session ${sessionId}`);
67
+ console.log(`[CodegenManager] URL: ${url}`);
68
+ console.log(`[CodegenManager] Output file: ${outputFile}`);
69
+ if (options?.testIdAttribute) {
70
+ console.log(`[CodegenManager] Test ID Attribute: ${options.testIdAttribute}`);
71
+ }
72
+ // Create the session
73
+ const session = {
74
+ id: sessionId,
75
+ process: null,
76
+ outputFile,
77
+ url,
78
+ status: 'running',
79
+ startedAt: Date.now(),
80
+ testIdAttribute: options?.testIdAttribute,
81
+ };
82
+ this.sessions.set(sessionId, session);
83
+ try {
84
+ // Spawn Playwright codegen
85
+ // Use npx to ensure we use the local playwright installation
86
+ const args = [
87
+ 'playwright',
88
+ 'codegen',
89
+ '--output', outputFile,
90
+ ];
91
+ // Add test-id-attribute if specified
92
+ if (options?.testIdAttribute) {
93
+ args.push('--test-id-attribute', options.testIdAttribute);
94
+ }
95
+ args.push(url);
96
+ const codegenProcess = (0, child_process_1.spawn)('npx', args, {
97
+ stdio: ['pipe', 'pipe', 'pipe'],
98
+ detached: false,
99
+ });
100
+ session.process = codegenProcess;
101
+ // Handle process exit
102
+ codegenProcess.on('exit', (code) => {
103
+ console.log(`[CodegenManager] Session ${sessionId} codegen process exited with code ${code}`);
104
+ const currentSession = this.sessions.get(sessionId);
105
+ if (currentSession && currentSession.status === 'running') {
106
+ currentSession.status = 'completed';
107
+ currentSession.process = null;
108
+ }
109
+ });
110
+ // Handle errors
111
+ codegenProcess.on('error', (err) => {
112
+ console.error(`[CodegenManager] Session ${sessionId} process error:`, err);
113
+ const currentSession = this.sessions.get(sessionId);
114
+ if (currentSession) {
115
+ currentSession.status = 'error';
116
+ currentSession.error = err.message;
117
+ currentSession.process = null;
118
+ }
119
+ });
120
+ // Log stderr for debugging
121
+ codegenProcess.stderr?.on('data', (data) => {
122
+ console.log(`[CodegenManager] Session ${sessionId} stderr:`, data.toString());
123
+ });
124
+ return sessionId;
125
+ }
126
+ catch (error) {
127
+ console.error(`[CodegenManager] Failed to start recording:`, error);
128
+ session.status = 'error';
129
+ session.error = error.message;
130
+ throw error;
131
+ }
132
+ }
133
+ /**
134
+ * Stop a recording session and get the recorded steps
135
+ */
136
+ async stopRecording(sessionId) {
137
+ const session = this.sessions.get(sessionId);
138
+ if (!session) {
139
+ throw new Error(`Session ${sessionId} not found`);
140
+ }
141
+ console.log(`[CodegenManager] Stopping session ${sessionId}`);
142
+ // If process is still running, terminate it
143
+ if (session.process) {
144
+ console.log(`[CodegenManager] Terminating codegen process for session ${sessionId}`);
145
+ // Send SIGTERM to gracefully close the browser
146
+ session.process.kill('SIGTERM');
147
+ // Wait for process to exit (with timeout)
148
+ await new Promise((resolve) => {
149
+ const timeout = setTimeout(() => {
150
+ // Force kill if not exited
151
+ if (session.process) {
152
+ session.process.kill('SIGKILL');
153
+ }
154
+ resolve();
155
+ }, 5000);
156
+ if (session.process) {
157
+ session.process.on('exit', () => {
158
+ clearTimeout(timeout);
159
+ resolve();
160
+ });
161
+ }
162
+ else {
163
+ clearTimeout(timeout);
164
+ resolve();
165
+ }
166
+ });
167
+ session.process = null;
168
+ }
169
+ // Wait a bit for file to be written
170
+ await new Promise(resolve => setTimeout(resolve, 500));
171
+ // Read and parse the generated file
172
+ let steps = [];
173
+ try {
174
+ if (fs.existsSync(session.outputFile)) {
175
+ const code = fs.readFileSync(session.outputFile, 'utf-8');
176
+ console.log(`[CodegenManager] Read generated code (${code.length} bytes)`);
177
+ // Pass the testIdAttribute for correct selector conversion
178
+ steps = (0, codegenParser_1.parsePlaywrightCode)(code, session.testIdAttribute || 'data-testid');
179
+ console.log(`[CodegenManager] Parsed ${steps.length} steps`);
180
+ }
181
+ else {
182
+ console.log(`[CodegenManager] Output file not found: ${session.outputFile}`);
183
+ }
184
+ }
185
+ catch (error) {
186
+ console.error(`[CodegenManager] Failed to read/parse output file:`, error);
187
+ }
188
+ // Update session status
189
+ session.status = 'completed';
190
+ return steps;
191
+ }
192
+ /**
193
+ * Get the status of a session
194
+ */
195
+ getStatus(sessionId) {
196
+ const session = this.sessions.get(sessionId);
197
+ if (!session) {
198
+ return undefined;
199
+ }
200
+ // Check if process has exited
201
+ if (session.process && session.status === 'running') {
202
+ // Check if the process is still alive
203
+ try {
204
+ // kill(0) checks if process exists without killing it
205
+ process.kill(session.process.pid, 0);
206
+ }
207
+ catch {
208
+ // Process no longer exists
209
+ session.status = 'completed';
210
+ session.process = null;
211
+ }
212
+ }
213
+ return session;
214
+ }
215
+ /**
216
+ * Clean up a session (remove temp file)
217
+ */
218
+ cleanup(sessionId) {
219
+ const session = this.sessions.get(sessionId);
220
+ if (!session) {
221
+ return;
222
+ }
223
+ console.log(`[CodegenManager] Cleaning up session ${sessionId}`);
224
+ // Kill process if still running
225
+ if (session.process) {
226
+ session.process.kill('SIGKILL');
227
+ }
228
+ // Remove temp file
229
+ try {
230
+ if (fs.existsSync(session.outputFile)) {
231
+ fs.unlinkSync(session.outputFile);
232
+ console.log(`[CodegenManager] Removed temp file: ${session.outputFile}`);
233
+ }
234
+ }
235
+ catch (error) {
236
+ console.error(`[CodegenManager] Failed to remove temp file:`, error);
237
+ }
238
+ // Remove session from map
239
+ this.sessions.delete(sessionId);
240
+ }
241
+ /**
242
+ * Clean up all sessions (call on server shutdown)
243
+ */
244
+ cleanupAll() {
245
+ console.log(`[CodegenManager] Cleaning up all sessions`);
246
+ for (const sessionId of this.sessions.keys()) {
247
+ this.cleanup(sessionId);
248
+ }
249
+ }
250
+ /**
251
+ * Get all active sessions
252
+ */
253
+ getActiveSessions() {
254
+ return Array.from(this.sessions.values()).filter(s => s.status === 'running');
255
+ }
256
+ }
257
+ exports.CodegenManager = CodegenManager;
258
+ // Singleton instance
259
+ exports.codegenManager = new CodegenManager();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Playwright Codegen Parser
3
+ *
4
+ * Parses Playwright-generated JavaScript code and converts it to TestBlocks TestStep format.
5
+ * Converts Playwright locators to CSS selectors where possible.
6
+ */
7
+ import { TestStep } from '../core';
8
+ /**
9
+ * Parse Playwright-generated code and convert to TestBlocks TestStep array
10
+ * @param code - The Playwright-generated code
11
+ * @param testIdAttribute - The attribute used for test IDs (default: 'data-testid')
12
+ */
13
+ export declare function parsePlaywrightCode(code: string, testIdAttribute?: string): TestStep[];
14
+ /**
15
+ * Check if the code looks like valid Playwright test code
16
+ */
17
+ export declare function isValidPlaywrightCode(code: string): boolean;