@testsmith/testblocks 0.9.2 → 0.9.4

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.
@@ -32,7 +32,10 @@ class TestExecutor {
32
32
  this.browser = await playwright_1.chromium.launch({
33
33
  headless: this.options.headless,
34
34
  });
35
- this.browserContext = await this.browser.newContext();
35
+ // Use consistent viewport size for headless and headed modes
36
+ this.browserContext = await this.browser.newContext({
37
+ viewport: { width: 1920, height: 1080 },
38
+ });
36
39
  this.page = await this.browserContext.newPage();
37
40
  if (this.options.timeout) {
38
41
  this.page.setDefaultTimeout(this.options.timeout);
package/dist/cli/index.js CHANGED
@@ -454,7 +454,7 @@ program
454
454
  'test:ci': 'testblocks run tests/**/*.testblocks.json -r console,html,junit -o reports',
455
455
  },
456
456
  devDependencies: {
457
- '@testsmith/testblocks': '^0.9.2',
457
+ '@testsmith/testblocks': '^0.9.4',
458
458
  },
459
459
  };
460
460
  fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
@@ -8,11 +8,23 @@ class ConsoleReporter {
8
8
  onTestFileComplete(file, testFile, results) {
9
9
  console.log('');
10
10
  for (const result of results) {
11
- const icon = result.status === 'passed' ? '✓' : '✗';
12
- const color = result.status === 'passed' ? '\x1b[32m' : '\x1b[31m';
11
+ let icon;
12
+ let color;
13
+ if (result.status === 'passed') {
14
+ icon = '✓';
15
+ color = '\x1b[32m'; // green
16
+ }
17
+ else if (result.status === 'skipped') {
18
+ icon = '○';
19
+ color = '\x1b[33m'; // yellow/orange
20
+ }
21
+ else {
22
+ icon = '✗';
23
+ color = '\x1b[31m'; // red
24
+ }
13
25
  const reset = '\x1b[0m';
14
26
  console.log(`${color} ${icon} ${result.testName}${reset} (${result.duration}ms)`);
15
- if (result.error) {
27
+ if (result.error && result.status !== 'skipped') {
16
28
  console.log(` ${result.error.message}`);
17
29
  }
18
30
  }
@@ -21,10 +33,19 @@ class ConsoleReporter {
21
33
  onComplete(allResults) {
22
34
  const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
23
35
  const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
24
- const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
36
+ const skipped = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'skipped').length, 0);
37
+ const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'failed' || t.status === 'error').length, 0);
25
38
  const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
26
39
  console.log('─'.repeat(50));
27
- console.log(`Tests: ${passed} passed, ${failed} failed, ${totalTests} total`);
40
+ let summary = `Tests: ${passed} passed`;
41
+ if (failed > 0) {
42
+ summary += `, ${failed} failed`;
43
+ }
44
+ if (skipped > 0) {
45
+ summary += `, ${skipped} skipped`;
46
+ }
47
+ summary += `, ${totalTests} total`;
48
+ console.log(summary);
28
49
  console.log(`Duration: ${(totalDuration / 1000).toFixed(2)}s`);
29
50
  console.log(`Test Files: ${allResults.length}`);
30
51
  console.log('─'.repeat(50));
@@ -2191,5 +2191,5 @@ def ${p.FUNCTION_NAME_PLACEHOLDER_}(msg):
2191
2191
  {"username": "user3", "password": "pass3"}
2192
2192
  ]`})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Using Data Values"}),E.jsxs("ol",{children:[E.jsxs("li",{children:["Use ",E.jsx("strong",{children:"Get Current Value"})," block with field name"]}),E.jsxs("li",{children:["Example: Get Current Value ",E.jsx("code",{children:"username"})]}),E.jsxs("li",{children:["Or use in text: ",E.jsx("code",{children:"${data.username}"})]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Other Data Blocks"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("strong",{children:"Data Table"})," - define data with headers and rows"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"CSV Data"})," - parse CSV-style data"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Range"})," - generate number sequences (1 to 10)"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"For Each Data"})," - iterate within a test"]})]})]}),E.jsxs("div",{className:"help-tip",children:[E.jsx("strong",{children:"Example Use Case:"})," Test login with valid users, invalid passwords, empty fields, and special characters all in one test."]})]})},{id:"custom-blocks",title:"Custom Blocks",content:E.jsxs(E.Fragment,{children:[E.jsx("h3",{children:"Creating Reusable Blocks"}),E.jsx("p",{children:"Turn any sequence of blocks into a reusable custom block."}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Creating a Custom Block"}),E.jsxs("ol",{children:[E.jsx("li",{children:"Build your block sequence in the workspace"}),E.jsxs("li",{children:[E.jsx("strong",{children:"Select multiple blocks"})," using Shift+Click or Ctrl/Cmd+Click"]}),E.jsxs("li",{children:["Right-click and choose ",E.jsx("strong",{children:'"Create Reusable Block"'})]}),E.jsx("li",{children:'Name your block (e.g., "Login", "Add to Cart")'}),E.jsx("li",{children:"Choose which values should be parameters"}),E.jsxs("li",{children:["Click ",E.jsx("strong",{children:"Create Block"})]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Using Custom Blocks"}),E.jsxs("ol",{children:[E.jsxs("li",{children:["Find your block in the ",E.jsx("strong",{children:"Custom"})," category in the toolbox"]}),E.jsx("li",{children:"Drag it into any test"}),E.jsx("li",{children:"Fill in the parameter values"})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Editing Custom Blocks"}),E.jsxs("ol",{children:[E.jsx("li",{children:"Right-click on any instance of your custom block"}),E.jsxs("li",{children:["Choose ",E.jsx("strong",{children:'"Edit Reusable Block"'})]}),E.jsx("li",{children:"Modify the steps or parameters"}),E.jsxs("li",{children:["Click ",E.jsx("strong",{children:"Save Changes"})]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Parameters"}),E.jsx("p",{children:"When creating a custom block, you can choose which field values become parameters:"}),E.jsxs("ul",{children:[E.jsx("li",{children:"Checked = user can customize this value when using the block"}),E.jsx("li",{children:"Unchecked = value is fixed inside the block"})]})]}),E.jsxs("div",{className:"help-tip",children:[E.jsx("strong",{children:"Best Practice:"})," Create custom blocks for common flows like login, navigation, or form filling to keep tests DRY (Don't Repeat Yourself)."]})]})},{id:"lifecycle",title:"Lifecycle Hooks",content:E.jsxs(E.Fragment,{children:[E.jsx("h3",{children:"Setup & Teardown"}),E.jsx("p",{children:"Run code before/after tests using lifecycle hooks."}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Available Hooks"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("strong",{children:"Before All"})," - runs once before all tests start"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"After All"})," - runs once after all tests complete"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Before Each"})," - runs before each individual test"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"After Each"})," - runs after each individual test"]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Accessing Lifecycle Tabs"}),E.jsxs("ol",{children:[E.jsxs("li",{children:["Click the tabs above the workspace: ",E.jsx("strong",{children:"Test | Before All | After All | Before Each | After Each"})]}),E.jsx("li",{children:"Add blocks to the selected lifecycle phase"}),E.jsx("li",{children:"Blocks run automatically at the appropriate time"})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Common Use Cases"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("strong",{children:"Before All:"})," Set up test data, authenticate API"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Before Each:"})," Navigate to starting page, reset state"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"After Each:"})," Take screenshot, log out user"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"After All:"})," Clean up test data, close connections"]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Special Lifecycle Blocks"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("strong",{children:"On Failure"})," - only runs if the test fails"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Skip If"})," - skip test based on condition"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Retry"})," - retry failed steps automatically"]})]})]}),E.jsxs("div",{className:"help-tip",children:[E.jsx("strong",{children:"Tip:"})," Use Before Each to ensure each test starts from a clean state, making tests independent and reliable."]})]})},{id:"logic",title:"Logic & Control Flow",content:E.jsxs(E.Fragment,{children:[E.jsx("h3",{children:"Control Flow"}),E.jsx("p",{children:"Add conditional logic, loops, and error handling to your tests."}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Conditional Execution (If/Else)"}),E.jsxs("ol",{children:[E.jsxs("li",{children:["Use the ",E.jsx("strong",{children:"If"})," block from Logic category"]}),E.jsxs("li",{children:["Connect a condition block (e.g., ",E.jsx("strong",{children:"Compare"}),")"]}),E.jsx("li",{children:'Add blocks inside the "then" section'}),E.jsx("li",{children:'Optionally add blocks to the "else" section'})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Comparisons"}),E.jsxs("p",{children:["Use the ",E.jsx("strong",{children:"Compare"})," block to check:"]}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("code",{children:"="})," equals"]}),E.jsxs("li",{children:[E.jsx("code",{children:"≠"})," not equals"]}),E.jsxs("li",{children:[E.jsx("code",{children:"<"})," ",E.jsx("code",{children:">"})," ",E.jsx("code",{children:"≤"})," ",E.jsx("code",{children:"≥"})," numeric comparisons"]}),E.jsxs("li",{children:[E.jsx("code",{children:"contains"})," text contains"]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Loops"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("strong",{children:"Repeat"})," - run blocks N times"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"For Each"})," - iterate over an array"]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Error Handling"}),E.jsxs("ol",{children:[E.jsxs("li",{children:["Use ",E.jsx("strong",{children:"Try/Catch"})," to handle potential failures"]}),E.jsx("li",{children:'Blocks in "try" run first'}),E.jsx("li",{children:'If they fail, "catch" blocks run instead'})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Logging & Debugging"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("strong",{children:"Log"})," - output messages (info, warn, error, debug)"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Comment"})," - add notes (doesn't run)"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Fail"})," - force test to fail with message"]})]})]})]})},{id:"tips",title:"Tips & Tricks",content:E.jsxs(E.Fragment,{children:[E.jsx("h3",{children:"Pro Tips"}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Selectors"}),E.jsx("p",{children:"Best practices for finding elements:"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("code",{children:"#id"})," - by ID (most reliable)"]}),E.jsxs("li",{children:[E.jsx("code",{children:'[data-testid="value"]'})," - by test ID attribute"]}),E.jsxs("li",{children:[E.jsx("code",{children:".class-name"})," - by CSS class"]}),E.jsxs("li",{children:[E.jsx("code",{children:"text=Click me"})," - by visible text"]}),E.jsxs("li",{children:[E.jsx("code",{children:'button:has-text("Submit")'})," - button containing text"]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Keyboard Shortcuts"}),E.jsxs("ul",{children:[E.jsxs("li",{children:[E.jsx("strong",{children:"Delete/Backspace"})," - delete selected block"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Ctrl/Cmd + C/V"})," - copy/paste blocks"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Ctrl/Cmd + Z"})," - undo"]}),E.jsxs("li",{children:[E.jsx("strong",{children:"Shift + Click"})," - select multiple blocks"]})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Debugging Failed Tests"}),E.jsxs("ol",{children:[E.jsxs("li",{children:["Turn off ",E.jsx("strong",{children:"Headless"})," mode to watch the browser"]}),E.jsxs("li",{children:["Add ",E.jsx("strong",{children:"Wait"})," blocks to slow down execution"]}),E.jsxs("li",{children:["Use ",E.jsx("strong",{children:"Screenshot"})," blocks to capture state"]}),E.jsx("li",{children:"Check the Results panel for error details"})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Test Organization"}),E.jsxs("ul",{children:[E.jsx("li",{children:"One test file per feature or page"}),E.jsx("li",{children:"Use descriptive test names"}),E.jsx("li",{children:"Keep tests independent (don't rely on other tests)"}),E.jsx("li",{children:"Use lifecycle hooks for common setup"})]})]}),E.jsxs("div",{className:"help-feature",children:[E.jsx("h4",{children:"Performance Tips"}),E.jsxs("ul",{children:[E.jsxs("li",{children:["Avoid fixed ",E.jsx("strong",{children:"Wait"})," blocks - use ",E.jsx("strong",{children:"Wait for Element"})," instead"]}),E.jsxs("li",{children:["Run tests in ",E.jsx("strong",{children:"Headless"})," mode for speed"]}),E.jsx("li",{children:"Use API calls to set up test data (faster than UI)"})]})]})]})}];function Vy({isOpen:f,onClose:g}){const[i,a]=Re.useState("getting-started");if(!f)return null;const v=TE.find(A=>A.id===i);return E.jsx("div",{className:"modal-overlay",onClick:g,children:E.jsxs("div",{className:"modal help-modal",onClick:A=>A.stopPropagation(),children:[E.jsxs("div",{className:"help-header",children:[E.jsx("h2",{children:"TestBlocks Guide"}),E.jsx("button",{className:"btn-close",onClick:g,children:"×"})]}),E.jsxs("div",{className:"help-layout",children:[E.jsx("nav",{className:"help-nav",children:TE.map(A=>E.jsx("button",{className:`help-nav-item ${i===A.id?"active":""}`,onClick:()=>a(A.id),children:A.title},A.id))}),E.jsx("div",{className:"help-content",children:v==null?void 0:v.content})]})]})})}function Hy(f){switch(f.type){case"web_navigate":return`Navigate → ${f.params.URL}`;case"web_click":return`Click → ${f.params.SELECTOR}`;case"web_fill":return`Fill → ${f.params.SELECTOR}`;case"web_type":return`Type → ${f.params.SELECTOR}`;case"web_checkbox":return`${f.params.ACTION==="check"?"Check":"Uncheck"} → ${f.params.SELECTOR}`;case"web_select":return`Select → ${f.params.SELECTOR}`;case"web_hover":return`Hover → ${f.params.SELECTOR}`;case"web_press_key":return`Press Key → ${f.params.KEY}`;case"web_wait_for_element":return`Wait for → ${f.params.SELECTOR}`;case"web_wait":return`Wait → ${f.params.DURATION}ms`;default:return f.type}}function Gy({isOpen:f,onClose:g,onStepsRecorded:i}){const[a,v]=Re.useState({stage:"url-input",url:"https://",sessionId:null,steps:[],error:null,showAdvanced:!1,options:{testIdAttribute:"data-testid"}}),A=Re.useRef(null);Re.useEffect(()=>(f&&fetch("/api/globals").then(Z=>Z.json()).then(Z=>{v({stage:"url-input",url:"https://",sessionId:null,steps:[],error:null,showAdvanced:!1,options:{testIdAttribute:Z.testIdAttribute||"data-testid"}})}).catch(()=>{v({stage:"url-input",url:"https://",sessionId:null,steps:[],error:null,showAdvanced:!1,options:{testIdAttribute:"data-testid"}})}),()=>{A.current&&(clearInterval(A.current),A.current=null)}),[f]),Re.useEffect(()=>{if(a.stage==="recording"&&a.sessionId)return A.current=setInterval(async()=>{try{const W=await(await fetch(`/api/record/status/${a.sessionId}`)).json();W.status==="completed"?V():W.status==="error"&&(v(X=>({...X,stage:"error",error:W.error||"Recording failed"})),A.current&&(clearInterval(A.current),A.current=null))}catch(Z){console.error("Failed to poll recording status:",Z)}},2e3),()=>{A.current&&(clearInterval(A.current),A.current=null)}},[a.stage,a.sessionId]);const R=Re.useCallback(async()=>{if(!a.url||a.url==="https://"){v(Z=>({...Z,error:"Please enter a valid URL"}));return}v(Z=>({...Z,stage:"processing",error:null}));try{const Z=await fetch("/api/record/start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:a.url,testIdAttribute:a.options.testIdAttribute||void 0})}),W=await Z.json();if(!Z.ok)throw new Error(W.message||W.error||"Failed to start recording");v(X=>({...X,stage:"recording",sessionId:W.sessionId}))}catch(Z){v(W=>({...W,stage:"error",error:Z.message}))}},[a.url,a.options.testIdAttribute]),V=Re.useCallback(async()=>{if(a.sessionId){A.current&&(clearInterval(A.current),A.current=null),v(Z=>({...Z,stage:"processing"}));try{const Z=await fetch("/api/record/stop",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:a.sessionId})}),W=await Z.json();if(!Z.ok)throw new Error(W.message||W.error||"Failed to stop recording");v(X=>({...X,stage:"preview",steps:W.steps||[]}))}catch(Z){v(W=>({...W,stage:"error",error:Z.message}))}}},[a.sessionId]),H=Re.useCallback(()=>{i(a.steps,"append"),g()},[a.steps,i,g]),re=Re.useCallback(()=>{i(a.steps,"new"),g()},[a.steps,i,g]),pe=Re.useCallback(()=>{if(a.stage==="recording"){if(!confirm("Recording is in progress. Are you sure you want to cancel?"))return;a.sessionId&&fetch("/api/record/stop",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:a.sessionId})}).catch(()=>{})}g()},[a.stage,a.sessionId,g]);return f?E.jsx("div",{className:"modal-overlay",onClick:pe,children:E.jsxs("div",{className:"modal record-dialog",onClick:Z=>Z.stopPropagation(),children:[E.jsxs("div",{className:"modal-header",children:[E.jsx("h2",{children:"Record Browser Actions"}),E.jsx("button",{className:"btn-close",onClick:pe,children:"×"})]}),E.jsxs("div",{className:"modal-body",children:[a.stage==="url-input"&&E.jsxs("div",{className:"record-url-input",children:[E.jsx("p",{children:"Enter the starting URL for recording:"}),E.jsx("input",{type:"url",className:"url-input",value:a.url,onChange:Z=>v(W=>({...W,url:Z.target.value,error:null})),placeholder:"https://example.com",autoFocus:!0}),a.error&&E.jsx("div",{className:"record-error",children:a.error}),E.jsx("p",{className:"record-hint",children:"A browser window will open where you can perform actions. Close the browser when you're done recording."}),E.jsxs("div",{className:"advanced-options",children:[E.jsxs("button",{type:"button",className:"advanced-toggle",onClick:()=>v(Z=>({...Z,showAdvanced:!Z.showAdvanced})),children:[E.jsx("span",{className:"toggle-icon",children:a.showAdvanced?"▼":"▶"}),"Advanced Options"]}),a.showAdvanced&&E.jsxs("div",{className:"advanced-content",children:[E.jsxs("div",{className:"option-row",children:[E.jsx("label",{htmlFor:"testIdAttribute",children:"Test ID Attribute:"}),E.jsx("input",{id:"testIdAttribute",type:"text",className:"option-input",value:a.options.testIdAttribute,onChange:Z=>{const W=Z.target.value;v(X=>({...X,options:{...X.options,testIdAttribute:W}}))},onBlur:Z=>{const W=Z.target.value.trim();W&&fetch("/api/globals/test-id-attribute",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({testIdAttribute:W})}).catch(console.error)},placeholder:"data-testid"})]}),E.jsx("p",{className:"option-hint",children:"Attribute used to identify elements (e.g., data-testid, data-test, data-cy). This setting is saved globally."})]})]})]}),a.stage==="recording"&&E.jsxs("div",{className:"record-status recording",children:[E.jsx("div",{className:"recording-indicator"}),E.jsxs("div",{className:"recording-info",children:[E.jsx("h3",{children:"Recording in progress..."}),E.jsx("p",{children:"Perform your test actions in the browser window."}),E.jsx("p",{children:'Close the browser or click "Stop Recording" when done.'})]})]}),a.stage==="processing"&&E.jsxs("div",{className:"record-status processing",children:[E.jsx("div",{className:"processing-spinner"}),E.jsx("p",{children:"Processing recorded actions..."})]}),a.stage==="preview"&&E.jsx("div",{className:"record-preview",children:a.steps.length===0?E.jsxs("div",{className:"record-empty",children:[E.jsx("p",{children:"No actions were recorded."}),E.jsx("p",{className:"record-hint",children:"Make sure to interact with elements in the browser (click, type, navigate, etc.)"})]}):E.jsxs(E.Fragment,{children:[E.jsxs("h3",{children:["Recorded ",a.steps.length," action",a.steps.length!==1?"s":"",":"]}),E.jsx("div",{className:"steps-preview",children:a.steps.map((Z,W)=>E.jsxs("div",{className:"step-preview-item",children:[E.jsxs("span",{className:"step-number",children:[W+1,"."]}),E.jsx("span",{className:"step-description",children:Hy(Z)})]},Z.id))})]})}),a.stage==="error"&&E.jsxs("div",{className:"record-error-state",children:[E.jsx("h3",{children:"Recording Failed"}),E.jsx("p",{className:"record-error",children:a.error}),E.jsx("button",{className:"btn btn-secondary",onClick:()=>v(Z=>({...Z,stage:"url-input",error:null})),children:"Try Again"})]})]}),E.jsxs("div",{className:"modal-actions",children:[E.jsx("button",{className:"btn btn-secondary",onClick:pe,children:"Cancel"}),a.stage==="url-input"&&E.jsx("button",{className:"btn btn-primary",onClick:R,children:"Start Recording"}),a.stage==="recording"&&E.jsx("button",{className:"btn btn-warning",onClick:V,children:"Stop Recording"}),a.stage==="preview"&&a.steps.length>0&&E.jsxs(E.Fragment,{children:[E.jsx("button",{className:"btn btn-secondary",onClick:H,children:"Add to Current Test"}),E.jsx("button",{className:"btn btn-primary",onClick:re,children:"Create New Test"})]})]})]})}):null}function jy({isOpen:f,title:g,fields:i,onSubmit:a,onCancel:v,submitLabel:A="Create"}){const[R,V]=Re.useState({}),H=Re.useRef(null);if(Re.useEffect(()=>{if(f){const Z={};i.forEach(W=>{Z[W.name]=W.defaultValue||""}),V(Z),setTimeout(()=>{var W;return(W=H.current)==null?void 0:W.focus()},50)}},[f,i]),!f)return null;const re=Z=>{Z.preventDefault(),i.every(X=>{var te;return!X.required||((te=R[X.name])==null?void 0:te.trim())})&&a(R)},pe=Z=>{Z.key==="Escape"&&v()};return E.jsx("div",{className:"modal-overlay",onClick:v,children:E.jsxs("div",{className:"modal prompt-dialog",onClick:Z=>Z.stopPropagation(),onKeyDown:pe,children:[E.jsxs("div",{className:"modal-header",children:[E.jsx("h2",{children:g}),E.jsx("button",{className:"btn-close",onClick:v,children:"×"})]}),E.jsxs("form",{onSubmit:re,children:[E.jsx("div",{className:"modal-body",children:i.map((Z,W)=>E.jsxs("div",{className:"prompt-field",children:[E.jsx("label",{htmlFor:`prompt-${Z.name}`,children:Z.label}),E.jsx("input",{ref:W===0?H:void 0,type:"text",id:`prompt-${Z.name}`,value:R[Z.name]||"",onChange:X=>V(te=>({...te,[Z.name]:X.target.value})),placeholder:Z.placeholder,required:Z.required})]},Z.name))}),E.jsxs("div",{className:"modal-actions",children:[E.jsx("button",{type:"button",className:"btn btn-secondary",onClick:v,children:"Cancel"}),E.jsx("button",{type:"submit",className:"btn btn-primary",children:A})]})]})]})})}const Wy=150,Xy={global:"#4CAF50",file:"#2196F3",folder:"#9C27B0",test:"#FF9800",procedure:"#607D8B"};function bE({variables:f,onChange:g,title:i="Variables",emptyMessage:a="No variables defined"}){const[v,A]=Re.useState(f),R=Re.useRef(f);Re.useEffect(()=>{JSON.stringify(f)!==JSON.stringify(R.current)&&(v.some(te=>!te.name.trim())||A(f),R.current=f)},[f,v]);const V=Re.useCallback(W=>{const X=W.filter(De=>De.name.trim()!==""),te=new Set;let ve=!1;for(const De of X){const Me=De.name.trim().toLowerCase();if(te.has(Me)){ve=!0;break}te.add(Me)}ve||(R.current=X,g(X))},[g]),H=()=>{const W=[...v,{name:"",value:""}];A(W)},re=(W,X,te)=>{const ve=[...v];ve[W]={...ve[W],[X]:te},A(ve),V(ve)},pe=W=>{const X=v.filter((te,ve)=>ve!==W);A(X),V(X)},Z=Re.useMemo(()=>{const W=new Map;for(const X of v)if(X.name.trim()){const te=X.name.trim().toLowerCase();W.set(te,(W.get(te)||0)+1)}return new Set([...W.entries()].filter(([,X])=>X>1).map(([X])=>X))},[v]);return E.jsxs("div",{className:"variables-editor",children:[i&&E.jsx("div",{className:"variables-section-header",children:E.jsx("h4",{children:i})}),v.length===0?E.jsx("div",{className:"variables-empty",children:a}):v.map((W,X)=>E.jsx(zy,{variable:W,isDuplicate:Z.has(W.name.trim().toLowerCase()),onUpdate:(te,ve)=>re(X,te,ve),onDelete:()=>pe(X)},X)),E.jsx("button",{className:"add-variable-btn",onClick:H,children:"+ Add Variable"})]})}function zy({variable:f,isDuplicate:g,onUpdate:i,onDelete:a}){const[v,A]=Re.useState([]),[R,V]=Re.useState(-1),[H,re]=Re.useState(!1),pe=Re.useRef(null),Z=Re.useRef(null);Re.useEffect(()=>{f.name===""&&Z.current&&Z.current.focus()},[]);const W=Re.useCallback((Me,Pe)=>{const ut=Me.substring(0,Pe),ot=ut.lastIndexOf("${");if(ot!==-1){const $t=ut.substring(ot);if(!$t.includes("}")){const Ve=$t.substring(2),ne=B$(Ve);if(ne.length>0){A(ne),V(-1),re(!0);return}}}re(!1),A([])},[]),X=Me=>{const Pe=Me.target.value;i("value",Pe),W(Pe,Me.target.selectionStart||0)},te=Re.useCallback(Me=>{if(Me<0||Me>=v.length||!pe.current)return;const Pe=v[Me],ut=pe.current,ot=ut.selectionStart||0,$t=f.value,ne=$t.substring(0,ot).lastIndexOf("${");if(ne!==-1){const Ie=$t.substring(0,ne)+"${"+Pe.name+"}"+$t.substring(ot);i("value",Ie);const Be=ne+Pe.name.length+3;setTimeout(()=>{ut.setSelectionRange(Be,Be),ut.focus()},0)}re(!1),A([])},[v,f.value,i]),ve=Me=>{if(H)switch(Me.key){case"ArrowDown":Me.preventDefault(),V(Pe=>Pe<v.length-1?Pe+1:0);break;case"ArrowUp":Me.preventDefault(),V(Pe=>Pe>0?Pe-1:v.length-1);break;case"Enter":case"Tab":R>=0?(Me.preventDefault(),te(R)):v.length>0&&Me.key==="Tab"&&(Me.preventDefault(),te(0));break;case"Escape":Me.preventDefault(),re(!1);break}},De=()=>{setTimeout(()=>{re(!1)},Wy)};return E.jsxs("div",{className:`variable-editor-item ${g?"duplicate":""}`,children:[E.jsxs("div",{className:"var-name",children:[E.jsx("input",{ref:Z,type:"text",value:f.name,onChange:Me=>i("name",Me.target.value),onBlur:De,placeholder:"name",style:g?{borderColor:"#f44336"}:void 0}),g&&E.jsx("span",{className:"duplicate-warning",title:"Duplicate variable name",children:"⚠"})]}),E.jsxs("div",{className:"var-value",style:{position:"relative"},children:[E.jsx("input",{ref:pe,type:"text",value:f.value,onChange:X,onKeyDown:ve,onBlur:De,placeholder:"value (use ${var} for references)"}),H&&v.length>0&&E.jsx("div",{style:{position:"absolute",top:"100%",left:0,right:0,background:"white",border:"1px solid #ccc",borderRadius:"4px",boxShadow:"0 2px 8px rgba(0,0,0,0.15)",maxHeight:"150px",overflowY:"auto",zIndex:1e3},children:v.map((Me,Pe)=>E.jsxs("div",{style:{padding:"6px 10px",cursor:"pointer",display:"flex",alignItems:"center",gap:"8px",background:Pe===R?"#e3f2fd":"white",borderBottom:"1px solid #eee",fontSize:"12px"},onMouseEnter:()=>V(Pe),onMouseDown:ut=>{ut.preventDefault(),te(Pe)},children:[E.jsx("span",{style:{background:Xy[Me.scope]||"#999",color:"white",padding:"1px 5px",borderRadius:"3px",fontSize:"9px",textTransform:"uppercase"},children:Me.scope.charAt(0)}),E.jsx("span",{style:{fontFamily:"monospace",flex:1},children:"${"+Me.name+"}"})]},Me.name))})]}),E.jsx("div",{className:"var-actions",children:E.jsx("button",{className:"btn-icon delete",onClick:a,title:"Delete variable",children:"×"})})]})}function Yy(f){return f?Object.entries(f).map(([g,i])=>({name:g,value:typeof i=="string"?i:JSON.stringify(i)})):[]}function Ky(f){const g={};for(const i of f)if(i.name.trim())try{i.value.startsWith("{")||i.value.startsWith("[")||i.value==="true"||i.value==="false"||!isNaN(Number(i.value))&&i.value!==""?g[i.name]=JSON.parse(i.value):g[i.name]=i.value}catch{g[i.name]=i.value}return g}const qy="testblocks-storage",dd="handles";async function FE(){return new Promise((f,g)=>{const i=indexedDB.open(qy,1);i.onerror=()=>g(i.error),i.onsuccess=()=>f(i.result),i.onupgradeneeded=()=>{const a=i.result;a.objectStoreNames.contains(dd)||a.createObjectStore(dd)}})}async function Jy(f){const g=await FE();return new Promise((i,a)=>{const R=g.transaction(dd,"readwrite").objectStore(dd).put(f,"lastDirectory");R.onerror=()=>a(R.error),R.onsuccess=()=>i()})}async function yE(){try{const f=await FE();return new Promise((g,i)=>{const A=f.transaction(dd,"readonly").objectStore(dd).get("lastDirectory");A.onerror=()=>i(A.error),A.onsuccess=()=>g(A.result||null)})}catch{return null}}async function pf(f,g=""){const i={name:f.name,path:g||f.name,type:"folder",children:[],folderHandle:f},a=[];for await(const v of f.values())a.push({name:v.name,kind:v.kind,handle:v});a.sort((v,A)=>v.kind!==A.kind?v.kind==="directory"?-1:1:v.name.localeCompare(A.name));for(const v of a){const A=g?`${g}/${v.name}`:v.name;if(v.kind==="directory"){const R=await pf(v.handle,A);R.children&&R.children.length>0&&i.children.push(R)}else if(v.name==="_hooks.testblocks.json")try{const R=v.handle,H=await(await R.getFile()).text(),re=JSON.parse(H);i.folderHooks=re,i.hooksFileHandle=R}catch(R){console.warn(`Skipping invalid hooks file: ${v.name}`,R)}else if(v.name.endsWith(".testblocks.json")||v.name.endsWith(".json"))try{const R=v.handle,H=await(await R.getFile()).text(),re=JSON.parse(H);re.version&&re.tests&&Array.isArray(re.tests)&&i.children.push({name:v.name,path:A,type:"file",testFile:re,handle:R})}catch(R){console.warn(`Skipping invalid file: ${v.name}`,R)}}return i}async function Qy(f){try{const g=await f.getFileHandle("globals.json"),a=await(await g.getFile()).text(),v=JSON.parse(a);return console.log("[loadGlobalsFile] Loaded globals:",v),{variables:v.variables||null,handle:g,fullContent:v}}catch{return console.log("[loadGlobalsFile] No globals.json found"),{variables:null,handle:null,fullContent:null}}}function ip(f,g){if(!f||!g)return null;if(f.path===g)return f;if(f.children)for(const i of f.children){const a=ip(i,g);if(a)return a}return null}function Zy(f,g,i){const a=JSON.parse(JSON.stringify(f)),v=g.split(".");let A=a;for(let R=0;R<v.length-1;R++)v[R]in A||(A[v[R]]={}),A=A[v[R]];return A[v[v.length-1]]=i,a}const ev={version:"1.0.0",name:"Untitled Test Suite",description:"",variables:{},tests:[{id:"test-1",name:"Test Case 1",description:"",steps:[],tags:[]}]};function tv(){var Oe,We;const{toasts:f,dismissToast:g}=vy(),[i,a]=Re.useState({projectRoot:null,lastFolderName:null,selectedFilePath:null,globalVariables:null,globalsFileContent:null,testFile:ev,selectedTestIndex:0,editingFolderHooks:null,folderHooks:{version:"1.0.0"},results:[],isRunning:!1,runningTestId:null,showVariables:!1,showGlobalVariables:!1,headless:localStorage.getItem("testblocks-headless")!=="false",editorTab:"test",sidebarTab:"files",showHelpDialog:!1,showRecordDialog:!1,pluginsLoaded:!1,autoSaveStatus:"idle",resultsPanelCollapsed:!1,sidebarCollapsed:!1,failedFiles:new Set,failedTestsMap:new Map,version:null}),[v,A]=Re.useState(null),R=Re.useCallback((U,B,ue)=>{A({isOpen:!0,title:U,fields:B,onSubmit:ue})},[]),V=Re.useCallback(()=>{A(null)},[]),H=Re.useRef(null),re=Re.useRef(null),pe=Re.useRef(null),Z=Re.useRef(null),W=Re.useRef(null),X=Re.useRef(!0),te=i.testFile.tests[i.selectedTestIndex];Re.useEffect(()=>{if(X.current){X.current=!1;return}if(!(!i.selectedFilePath||!i.projectRoot))return W.current&&clearTimeout(W.current),a(U=>({...U,autoSaveStatus:"saving"})),W.current=setTimeout(async()=>{const U=ip(i.projectRoot,i.selectedFilePath);if(U!=null&&U.handle)try{const B=await U.handle.createWritable();await B.write(JSON.stringify(i.testFile,null,2)),await B.close(),U.testFile=i.testFile,console.log("[Auto-save] Saved:",i.selectedFilePath),a(ue=>({...ue,autoSaveStatus:"saved"})),setTimeout(()=>{a(ue=>ue.autoSaveStatus==="saved"?{...ue,autoSaveStatus:"idle"}:ue)},2e3)}catch(B){console.error("[Auto-save] Failed to save:",B),a(ue=>({...ue,autoSaveStatus:"idle"}))}else a(B=>({...B,autoSaveStatus:"idle"}))},1e3),()=>{W.current&&clearTimeout(W.current)}},[i.testFile,i.selectedFilePath,i.projectRoot]),Re.useEffect(()=>{if(i.editingFolderHooks){if(X.current){X.current=!1;return}return W.current&&clearTimeout(W.current),a(U=>({...U,autoSaveStatus:"saving"})),W.current=setTimeout(async()=>{var B,ue,q,Ue;const U=i.editingFolderHooks;if(!(U!=null&&U.folderHandle)){a(ge=>({...ge,autoSaveStatus:"idle"}));return}try{if(((B=i.folderHooks.beforeAll)==null?void 0:B.length)||((ue=i.folderHooks.afterAll)==null?void 0:ue.length)||((q=i.folderHooks.beforeEach)==null?void 0:q.length)||((Ue=i.folderHooks.afterEach)==null?void 0:Ue.length)){const me=U.hooksFileHandle||await U.folderHandle.getFileHandle("_hooks.testblocks.json",{create:!0}),$e=await me.createWritable();await $e.write(JSON.stringify(i.folderHooks,null,2)),await $e.close(),U.folderHooks=i.folderHooks,U.hooksFileHandle=me,console.log("[Auto-save] Saved folder hooks:",U.path)}else if(U.hooksFileHandle)try{await U.folderHandle.removeEntry("_hooks.testblocks.json"),U.folderHooks=void 0,U.hooksFileHandle=void 0,console.log("[Auto-save] Removed empty folder hooks:",U.path)}catch{}a(me=>({...me,autoSaveStatus:"saved"})),setTimeout(()=>{a(me=>me.autoSaveStatus==="saved"?{...me,autoSaveStatus:"idle"}:me)},2e3)}catch(ge){console.error("[Auto-save] Failed to save folder hooks:",ge),a(me=>({...me,autoSaveStatus:"idle"}))}},1e3),()=>{W.current&&clearTimeout(W.current)}}},[i.folderHooks,i.editingFolderHooks]);const ve=Re.useCallback(async U=>{pe.current=U;const[B,ue]=await Promise.all([pf(U),Qy(U)]);Z.current=ue.handle,a(q=>({...q,projectRoot:B,globalVariables:ue.variables,globalsFileContent:ue.fullContent,selectedFilePath:null,sidebarTab:"files"}))},[]);Re.useEffect(()=>{(async()=>{try{const B=await yE();B&&(await B.queryPermission({mode:"readwrite"})==="granted"?(await ve(B),console.log("Restored last opened folder:",B.name)):(a(q=>({...q,lastFolderName:B.name})),console.log("Last folder needs permission:",B.name)))}catch(B){console.log("Could not restore last folder:",B.message)}})()},[ve]),Re.useEffect(()=>{fetch("/api/version").then(U=>U.json()).then(U=>{U.version&&a(B=>({...B,version:U.version}))}).catch(()=>{})},[]),Re.useEffect(()=>{(async()=>{try{const B=await fetch("/api/plugins");if(!B.ok){console.warn("Failed to fetch plugins from server"),a(q=>({...q,pluginsLoaded:!0}));return}const ue=await B.json();if(ue.loaded&&Array.isArray(ue.loaded)){for(const q of ue.loaded)q.blocks&&Array.isArray(q.blocks)&&LE({name:q.name,version:q.version,description:q.description,blocks:q.blocks});console.log(`Loaded ${ue.loaded.length} plugin(s) from server`)}}catch(B){console.warn("Could not fetch plugins from server:",B.message)}a(B=>({...B,pluginsLoaded:!0}))})()},[]),Re.useEffect(()=>{Ub(i.globalVariables),i.editingFolderHooks?rE(null):rE(i.testFile.variables||null),Vb(i.editingFolderHooks?"folder":"file");const U=i.testFile.tests[i.selectedTestIndex];if(U!=null&&U.data&&U.data.length>0){const B=Object.keys(U.data[0].values||{});nE(B)}else nE([])},[i.globalVariables,i.testFile.variables,i.editingFolderHooks,i.testFile.tests,i.selectedTestIndex]);const De=Re.useCallback(async()=>{try{const U=await yE();U&&await U.requestPermission({mode:"readwrite"})==="granted"&&(await ve(U),a(ue=>({...ue,lastFolderName:null})))}catch(U){console.error("Failed to reopen folder:",U),a(B=>({...B,lastFolderName:null}))}},[ve]),Me=Re.useCallback(async()=>{var U;if("showDirectoryPicker"in window)try{const B=await window.showDirectoryPicker();await Jy(B),await ve(B),a(ue=>({...ue,lastFolderName:null}))}catch(B){B.name!=="AbortError"&&(console.error("Failed to open folder:",B),Rt.error("Failed to open folder: "+B.message))}else(U=re.current)==null||U.click()},[ve]),Pe=Re.useCallback(U=>{const B=U.target.files;if(!B||B.length===0)return;const ue={name:"Selected Folder",path:"",type:"folder",children:[]},q=new Map;q.set("",ue);const Ue=[];for(let ge=0;ge<B.length;ge++){const me=B[ge],$e=me.webkitRelativePath||me.name;if(!$e.endsWith(".testblocks.json")&&!$e.endsWith(".json"))continue;const he=$e.split("/"),tt=he.pop();let D="",h=ue;for(const j of he){const be=D?`${D}/${j}`:j;if(!q.has(be)){const ye={name:j,path:be,type:"folder",children:[]};h.children.push(ye),q.set(be,ye)}h=q.get(be),D=be}const w=$e,J=me.text().then(j=>{try{const be=JSON.parse(j);be.version&&be.tests&&Array.isArray(be.tests)&&h.children.push({name:tt,path:w,type:"file",testFile:be})}catch(be){console.warn(`Skipping invalid file: ${tt}`,be)}});Ue.push(J)}Promise.all(Ue).then(()=>{var me;const ge=$e=>{$e.children&&($e.children.sort((he,tt)=>he.type!==tt.type?he.type==="folder"?-1:1:he.name.localeCompare(tt.name)),$e.children.forEach(ge))};ge(ue),(me=B[0])!=null&&me.webkitRelativePath&&(ue.name=B[0].webkitRelativePath.split("/")[0]),a($e=>({...$e,projectRoot:ue,selectedFilePath:null,sidebarTab:"files"}))}),U.target.value=""},[]),ut=Re.useCallback(async()=>{if(pe.current)try{const U=await pf(pe.current);a(B=>({...B,projectRoot:U}))}catch(U){console.error("Failed to refresh folder:",U)}},[]),ot=Re.useCallback(U=>{U.type!=="file"||!U.testFile||(U.testFile.procedures?aE(U.testFile.procedures):uE(),X.current=!0,a(B=>({...B,selectedFilePath:U.path,testFile:U.testFile,selectedTestIndex:0,results:[],editorTab:"test",sidebarTab:"tests",editingFolderHooks:null,folderHooks:{version:"1.0.0"}})))},[]),$t=Re.useCallback(U=>{if(U.type!=="folder")return;const B=U.folderHooks||{version:"1.0.0"};a(ue=>({...ue,editingFolderHooks:U,folderHooks:B,selectedFilePath:null,editorTab:"beforeAll",sidebarTab:"files"}))},[]),Ve=Re.useCallback(U=>{if(!U.folderHandle){Rt.error("Cannot create file: folder handle not available");return}R("Create Test File",[{name:"fileName",label:"File name",defaultValue:"new-test",placeholder:"Enter file name",required:!0}],async B=>{const ue=B.fileName;if(!ue)return;const q=ue.endsWith(".testblocks.json")?ue:ue.endsWith(".json")?ue.replace(".json",".testblocks.json"):`${ue}.testblocks.json`;try{const Ue=await U.folderHandle.getFileHandle(q,{create:!0}),ge={version:"1.0.0",name:q.replace(".testblocks.json",""),description:"",variables:{},tests:[{id:`test-${Date.now()}`,name:"New Test",description:"",steps:[],tags:[]}]},me=await Ue.createWritable();await me.write(JSON.stringify(ge,null,2)),await me.close();const $e=`${U.path}/${q}`,he={name:q,path:$e,type:"file",testFile:ge,handle:Ue};a(tt=>{const D=w=>{const J={...w};return w.children&&(J.children=w.children.map(D)),J},h=tt.projectRoot?D(tt.projectRoot):null;if(h){const w=J=>{if(J.path===U.path)return J.children||(J.children=[]),J.children.push(he),J.children.sort((j,be)=>j.type!==be.type?j.type==="folder"?-1:1:j.name.localeCompare(be.name)),!0;if(J.children){for(const j of J.children)if(w(j))return!0}return!1};w(h)}return{...tt,projectRoot:h,selectedFilePath:$e,testFile:ge,selectedTestIndex:0,editorTab:"test",editingFolderHooks:null}}),Rt.success(`Created: ${q}`)}catch(Ue){console.error("Failed to create file:",Ue),Rt.error("Failed to create file")}})},[R]),ne=Re.useCallback(U=>{if(!U.folderHandle){Rt.error("Cannot create folder: folder handle not available");return}R("Create Folder",[{name:"folderName",label:"Folder name",placeholder:"Enter folder name",required:!0}],async B=>{const ue=B.folderName;if(ue)try{const q=await U.folderHandle.getDirectoryHandle(ue,{create:!0}),Ue=`${U.path}/${ue}`,ge={name:ue,path:Ue,type:"folder",children:[],folderHandle:q};a(me=>{const $e=tt=>{const D={...tt};return tt.children&&(D.children=tt.children.map($e)),D},he=me.projectRoot?$e(me.projectRoot):null;if(he){const tt=D=>{if(D.path===U.path)return D.children||(D.children=[]),D.children.push(ge),D.children.sort((h,w)=>h.type!==w.type?h.type==="folder"?-1:1:h.name.localeCompare(w.name)),!0;if(D.children){for(const h of D.children)if(tt(h))return!0}return!1};tt(he)}return{...me,projectRoot:he}}),Rt.success(`Created folder: ${ue}`)}catch(q){console.error("Failed to create folder:",q),Rt.error("Failed to create folder")}})},[R]),Ie=Re.useCallback(U=>{const B=U.type==="folder",ue=B?U.name:U.name.replace(".testblocks.json","");R(B?"Rename Folder":"Rename File",[{name:"newName",label:"New name",defaultValue:ue,placeholder:"Enter new name",required:!0}],async q=>{var tt;const Ue=(tt=q.newName)==null?void 0:tt.trim();if(!Ue||Ue===ue)return;const ge=U.path.split("/");ge.pop();const me=ge.join("/"),$e=(D,h)=>{if(!D)return null;if(D.path===h)return D;if(D.children)for(const w of D.children){const J=$e(w,h);if(J)return J}return null},he=$e(i.projectRoot,me);if(!(he!=null&&he.folderHandle)){Rt.error("Cannot rename: parent folder handle not available");return}try{if(B){const D=await he.folderHandle.getDirectoryHandle(Ue,{create:!0}),h=async(J,j)=>{for await(const be of J.values())if(be.kind==="file"){const ye=await be.getFile(),Ke=await(await j.getFileHandle(be.name,{create:!0})).createWritable();await Ke.write(await ye.arrayBuffer()),await Ke.close()}else if(be.kind==="directory"){const ye=await j.getDirectoryHandle(be.name,{create:!0});await h(be,ye)}};U.folderHandle&&await h(U.folderHandle,D),await he.folderHandle.removeEntry(U.name,{recursive:!0});const w=`${me}/${Ue}`;a(J=>{var He;const j=Ke=>{if(Ke.path===U.path){const C=(k,M,z)=>{const fe={...k,path:k.path.replace(M,z),name:k.path===U.path?Ue:k.name,folderHandle:k.path===U.path?D:k.folderHandle};return k.children&&(fe.children=k.children.map($=>C($,M,z))),fe};return C(Ke,U.path,w)}return Ke.children?{...Ke,children:Ke.children.map(j)}:Ke},be=J.projectRoot?j(J.projectRoot):null;let ye=J.selectedFilePath;return(He=J.selectedFilePath)!=null&&He.startsWith(U.path+"/")?ye=J.selectedFilePath.replace(U.path,w):J.selectedFilePath===U.path&&(ye=w),{...J,projectRoot:be,selectedFilePath:ye}}),Rt.success(`Renamed to: ${Ue}`)}else{const D=Ue.endsWith(".testblocks.json")?Ue:`${Ue}.testblocks.json`;if(!U.handle){Rt.error("Cannot rename: file handle not available");return}const w=await(await U.handle.getFile()).text(),J=await he.folderHandle.getFileHandle(D,{create:!0}),j=await J.createWritable();await j.write(w),await j.close(),await he.folderHandle.removeEntry(U.name);const be=`${me}/${D}`;a(ye=>{const He=k=>k.path===U.path?{...k,name:D,path:be,handle:J}:k.children?{...k,children:k.children.map(He)}:k,Ke=ye.projectRoot?He(ye.projectRoot):null,C=ye.selectedFilePath===U.path?be:ye.selectedFilePath;return{...ye,projectRoot:Ke,selectedFilePath:C}}),Rt.success(`Renamed to: ${D}`)}}catch(D){console.error("Failed to rename:",D),Rt.error(`Failed to rename: ${D instanceof Error?D.message:"Unknown error"}`)}})},[R,i.projectRoot]),Be=Re.useCallback(U=>{const B=U.type==="folder",ue=B?"folder":"file",q=B?U.name:U.name.replace(".testblocks.json","");if(!confirm(`Are you sure you want to delete the ${ue} "${q}"?${B?`
2193
2193
 
2194
- This will delete all files and subfolders inside it.`:""}`))return;const Ue=U.path.split("/");Ue.pop();const ge=Ue.join("/"),me=(he,tt)=>{if(!he)return null;if(he.path===tt)return he;if(he.children)for(const D of he.children){const h=me(D,tt);if(h)return h}return null},$e=me(i.projectRoot,ge);if(!($e!=null&&$e.folderHandle)){Rt.error("Cannot delete: parent folder handle not available");return}(async()=>{try{await $e.folderHandle.removeEntry(U.name,{recursive:B}),a(he=>{var j;const tt=be=>be.children?{...be,children:be.children.filter(ye=>ye.path!==U.path).map(tt)}:be,D=he.projectRoot?tt(he.projectRoot):null;let h=he.selectedFilePath,w=he.selectedTestIndex,J=he.testFile;return(he.selectedFilePath===U.path||(j=he.selectedFilePath)!=null&&j.startsWith(U.path+"/"))&&(h=null,w=0,J=null),{...he,projectRoot:D,selectedFilePath:h,selectedTestIndex:w,testFile:J}}),Rt.success(`Deleted: ${q}`)}catch(he){console.error("Failed to delete:",he),Rt.error(`Failed to delete: ${he instanceof Error?he.message:"Unknown error"}`)}})()},[i.projectRoot]),ze=Re.useCallback(async(U,B)=>{const ue=U.type==="folder",q=U.path.split("/");q.pop();const Ue=q.join("/"),ge=($e,he)=>{if(!$e)return null;if($e.path===he)return $e;if($e.children)for(const tt of $e.children){const D=ge(tt,he);if(D)return D}return null},me=ge(i.projectRoot,Ue);if(!(me!=null&&me.folderHandle)){Rt.error("Cannot move: source parent folder handle not available");return}if(!B.folderHandle){Rt.error("Cannot move: target folder handle not available");return}try{if(ue){const $e=await B.folderHandle.getDirectoryHandle(U.name,{create:!0}),he=async(D,h)=>{for await(const w of D.values())if(w.kind==="file"){const J=await w.getFile(),be=await(await h.getFileHandle(w.name,{create:!0})).createWritable();await be.write(await J.arrayBuffer()),await be.close()}else if(w.kind==="directory"){const J=await h.getDirectoryHandle(w.name,{create:!0});await he(w,J)}};U.folderHandle&&await he(U.folderHandle,$e),await me.folderHandle.removeEntry(U.name,{recursive:!0});const tt=`${B.path}/${U.name}`;a(D=>{var be;const h=ye=>ye.children?{...ye,children:ye.children.filter(He=>He.path!==U.path).map(h)}:ye,w=ye=>{if(ye.path===B.path){const He=(C,k,M)=>{const z={...C,path:C.path.replace(k,M),folderHandle:C.path===U.path?$e:C.folderHandle};return C.children&&(z.children=C.children.map(fe=>He(fe,k,M))),z},Ke=He(U,U.path,tt);return{...ye,children:[...ye.children||[],Ke].sort((C,k)=>C.type!==k.type?C.type==="folder"?-1:1:C.name.localeCompare(k.name))}}return ye.children?{...ye,children:ye.children.map(w)}:ye};let J=D.projectRoot?h(D.projectRoot):null;J=J?w(J):null;let j=D.selectedFilePath;return(be=D.selectedFilePath)!=null&&be.startsWith(U.path+"/")?j=D.selectedFilePath.replace(U.path,tt):D.selectedFilePath===U.path&&(j=tt),{...D,projectRoot:J,selectedFilePath:j}}),Rt.success(`Moved ${U.name} to ${B.name}`)}else{if(!U.handle){Rt.error("Cannot move: file handle not available");return}const he=await(await U.handle.getFile()).text(),tt=await B.folderHandle.getFileHandle(U.name,{create:!0}),D=await tt.createWritable();await D.write(he),await D.close(),await me.folderHandle.removeEntry(U.name);const h=`${B.path}/${U.name}`;a(w=>{const J=He=>He.children?{...He,children:He.children.filter(Ke=>Ke.path!==U.path).map(J)}:He,j=He=>{if(He.path===B.path){const Ke={...U,path:h,handle:tt};return{...He,children:[...He.children||[],Ke].sort((C,k)=>C.type!==k.type?C.type==="folder"?-1:1:C.name.localeCompare(k.name))}}return He.children?{...He,children:He.children.map(j)}:He};let be=w.projectRoot?J(w.projectRoot):null;be=be?j(be):null;const ye=w.selectedFilePath===U.path?h:w.selectedFilePath;return{...w,projectRoot:be,selectedFilePath:ye}}),Rt.success(`Moved ${U.name} to ${B.name}`)}}catch($e){console.error("Failed to move:",$e),Rt.error(`Failed to move: ${$e instanceof Error?$e.message:"Unknown error"}`)}},[i.projectRoot]),dt=Re.useCallback((U,B,ue,q)=>{a(Ue=>{if(Ue.editingFolderHooks){const me=Ue.editorTab;return me==="test"?Ue:{...Ue,folderHooks:{...Ue.folderHooks,[me]:U.length>0?U:void 0}}}if(Ue.editorTab!=="test")return{...Ue,testFile:{...Ue.testFile,[Ue.editorTab]:U}};const ge=[...Ue.testFile.tests];return ge[Ue.selectedTestIndex]={...ge[Ue.selectedTestIndex],steps:U,...B&&{name:B},...ue!==void 0&&{data:ue.length>0?ue:void 0},...q!==void 0&&{softAssertions:q}},{...Ue,testFile:{...Ue.testFile,tests:ge}}})},[]),it=Re.useCallback(async(U,B)=>{const{selectedMatches:ue,config:q}=U;console.log("[handleReplaceMatches] Processing",ue.length,"matches for blockType:",B);const Ue={name:q.name,description:q.description,params:q.parameters.map(ge=>({name:ge.name,type:ge.fieldType==="number"?"number":"string",default:ge.defaultValue,description:`From ${ge.blockType}.${ge.originalFieldName}`})),steps:q.steps};a(ge=>{if(!ge.projectRoot)return console.log("[handleReplaceMatches] No projectRoot, skipping"),ge;const me=new Map;for(const h of ue){const w=me.get(h.filePath)||[];w.push(h),me.set(h.filePath,w)}console.log("[handleReplaceMatches] Grouped into",me.size,"files");const $e=h=>{const w={...h};return h.testFile&&(w.testFile=JSON.parse(JSON.stringify(h.testFile))),h.children&&(w.children=h.children.map($e)),w.handle=h.handle,w},he=$e(ge.projectRoot);let tt=null;const D=(h,w,J)=>{if(h.type==="file"&&h.path===w&&h.testFile){console.log("[handleReplaceMatches] Updating file:",w,"with",J.length,"matches");const j=new Map;for(const He of J){const Ke=`${He.location}:${He.testCaseId}`,C=j.get(Ke)||[];C.push(He),j.set(Ke,C)}const be=[];for(const He of j.values())He.sort((Ke,C)=>C.startIndex-Ke.startIndex),be.push(...He);console.log("[handleReplaceMatches] Sorted matches:",be.map(He=>`${He.testCaseId}:${He.location}:${He.startIndex}-${He.endIndex}`));let ye=h.testFile;for(const He of be)console.log("[handleReplaceMatches] Applying match:",He.testCaseId,He.location,He.startIndex,"-",He.endIndex),ye=$y(ye,He,B,{});if(ye={...ye,procedures:{...ye.procedures,[q.name]:Ue}},console.log("[handleReplaceMatches] Added procedure to file:",q.name),h.testFile=ye,w===ge.selectedFilePath&&(console.log("[handleReplaceMatches] This is the current file, updating testFile state"),tt=ye),h.handle){const He=h.handle,Ke=ye;(async()=>{try{const C=await He.createWritable();await C.write(JSON.stringify(Ke,null,2)),await C.close(),console.log("[handleReplaceMatches] Saved file:",w)}catch(C){console.error("[handleReplaceMatches] Failed to save file",w,":",C)}})()}else console.log("[handleReplaceMatches] No file handle for:",w,"- file will not be saved to disk");return h}if(h.children)for(const j of h.children){const be=D(j,w,J);if(be)return be}return null};for(const[h,w]of me)D(he,h,w)||console.warn("[handleReplaceMatches] Could not find file in tree:",h);if(ge.selectedFilePath){console.log("[handleReplaceMatches] Updating current file:",ge.selectedFilePath);const h=J=>{if(J.type==="file"&&J.path===ge.selectedFilePath)return J;if(J.children)for(const j of J.children){const be=h(j);if(be)return be}return null},w=h(he);if(w&&w.testFile){const J=ge.testFile.tests[ge.selectedTestIndex],j=w.testFile.tests.map((ye,He)=>He===ge.selectedTestIndex?J:ye),be={...w.testFile,tests:j,...ge.editorTab!=="test"&&ge.editorTab!=="beforeAll"&&{beforeAll:ge.testFile.beforeAll},...ge.editorTab!=="test"&&ge.editorTab!=="afterAll"&&{afterAll:ge.testFile.afterAll},...ge.editorTab!=="test"&&ge.editorTab!=="beforeEach"&&{beforeEach:ge.testFile.beforeEach},...ge.editorTab!=="test"&&ge.editorTab!=="afterEach"&&{afterEach:ge.testFile.afterEach},procedures:{...w.testFile.procedures,[q.name]:Ue}};if(ge.editorTab==="beforeAll"?be.beforeAll=ge.testFile.beforeAll:ge.editorTab==="afterAll"?be.afterAll=ge.testFile.afterAll:ge.editorTab==="beforeEach"?be.beforeEach=ge.testFile.beforeEach:ge.editorTab==="afterEach"&&(be.afterEach=ge.testFile.afterEach),w.testFile=be,tt=be,w.handle){const ye=w.handle;(async()=>{try{const He=await ye.createWritable();await He.write(JSON.stringify(be,null,2)),await He.close(),console.log("[handleReplaceMatches] Saved current file:",ge.selectedFilePath)}catch(He){console.error("[handleReplaceMatches] Failed to save current file:",He)}})()}else console.log("[handleReplaceMatches] No file handle for current file - will not auto-save")}}return{...ge,projectRoot:he,...tt&&{testFile:tt}}})},[]),qe=Re.useCallback(()=>{if(i.editingFolderHooks){const U=i.editorTab;return i.folderHooks[U]||[]}return i.editorTab==="test"?te==null?void 0:te.steps:i.testFile[i.editorTab]||[]},[i.editorTab,i.testFile,i.editingFolderHooks,i.folderHooks,te]),we=Re.useCallback(()=>{const U=i.editingFolderHooks?"Folder ":"";switch(i.editorTab){case"beforeAll":return`${U}Before All`;case"afterAll":return`${U}After All`;case"beforeEach":return`${U}Before Each`;case"afterEach":return`${U}After Each`;default:return(te==null?void 0:te.name)||"Test"}},[i.editorTab,i.editingFolderHooks,te]),Ze=Re.useCallback(async()=>{const U=document.querySelector(".blocklySvg"),B=U==null?void 0:U.workspace,ue=uy(),q={...i.testFile,procedures:{...i.testFile.procedures,...ue},tests:i.testFile.tests.map((he,tt)=>{if(tt===i.selectedTestIndex&&B){const D=ud(B);return{...he,steps:D}}return he})},Ue=ip(i.projectRoot,i.selectedFilePath);if(Ue!=null&&Ue.handle)try{const he=await Ue.handle.createWritable();await he.write(JSON.stringify(q,null,2)),await he.close(),Ue.testFile=q,a(tt=>({...tt,testFile:q}));return}catch(he){console.error("Failed to save file:",he)}const ge=new Blob([JSON.stringify(q,null,2)],{type:"application/json"}),me=URL.createObjectURL(ge),$e=document.createElement("a");$e.href=me,$e.download=`${i.testFile.name.toLowerCase().replace(/\s+/g,"-")}.testblocks.json`,$e.click(),URL.revokeObjectURL(me)},[i.testFile,i.selectedTestIndex,i.projectRoot,i.selectedFilePath]),Le=Re.useCallback(()=>{var U;(U=H.current)==null||U.click()},[]),_e=Re.useCallback(U=>{var q;const B=(q=U.target.files)==null?void 0:q[0];if(!B)return;const ue=new FileReader;ue.onload=Ue=>{var ge;try{const me=JSON.parse((ge=Ue.target)==null?void 0:ge.result);me.variables||(me.variables={}),me.procedures?aE(me.procedures):uE(),a($e=>({...$e,testFile:me,selectedTestIndex:0,selectedFilePath:null,results:[],sidebarTab:"tests"}))}catch(me){Rt.error("Failed to load test file: "+me.message)}},ue.readAsText(B),U.target.value=""},[]),ce=Re.useCallback(()=>{const U={id:`test-${Date.now()}`,name:`Test Case ${i.testFile.tests.length+1}`,description:"",steps:[],tags:[]};a(B=>({...B,testFile:{...B.testFile,tests:[...B.testFile.tests,U]},selectedTestIndex:B.testFile.tests.length}))},[i.testFile.tests.length]),xe=Re.useCallback(U=>{if(i.testFile.tests.length<=1){Rt.warning("Cannot delete the last test case");return}a(B=>{const ue=B.testFile.tests.filter((q,Ue)=>Ue!==U);return{...B,testFile:{...B.testFile,tests:ue},selectedTestIndex:Math.min(B.selectedTestIndex,ue.length-1)}})},[i.testFile.tests.length]),se=Re.useCallback(U=>{a(B=>{const ue=[...B.testFile.tests];return ue[U]={...ue[U],disabled:!ue[U].disabled},{...B,testFile:{...B.testFile,tests:ue}}})},[]),ee=Re.useCallback(async()=>{a(U=>({...U,isRunning:!0,runningTestId:null,results:[]}));try{const U=rf(i.projectRoot,i.selectedFilePath),B=await fetch(`/api/run?headless=${i.headless}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:i.testFile,folderHooks:U.length>0?U:void 0})}),ue=await B.json();if(!B.ok||!Array.isArray(ue)){const ge=ue.message||ue.error||"Unknown error";console.error("Test run failed:",ge),a(me=>({...me,isRunning:!1})),Rt.error(`Test run failed: ${ge}`);return}const q=ue.some(ge=>ge.status!=="passed"),Ue=new Set;ue.forEach(ge=>{ge.status!=="passed"&&Ue.add(ge.testId)}),a(ge=>{const me=new Set(ge.failedFiles),$e=new Map(ge.failedTestsMap);return ge.selectedFilePath&&(q?(me.add(ge.selectedFilePath),$e.set(ge.selectedFilePath,Ue)):(me.delete(ge.selectedFilePath),$e.delete(ge.selectedFilePath))),{...ge,results:ue,isRunning:!1,failedFiles:me,failedTestsMap:$e}})}catch(U){console.error("Failed to run tests:",U),a(B=>({...B,isRunning:!1})),Rt.error("Failed to run tests. Make sure the server is running.")}},[i.testFile,i.headless,i.projectRoot,i.selectedFilePath]),ae=Re.useCallback(async(U,B)=>{B&&B.stopPropagation(),a(ue=>({...ue,isRunning:!0,runningTestId:U,results:[]}));try{const ue=rf(i.projectRoot,i.selectedFilePath),q=await fetch(`/api/run?headless=${i.headless}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:{...i.testFile,tests:i.testFile.tests.filter(ge=>ge.id===U)},folderHooks:ue.length>0?ue:void 0})}),Ue=await q.json();if(!q.ok||!Array.isArray(Ue)){const ge=Ue.message||Ue.error||"Unknown error";console.error("Test run failed:",ge),a(me=>({...me,isRunning:!1,runningTestId:null})),Rt.error(`Test run failed: ${ge}`);return}a(ge=>{const me=new Set(ge.failedFiles),$e=new Map(ge.failedTestsMap);if(ge.selectedFilePath){const he=new Set($e.get(ge.selectedFilePath)||[]);Ue.forEach(tt=>{tt.status!=="passed"?he.add(tt.testId):he.delete(tt.testId)}),he.size>0?($e.set(ge.selectedFilePath,he),me.add(ge.selectedFilePath)):($e.delete(ge.selectedFilePath),me.delete(ge.selectedFilePath))}return{...ge,results:Ue,isRunning:!1,runningTestId:null,failedFiles:me,failedTestsMap:$e}})}catch(ue){console.error("Failed to run test:",ue),a(q=>({...q,isRunning:!1,runningTestId:null})),Rt.error("Failed to run test. Make sure the server is running.")}},[i.testFile,i.headless,i.projectRoot,i.selectedFilePath]),et=Re.useCallback(async U=>{const B=$e=>{const he=[];if($e.type==="file"&&$e.testFile&&he.push($e),$e.children)for(const tt of $e.children)he.push(...B(tt));return he},ue=B(U);if(ue.length===0){Rt.warning("No test files found in this folder");return}a($e=>({...$e,isRunning:!0,runningTestId:null,results:[]})),Rt.info(`Running ${ue.length} test file(s)...`);const q=[],Ue=new Map,ge=new Map;let me=0;try{for(const tt of ue){if(!tt.testFile)continue;me++,Rt.info(`Running ${tt.name} (${me}/${ue.length})...`);const D=rf(i.projectRoot,tt.path),h=await fetch(`/api/run?headless=${i.headless}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:tt.testFile,folderHooks:D.length>0?D:void 0})}),w=await h.json();if(h.ok&&Array.isArray(w)){const J=w.map(ye=>({...ye,fileName:tt.name.replace(".testblocks.json","")}));q.push(...J);const j=new Set;w.forEach(ye=>{ye.status!=="passed"&&j.add(ye.testId)});const be=j.size>0;Ue.set(tt.path,be),be&&ge.set(tt.path,j)}else q.push({testId:`file-error-${tt.path}`,testName:`${tt.name} (Error)`,status:"error",duration:0,steps:[],error:{message:w.message||w.error||"Unknown error"},startedAt:new Date().toISOString(),finishedAt:new Date().toISOString(),fileName:tt.name.replace(".testblocks.json","")}),Ue.set(tt.path,!0);a(J=>({...J,results:[...q]}))}const $e=q.filter(tt=>tt.status==="passed").length,he=q.filter(tt=>tt.status==="failed"||tt.status==="error").length;he>0?Rt.error(`Completed: ${$e} passed, ${he} failed`):Rt.success(`All ${$e} tests passed!`),a(tt=>{const D=new Set(tt.failedFiles),h=new Map(tt.failedTestsMap);for(const[w,J]of Ue)if(J){D.add(w);const j=ge.get(w);j&&h.set(w,j)}else D.delete(w),h.delete(w);return{...tt,isRunning:!1,failedFiles:D,failedTestsMap:h}})}catch($e){console.error("Failed to run folder tests:",$e),a(he=>({...he,isRunning:!1,results:q})),Rt.error("Failed to run tests. Make sure the server is running.")}},[i.headless,i.projectRoot]),Ae=Re.useCallback(U=>{a(B=>{const ue=[...B.testFile.tests];return ue[B.selectedTestIndex]={...ue[B.selectedTestIndex],name:U},{...B,testFile:{...B.testFile,tests:ue}}})},[]);Re.useCallback(U=>{a(B=>({...B,testFile:{...B.testFile,name:U}}))},[]),Re.useCallback(()=>{R("Add Variable",[{name:"name",label:"Variable name",placeholder:"Enter variable name",required:!0},{name:"defaultValue",label:"Default value",placeholder:"Enter default value"}],U=>{const B=U.name;B&&a(ue=>({...ue,testFile:{...ue.testFile,variables:{...ue.testFile.variables,[B]:{type:"string",default:U.defaultValue||""}}}}))})},[R]),Re.useCallback((U,B)=>{a(ue=>{var q;return{...ue,testFile:{...ue.testFile,variables:{...ue.testFile.variables,[U]:{...(q=ue.testFile.variables)==null?void 0:q[U],type:"string",default:B}}}}})},[]),Re.useCallback(U=>{a(B=>{const ue={...B.testFile.variables};return delete ue[U],{...B,testFile:{...B.testFile,variables:ue}}})},[]),Re.useCallback((U,B)=>{a(ue=>{if(!ue.globalVariables)return ue;const q=Zy(ue.globalVariables,U,B),Ue={...ue.globalsFileContent,variables:q};return Z.current&&(async()=>{try{const ge=await Z.current.createWritable();await ge.write(JSON.stringify(Ue,null,2)),await ge.close(),console.log("[handleUpdateGlobalVariable] Saved globals.json")}catch(ge){console.error("[handleUpdateGlobalVariable] Failed to save globals.json:",ge)}})(),{...ue,globalVariables:q,globalsFileContent:Ue}})},[]);const lt=Re.useCallback(U=>{const B=Ky(U);a(ue=>{const q={...ue.globalsFileContent,variables:B};return Z.current&&(async()=>{try{const Ue=await Z.current.createWritable();await Ue.write(JSON.stringify(q,null,2)),await Ue.close(),console.log("[handleGlobalVariablesChange] Saved globals.json")}catch(Ue){console.error("[handleGlobalVariablesChange] Failed to save globals.json:",Ue)}})(),{...ue,globalVariables:B,globalsFileContent:q}})},[]),Ye=Re.useCallback(U=>{a(B=>{const ue={};for(const q of U)if(q.name.trim()){let Ue=q.value;try{(q.value.startsWith("{")||q.value.startsWith("[")||q.value==="true"||q.value==="false"||!isNaN(Number(q.value))&&q.value!=="")&&(Ue=JSON.parse(q.value))}catch{}ue[q.name]={default:Ue}}return{...B,testFile:{...B.testFile,variables:ue}}})},[]),gt=Re.useCallback((U,B)=>{if(B==="append")a(ue=>{const q=[...ue.testFile.tests],Ue=Array.isArray(q[ue.selectedTestIndex].steps)?q[ue.selectedTestIndex].steps:[];q[ue.selectedTestIndex]={...q[ue.selectedTestIndex],steps:[...Ue,...U]};const ge={...ue.testFile,tests:q},me=ip(ue.projectRoot,ue.selectedFilePath);return me!=null&&me.handle&&(async()=>{try{const $e=await me.handle.createWritable();await $e.write(JSON.stringify(ge,null,2)),await $e.close(),console.log("[handleStepsRecorded] Saved recorded steps to:",ue.selectedFilePath)}catch($e){console.error("[handleStepsRecorded] Failed to save:",$e)}})(),{...ue,testFile:ge,showRecordDialog:!1}});else{const q=`recorded-${new Date().toISOString().replace(/[:.]/g,"-").slice(0,19)}.testblocks.json`,Ue={id:`test-${Date.now()}`,name:"Recorded Test",description:"Test recorded from browser actions",steps:U,tags:["recorded"]},ge={version:"1.0.0",name:"Recorded Test Suite",description:`Recorded on ${new Date().toLocaleString()}`,variables:{},tests:[Ue]};a(me=>{var tt;let $e=null,he="";if(me.selectedFilePath&&me.projectRoot){const D=me.selectedFilePath.split("/");D.pop();const h=D.join("/");if(h){const w=ip(me.projectRoot,h);w!=null&&w.folderHandle&&($e=w.folderHandle,he=h)}}return!$e&&((tt=me.projectRoot)!=null&&tt.folderHandle)&&($e=me.projectRoot.folderHandle,he=me.projectRoot.path),$e?(async()=>{try{const D=await $e.getFileHandle(q,{create:!0}),h=await D.createWritable();await h.write(JSON.stringify(ge,null,2)),await h.close(),console.log("[handleStepsRecorded] Created new file:",q);const w=he?`${he}/${q}`:q,J={name:q,path:w,type:"file",testFile:ge,handle:D},j=ye=>{const He={...ye};return ye.children&&(He.children=ye.children.map(j)),He},be=me.projectRoot?j(me.projectRoot):null;if(be){const ye=He=>{if(He.path===he||He.path===He.name&&he===He.name)return He.children||(He.children=[]),He.children.push(J),He.children.sort((Ke,C)=>Ke.type!==C.type?Ke.type==="folder"?-1:1:Ke.name.localeCompare(C.name)),!0;if(He.children){for(const Ke of He.children)if(ye(Ke))return!0}return!1};ye(be),a(He=>({...He,projectRoot:be,selectedFilePath:w,testFile:ge,selectedTestIndex:0,editorTab:"test"}))}Rt.success(`Created new test file: ${q}`)}catch(D){console.error("[handleStepsRecorded] Failed to create file:",D),Rt.error("Failed to create test file")}})():Rt.warning("No project folder open. Please open a folder first."),{...me,showRecordDialog:!1}})}},[]),nt=U=>i.results.find(B=>B.testId===U),Ot=Re.useCallback(async()=>{if(i.results.length!==0)try{const U=await fetch("/api/reports/html",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:i.testFile,results:i.results})});if(!U.ok)throw new Error("Failed to generate report");const B=await U.blob(),ue=U.headers.get("Content-Disposition"),q=ue==null?void 0:ue.match(/filename="(.+)"/),Ue=q?q[1]:"report.html",ge=URL.createObjectURL(B),me=document.createElement("a");me.href=ge,me.download=Ue,me.click(),URL.revokeObjectURL(ge)}catch(U){console.error("Failed to download HTML report:",U),Rt.error("Failed to download HTML report")}},[i.testFile,i.results]),Je=Re.useCallback(async()=>{if(i.results.length!==0)try{const U=await fetch("/api/reports/junit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:i.testFile,results:i.results})});if(!U.ok)throw new Error("Failed to generate report");const B=await U.blob(),ue=U.headers.get("Content-Disposition"),q=ue==null?void 0:ue.match(/filename="(.+)"/),Ue=q?q[1]:"junit.xml",ge=URL.createObjectURL(B),me=document.createElement("a");me.href=ge,me.download=Ue,me.click(),URL.revokeObjectURL(ge)}catch(U){console.error("Failed to download JUnit report:",U),Rt.error("Failed to download JUnit report")}},[i.testFile,i.results]),Ut=Re.useCallback((U,B,ue)=>{if(B==="global"){const q={...i.globalVariables||{},[ue]:U},Ue={...i.globalsFileContent,variables:q};Z.current&&(async()=>{try{const ge=await Z.current.createWritable();await ge.write(JSON.stringify(Ue,null,2)),await ge.close(),console.log("[handleCreateVariable] Saved globals.json")}catch(ge){console.error("[handleCreateVariable] Failed to save globals.json:",ge)}})(),a(ge=>({...ge,globalVariables:q,globalsFileContent:Ue})),Rt.success(`Created global variable: ${ue}`)}else a(q=>({...q,testFile:{...q.testFile,variables:{...q.testFile.variables,[ue]:{default:U}}}})),Rt.success(`Created file variable: ${ue}`)},[i.globalVariables,i.globalsFileContent]);return E.jsxs("div",{className:"app",children:[E.jsxs("header",{className:"header",children:[E.jsxs("h1",{children:[E.jsx("span",{children:"TestBlocks"}),i.version&&E.jsxs("span",{className:"header-version",children:["v",i.version]}),(i.selectedFilePath||i.editingFolderHooks)&&E.jsxs("span",{className:"header-file-path",children:[i.editingFolderHooks?E.jsxs(E.Fragment,{children:[E.jsx("span",{className:"folder-hooks-label",children:"Folder Hooks:"})," ",i.editingFolderHooks.path]}):i.selectedFilePath,i.autoSaveStatus==="saving"&&E.jsx("span",{className:"auto-save-indicator saving",children:"Saving..."}),i.autoSaveStatus==="saved"&&E.jsx("span",{className:"auto-save-indicator saved",children:"Saved"})]})]}),E.jsxs("div",{className:"header-actions",children:[E.jsx("button",{className:"btn btn-secondary",onClick:()=>a(U=>({...U,showHelpDialog:!0})),title:"Help",children:"Help"}),E.jsx("button",{className:"btn btn-secondary",onClick:Me,children:"Open Folder"}),E.jsx("button",{className:"btn btn-secondary",onClick:Le,children:"Open File"}),E.jsx("button",{className:"btn btn-secondary",onClick:Ze,children:"Save"}),E.jsx("button",{className:"btn btn-secondary",onClick:()=>a(U=>({...U,showRecordDialog:!0})),title:"Record browser actions",children:"Record"}),((Oe=i.globalsFileContent)==null?void 0:Oe.testIdAttribute)&&E.jsxs("span",{className:"test-id-indicator",title:"Test ID Attribute (from globals.json)",children:["TestID: ",E.jsx("code",{children:i.globalsFileContent.testIdAttribute})]}),E.jsxs("label",{className:"headless-toggle",children:[E.jsx("input",{type:"checkbox",checked:i.headless,onChange:U=>{localStorage.setItem("testblocks-headless",String(U.target.checked)),a(B=>({...B,headless:U.target.checked}))}}),E.jsx("span",{children:"Headless"})]}),E.jsx("button",{className:"btn btn-primary",onClick:ee,disabled:i.isRunning,children:i.isRunning&&!i.runningTestId?"Running...":"Run All Tests"})]})]}),E.jsx("input",{ref:H,type:"file",accept:".json,.testblocks.json",style:{display:"none"},onChange:_e}),E.jsx("input",{ref:re,type:"file",accept:".json,.testblocks.json",style:{display:"none"},onChange:Pe,webkitdirectory:"",directory:"",multiple:!0}),E.jsxs("main",{className:"main-content",children:[E.jsxs("aside",{className:`sidebar${i.sidebarCollapsed?" collapsed":""}`,children:[E.jsx("div",{className:"sidebar-toggle-header",children:E.jsx("button",{className:"panel-toggle-btn",onClick:()=>{a(U=>({...U,sidebarCollapsed:!U.sidebarCollapsed})),setTimeout(()=>window.dispatchEvent(new Event("resize")),250)},title:i.sidebarCollapsed?"Expand panel":"Collapse panel",children:i.sidebarCollapsed?"▶":"◀"})}),!i.sidebarCollapsed&&E.jsxs(E.Fragment,{children:[E.jsxs("div",{className:"sidebar-tabs",children:[E.jsx("button",{className:`sidebar-tab ${i.sidebarTab==="files"?"active":""}`,onClick:()=>a(U=>({...U,sidebarTab:"files"})),children:"Files"}),E.jsx("button",{className:`sidebar-tab ${i.sidebarTab==="tests"?"active":""}`,onClick:()=>a(U=>({...U,sidebarTab:"tests"})),children:"Tests"})]}),i.sidebarTab==="files"?E.jsxs("div",{className:"sidebar-section file-tree-section",children:[!i.projectRoot&&i.lastFolderName&&E.jsxs("div",{className:"reopen-folder-prompt",children:[E.jsxs("span",{children:["Last folder: ",E.jsx("strong",{children:i.lastFolderName})]}),E.jsx("button",{className:"btn btn-small btn-primary",onClick:De,children:"Reopen"})]}),E.jsx(Uy,{root:i.projectRoot,selectedPath:i.selectedFilePath,onSelectFile:ot,onSelectFolder:$t,onRefresh:i.projectRoot?ut:void 0,onCreateFile:Ve,onCreateFolder:ne,onRename:Ie,onDelete:Be,onMove:ze,onRunFolder:et,isRunning:i.isRunning,failedFiles:i.failedFiles})]}):E.jsxs(E.Fragment,{children:[E.jsxs("div",{className:"sidebar-section",children:[E.jsx("div",{className:"sidebar-header clickable",onClick:()=>a(U=>({...U,showGlobalVariables:!U.showGlobalVariables})),children:E.jsxs("h2",{children:[E.jsx("span",{style:{marginRight:"8px"},children:i.showGlobalVariables?"▼":"▶"}),"Global Variables",E.jsx("span",{className:"global-badge",title:"From globals.json",children:"🌐"})]})}),i.showGlobalVariables&&E.jsx("div",{className:"variables-list global-variables",style:{padding:"8px"},children:E.jsx(bE,{variables:Yy(i.globalVariables),onChange:lt,title:"",emptyMessage:"No global variables. Add variables here to use across all test files."})})]}),E.jsxs("div",{className:"sidebar-section",children:[E.jsx("div",{className:"sidebar-header clickable",onClick:()=>a(U=>({...U,showVariables:!U.showVariables})),children:E.jsxs("h2",{children:[E.jsx("span",{style:{marginRight:"8px"},children:i.showVariables?"▼":"▶"}),"File Variables"]})}),i.showVariables&&E.jsx("div",{className:"variables-list",style:{padding:"8px"},children:E.jsx(bE,{variables:Object.entries(i.testFile.variables||{}).map(([U,B])=>({name:U,value:typeof B.default=="string"?B.default:JSON.stringify(B.default)})),onChange:Ye,title:"",emptyMessage:"No file variables. Add variables specific to this test file."})})]}),E.jsxs("div",{className:"sidebar-section",children:[E.jsx("div",{className:"sidebar-header",children:E.jsx("h2",{children:"Test Cases"})}),E.jsx("div",{className:"test-list",children:i.testFile.tests.map((U,B)=>{var ge;const ue=nt(U.id),q=i.runningTestId===U.id,Ue=U.disabled===!0;return E.jsxs("div",{className:`test-item ${B===i.selectedTestIndex?"active":""} ${(ue==null?void 0:ue.status)||""} ${Ue?"disabled":""}`,onClick:()=>a(me=>({...me,selectedTestIndex:B,editorTab:"test"})),children:[E.jsxs("div",{className:"test-item-content",children:[E.jsxs("div",{className:"test-item-name",children:[Ue?E.jsx("span",{className:"status-dot skipped",title:"Test is disabled"}):ue?E.jsx("span",{className:`status-dot ${ue.status}`}):i.selectedFilePath&&((ge=i.failedTestsMap.get(i.selectedFilePath))!=null&&ge.has(U.id))?E.jsx("span",{className:"status-dot failed",title:"Failed in previous run"}):null,E.jsx("span",{className:Ue?"test-name-disabled":"",children:U.name}),U.data&&U.data.length>0&&E.jsxs("span",{className:"data-driven-badge",title:`Data-driven: ${U.data.length} iterations`,children:["×",U.data.length]})]}),E.jsxs("div",{className:"test-item-steps",children:[Array.isArray(U.steps)?U.steps.length:0," steps"]})]}),E.jsx("button",{className:"btn-run-test",onClick:me=>ae(U.id,me),disabled:i.isRunning||Ue,title:Ue?"Test is disabled":"Run this test",children:q?"...":"▶"})]},U.id)})}),E.jsx("button",{className:"add-test-btn",onClick:ce,children:"+ Add Test Case"})]})]})]})]}),E.jsxs("div",{className:"editor-area",children:[E.jsxs("div",{className:"editor-tabs",children:[E.jsxs("button",{className:`editor-tab ${i.editorTab==="beforeAll"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"beforeAll"})),children:["Before All",i.editingFolderHooks?i.folderHooks.beforeAll&&i.folderHooks.beforeAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.beforeAll.length}):i.testFile.beforeAll&&i.testFile.beforeAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.beforeAll.length})]}),E.jsxs("button",{className:`editor-tab ${i.editorTab==="beforeEach"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"beforeEach"})),children:["Before Each",i.editingFolderHooks?i.folderHooks.beforeEach&&i.folderHooks.beforeEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.beforeEach.length}):i.testFile.beforeEach&&i.testFile.beforeEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.beforeEach.length})]}),!i.editingFolderHooks&&E.jsx("button",{className:`editor-tab test-tab ${i.editorTab==="test"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"test"})),children:"Test Steps"}),E.jsxs("button",{className:`editor-tab ${i.editorTab==="afterEach"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"afterEach"})),children:["After Each",i.editingFolderHooks?i.folderHooks.afterEach&&i.folderHooks.afterEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.afterEach.length}):i.testFile.afterEach&&i.testFile.afterEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.afterEach.length})]}),E.jsxs("button",{className:`editor-tab ${i.editorTab==="afterAll"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"afterAll"})),children:["After All",i.editingFolderHooks?i.folderHooks.afterAll&&i.folderHooks.afterAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.afterAll.length}):i.testFile.afterAll&&i.testFile.afterAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.afterAll.length})]})]}),E.jsx("div",{className:"editor-toolbar",children:i.editorTab==="test"&&!i.editingFolderHooks?E.jsxs(E.Fragment,{children:[E.jsx("input",{type:"text",className:"test-name-input",value:(te==null?void 0:te.name)||"",onChange:U=>Ae(U.target.value),placeholder:"Test name"}),E.jsx("button",{className:"btn btn-success",onClick:()=>ae(te==null?void 0:te.id),disabled:i.isRunning||(te==null?void 0:te.disabled),style:{padding:"6px 12px",fontSize:"12px",marginRight:"8px"},title:te!=null&&te.disabled?"Test is disabled":"Run this test",children:i.runningTestId===(te==null?void 0:te.id)?"Running...":"Run Test"}),E.jsx("button",{className:`btn ${te!=null&&te.disabled?"btn-success":"btn-warning"}`,onClick:()=>se(i.selectedTestIndex),style:{padding:"6px 12px",fontSize:"12px",marginRight:"8px"},title:te!=null&&te.disabled?"Enable this test":"Disable this test",children:te!=null&&te.disabled?"Enable":"Disable"}),E.jsx("button",{className:"btn btn-danger",onClick:()=>xe(i.selectedTestIndex),style:{padding:"6px 12px",fontSize:"12px"},children:"Delete"})]}):E.jsxs("div",{className:"lifecycle-toolbar-info",children:[E.jsx("span",{className:"lifecycle-icon",children:i.editingFolderHooks?"📁":"⚡"}),E.jsx("span",{children:we()}),E.jsx("span",{className:"lifecycle-hint",children:i.editingFolderHooks?E.jsxs(E.Fragment,{children:["— Applies to all tests in this folder",i.editorTab==="beforeAll"||i.editorTab==="afterAll"?"":" and subfolders"]}):E.jsxs(E.Fragment,{children:[i.editorTab==="beforeAll"&&"— Runs once before all tests",i.editorTab==="afterAll"&&"— Runs once after all tests",i.editorTab==="beforeEach"&&"— Runs before each test",i.editorTab==="afterEach"&&"— Runs after each test"]})})]})}),E.jsx("div",{className:"blockly-container",children:E.jsx(xy,{onWorkspaceChange:dt,onReplaceMatches:it,onCreateVariable:Ut,initialSteps:qe(),testName:i.editorTab==="test"?te==null?void 0:te.name:we(),lifecycleType:i.editorTab!=="test"?i.editorTab:void 0,testData:i.editorTab==="test"?te==null?void 0:te.data:void 0,softAssertions:i.editorTab==="test"?te==null?void 0:te.softAssertions:void 0,projectRoot:i.projectRoot,currentFilePath:i.selectedFilePath||void 0},`${i.editorTab}-${i.editorTab==="test"?te==null?void 0:te.id:"lifecycle"}-${i.selectedFilePath||((We=i.editingFolderHooks)==null?void 0:We.path)}-${i.pluginsLoaded}`)})]}),E.jsxs("aside",{className:`results-panel${i.resultsPanelCollapsed?" collapsed":""}`,children:[E.jsxs("div",{className:"results-header",children:[E.jsx("button",{className:"panel-toggle-btn",onClick:()=>{a(U=>({...U,resultsPanelCollapsed:!U.resultsPanelCollapsed})),setTimeout(()=>window.dispatchEvent(new Event("resize")),250)},title:i.resultsPanelCollapsed?"Expand panel":"Collapse panel",children:i.resultsPanelCollapsed?"◀":"▶"}),!i.resultsPanelCollapsed&&E.jsxs(E.Fragment,{children:[E.jsxs("h2",{children:["Results",i.results.length>0&&E.jsxs("span",{className:"results-summary",children:[E.jsxs("span",{className:"passed-count",children:[i.results.filter(U=>U.status==="passed").length," passed"]}),i.results.filter(U=>U.status==="failed"||U.status==="error").length>0&&E.jsxs("span",{className:"failed-count",children:[i.results.filter(U=>U.status==="failed"||U.status==="error").length," failed"]})]})]}),i.results.length>0&&E.jsxs("div",{className:"results-actions",children:[E.jsx("button",{className:"btn-report",onClick:Ot,title:"Download HTML Report",children:"HTML"}),E.jsx("button",{className:"btn-report",onClick:Je,title:"Download JUnit XML Report",children:"xUnit"})]})]})]}),!i.resultsPanelCollapsed&&E.jsx("div",{className:"results-content",children:i.results.length===0?E.jsxs("div",{className:"empty-state",children:[E.jsx("h3",{children:"No results yet"}),E.jsx("p",{children:"Run your tests to see results here"})]}):i.results.map(U=>E.jsxs("div",{className:`result-item ${U.status}${U.isLifecycle?" lifecycle":""}`,children:[E.jsxs("div",{className:"result-test-header",children:[E.jsx("span",{className:`status-indicator ${U.status}`}),U.isLifecycle&&E.jsx("span",{className:"lifecycle-badge",children:U.lifecycleType}),U.fileName&&E.jsxs("span",{className:"result-file-name",children:[U.fileName," /"]}),E.jsx("span",{className:"result-test-name",children:U.testName}),E.jsxs("span",{className:"result-duration",children:[U.duration,"ms"]})]}),U.error&&E.jsx("div",{className:"result-error",children:U.error.message}),U.steps&&U.steps.length>0&&E.jsx("div",{className:"result-steps",children:U.steps.map((B,ue)=>E.jsx(Fy,{step:B},B.stepId||ue))})]},U.testId))})]})]}),E.jsx(Vy,{isOpen:i.showHelpDialog,onClose:()=>a(U=>({...U,showHelpDialog:!1}))}),E.jsx(Gy,{isOpen:i.showRecordDialog,onClose:()=>a(U=>({...U,showRecordDialog:!1})),onStepsRecorded:gt}),v&&E.jsx(jy,{isOpen:v.isOpen,title:v.title,fields:v.fields,onSubmit:U=>{v.onSubmit(U),V()},onCancel:V}),E.jsx(yy,{toasts:f,onDismiss:g})]})}function rf(f,g){if(!f||!g)return[];function i(a,v,A){const R=a.folderHooks?[...A,a.folderHooks]:A;if(a.children)for(const V of a.children){if(V.type==="file"&&V.path===v)return R;if(V.type==="folder"){const H=i(V,v,R);if(H!==null)return H}}return null}return i(f,g,[])||[]}xT.createRoot(document.getElementById("root")).render(E.jsx(CT.StrictMode,{children:E.jsx(tv,{})}));
2195
- //# sourceMappingURL=index-CVn_B7zc.js.map
2194
+ This will delete all files and subfolders inside it.`:""}`))return;const Ue=U.path.split("/");Ue.pop();const ge=Ue.join("/"),me=(he,tt)=>{if(!he)return null;if(he.path===tt)return he;if(he.children)for(const D of he.children){const h=me(D,tt);if(h)return h}return null},$e=me(i.projectRoot,ge);if(!($e!=null&&$e.folderHandle)){Rt.error("Cannot delete: parent folder handle not available");return}(async()=>{try{await $e.folderHandle.removeEntry(U.name,{recursive:B}),a(he=>{var j;const tt=be=>be.children?{...be,children:be.children.filter(ye=>ye.path!==U.path).map(tt)}:be,D=he.projectRoot?tt(he.projectRoot):null;let h=he.selectedFilePath,w=he.selectedTestIndex,J=he.testFile;return(he.selectedFilePath===U.path||(j=he.selectedFilePath)!=null&&j.startsWith(U.path+"/"))&&(h=null,w=0,J=null),{...he,projectRoot:D,selectedFilePath:h,selectedTestIndex:w,testFile:J}}),Rt.success(`Deleted: ${q}`)}catch(he){console.error("Failed to delete:",he),Rt.error(`Failed to delete: ${he instanceof Error?he.message:"Unknown error"}`)}})()},[i.projectRoot]),ze=Re.useCallback(async(U,B)=>{const ue=U.type==="folder",q=U.path.split("/");q.pop();const Ue=q.join("/"),ge=($e,he)=>{if(!$e)return null;if($e.path===he)return $e;if($e.children)for(const tt of $e.children){const D=ge(tt,he);if(D)return D}return null},me=ge(i.projectRoot,Ue);if(!(me!=null&&me.folderHandle)){Rt.error("Cannot move: source parent folder handle not available");return}if(!B.folderHandle){Rt.error("Cannot move: target folder handle not available");return}try{if(ue){const $e=await B.folderHandle.getDirectoryHandle(U.name,{create:!0}),he=async(D,h)=>{for await(const w of D.values())if(w.kind==="file"){const J=await w.getFile(),be=await(await h.getFileHandle(w.name,{create:!0})).createWritable();await be.write(await J.arrayBuffer()),await be.close()}else if(w.kind==="directory"){const J=await h.getDirectoryHandle(w.name,{create:!0});await he(w,J)}};U.folderHandle&&await he(U.folderHandle,$e),await me.folderHandle.removeEntry(U.name,{recursive:!0});const tt=`${B.path}/${U.name}`;a(D=>{var be;const h=ye=>ye.children?{...ye,children:ye.children.filter(He=>He.path!==U.path).map(h)}:ye,w=ye=>{if(ye.path===B.path){const He=(C,k,M)=>{const z={...C,path:C.path.replace(k,M),folderHandle:C.path===U.path?$e:C.folderHandle};return C.children&&(z.children=C.children.map(fe=>He(fe,k,M))),z},Ke=He(U,U.path,tt);return{...ye,children:[...ye.children||[],Ke].sort((C,k)=>C.type!==k.type?C.type==="folder"?-1:1:C.name.localeCompare(k.name))}}return ye.children?{...ye,children:ye.children.map(w)}:ye};let J=D.projectRoot?h(D.projectRoot):null;J=J?w(J):null;let j=D.selectedFilePath;return(be=D.selectedFilePath)!=null&&be.startsWith(U.path+"/")?j=D.selectedFilePath.replace(U.path,tt):D.selectedFilePath===U.path&&(j=tt),{...D,projectRoot:J,selectedFilePath:j}}),Rt.success(`Moved ${U.name} to ${B.name}`)}else{if(!U.handle){Rt.error("Cannot move: file handle not available");return}const he=await(await U.handle.getFile()).text(),tt=await B.folderHandle.getFileHandle(U.name,{create:!0}),D=await tt.createWritable();await D.write(he),await D.close(),await me.folderHandle.removeEntry(U.name);const h=`${B.path}/${U.name}`;a(w=>{const J=He=>He.children?{...He,children:He.children.filter(Ke=>Ke.path!==U.path).map(J)}:He,j=He=>{if(He.path===B.path){const Ke={...U,path:h,handle:tt};return{...He,children:[...He.children||[],Ke].sort((C,k)=>C.type!==k.type?C.type==="folder"?-1:1:C.name.localeCompare(k.name))}}return He.children?{...He,children:He.children.map(j)}:He};let be=w.projectRoot?J(w.projectRoot):null;be=be?j(be):null;const ye=w.selectedFilePath===U.path?h:w.selectedFilePath;return{...w,projectRoot:be,selectedFilePath:ye}}),Rt.success(`Moved ${U.name} to ${B.name}`)}}catch($e){console.error("Failed to move:",$e),Rt.error(`Failed to move: ${$e instanceof Error?$e.message:"Unknown error"}`)}},[i.projectRoot]),dt=Re.useCallback((U,B,ue,q)=>{a(Ue=>{if(Ue.editingFolderHooks){const me=Ue.editorTab;return me==="test"?Ue:{...Ue,folderHooks:{...Ue.folderHooks,[me]:U.length>0?U:void 0}}}if(Ue.editorTab!=="test")return{...Ue,testFile:{...Ue.testFile,[Ue.editorTab]:U}};const ge=[...Ue.testFile.tests];return ge[Ue.selectedTestIndex]={...ge[Ue.selectedTestIndex],steps:U,...B&&{name:B},...ue!==void 0&&{data:ue.length>0?ue:void 0},...q!==void 0&&{softAssertions:q}},{...Ue,testFile:{...Ue.testFile,tests:ge}}})},[]),it=Re.useCallback(async(U,B)=>{const{selectedMatches:ue,config:q}=U;console.log("[handleReplaceMatches] Processing",ue.length,"matches for blockType:",B);const Ue={name:q.name,description:q.description,params:q.parameters.map(ge=>({name:ge.name,type:ge.fieldType==="number"?"number":"string",default:ge.defaultValue,description:`From ${ge.blockType}.${ge.originalFieldName}`})),steps:q.steps};a(ge=>{if(!ge.projectRoot)return console.log("[handleReplaceMatches] No projectRoot, skipping"),ge;const me=new Map;for(const h of ue){const w=me.get(h.filePath)||[];w.push(h),me.set(h.filePath,w)}console.log("[handleReplaceMatches] Grouped into",me.size,"files");const $e=h=>{const w={...h};return h.testFile&&(w.testFile=JSON.parse(JSON.stringify(h.testFile))),h.children&&(w.children=h.children.map($e)),w.handle=h.handle,w},he=$e(ge.projectRoot);let tt=null;const D=(h,w,J)=>{if(h.type==="file"&&h.path===w&&h.testFile){console.log("[handleReplaceMatches] Updating file:",w,"with",J.length,"matches");const j=new Map;for(const He of J){const Ke=`${He.location}:${He.testCaseId}`,C=j.get(Ke)||[];C.push(He),j.set(Ke,C)}const be=[];for(const He of j.values())He.sort((Ke,C)=>C.startIndex-Ke.startIndex),be.push(...He);console.log("[handleReplaceMatches] Sorted matches:",be.map(He=>`${He.testCaseId}:${He.location}:${He.startIndex}-${He.endIndex}`));let ye=h.testFile;for(const He of be)console.log("[handleReplaceMatches] Applying match:",He.testCaseId,He.location,He.startIndex,"-",He.endIndex),ye=$y(ye,He,B,{});if(ye={...ye,procedures:{...ye.procedures,[q.name]:Ue}},console.log("[handleReplaceMatches] Added procedure to file:",q.name),h.testFile=ye,w===ge.selectedFilePath&&(console.log("[handleReplaceMatches] This is the current file, updating testFile state"),tt=ye),h.handle){const He=h.handle,Ke=ye;(async()=>{try{const C=await He.createWritable();await C.write(JSON.stringify(Ke,null,2)),await C.close(),console.log("[handleReplaceMatches] Saved file:",w)}catch(C){console.error("[handleReplaceMatches] Failed to save file",w,":",C)}})()}else console.log("[handleReplaceMatches] No file handle for:",w,"- file will not be saved to disk");return h}if(h.children)for(const j of h.children){const be=D(j,w,J);if(be)return be}return null};for(const[h,w]of me)D(he,h,w)||console.warn("[handleReplaceMatches] Could not find file in tree:",h);if(ge.selectedFilePath){console.log("[handleReplaceMatches] Updating current file:",ge.selectedFilePath);const h=J=>{if(J.type==="file"&&J.path===ge.selectedFilePath)return J;if(J.children)for(const j of J.children){const be=h(j);if(be)return be}return null},w=h(he);if(w&&w.testFile){const J=ge.testFile.tests[ge.selectedTestIndex],j=w.testFile.tests.map((ye,He)=>He===ge.selectedTestIndex?J:ye),be={...w.testFile,tests:j,...ge.editorTab!=="test"&&ge.editorTab!=="beforeAll"&&{beforeAll:ge.testFile.beforeAll},...ge.editorTab!=="test"&&ge.editorTab!=="afterAll"&&{afterAll:ge.testFile.afterAll},...ge.editorTab!=="test"&&ge.editorTab!=="beforeEach"&&{beforeEach:ge.testFile.beforeEach},...ge.editorTab!=="test"&&ge.editorTab!=="afterEach"&&{afterEach:ge.testFile.afterEach},procedures:{...w.testFile.procedures,[q.name]:Ue}};if(ge.editorTab==="beforeAll"?be.beforeAll=ge.testFile.beforeAll:ge.editorTab==="afterAll"?be.afterAll=ge.testFile.afterAll:ge.editorTab==="beforeEach"?be.beforeEach=ge.testFile.beforeEach:ge.editorTab==="afterEach"&&(be.afterEach=ge.testFile.afterEach),w.testFile=be,tt=be,w.handle){const ye=w.handle;(async()=>{try{const He=await ye.createWritable();await He.write(JSON.stringify(be,null,2)),await He.close(),console.log("[handleReplaceMatches] Saved current file:",ge.selectedFilePath)}catch(He){console.error("[handleReplaceMatches] Failed to save current file:",He)}})()}else console.log("[handleReplaceMatches] No file handle for current file - will not auto-save")}}return{...ge,projectRoot:he,...tt&&{testFile:tt}}})},[]),qe=Re.useCallback(()=>{if(i.editingFolderHooks){const U=i.editorTab;return i.folderHooks[U]||[]}return i.editorTab==="test"?te==null?void 0:te.steps:i.testFile[i.editorTab]||[]},[i.editorTab,i.testFile,i.editingFolderHooks,i.folderHooks,te]),we=Re.useCallback(()=>{const U=i.editingFolderHooks?"Folder ":"";switch(i.editorTab){case"beforeAll":return`${U}Before All`;case"afterAll":return`${U}After All`;case"beforeEach":return`${U}Before Each`;case"afterEach":return`${U}After Each`;default:return(te==null?void 0:te.name)||"Test"}},[i.editorTab,i.editingFolderHooks,te]),Ze=Re.useCallback(async()=>{const U=document.querySelector(".blocklySvg"),B=U==null?void 0:U.workspace,ue=uy(),q={...i.testFile,procedures:{...i.testFile.procedures,...ue},tests:i.testFile.tests.map((he,tt)=>{if(tt===i.selectedTestIndex&&B){const D=ud(B);return{...he,steps:D}}return he})},Ue=ip(i.projectRoot,i.selectedFilePath);if(Ue!=null&&Ue.handle)try{const he=await Ue.handle.createWritable();await he.write(JSON.stringify(q,null,2)),await he.close(),Ue.testFile=q,a(tt=>({...tt,testFile:q}));return}catch(he){console.error("Failed to save file:",he)}const ge=new Blob([JSON.stringify(q,null,2)],{type:"application/json"}),me=URL.createObjectURL(ge),$e=document.createElement("a");$e.href=me,$e.download=`${i.testFile.name.toLowerCase().replace(/\s+/g,"-")}.testblocks.json`,$e.click(),URL.revokeObjectURL(me)},[i.testFile,i.selectedTestIndex,i.projectRoot,i.selectedFilePath]),Le=Re.useCallback(()=>{var U;(U=H.current)==null||U.click()},[]),_e=Re.useCallback(U=>{var q;const B=(q=U.target.files)==null?void 0:q[0];if(!B)return;const ue=new FileReader;ue.onload=Ue=>{var ge;try{const me=JSON.parse((ge=Ue.target)==null?void 0:ge.result);me.variables||(me.variables={}),me.procedures?aE(me.procedures):uE(),a($e=>({...$e,testFile:me,selectedTestIndex:0,selectedFilePath:null,results:[],sidebarTab:"tests"}))}catch(me){Rt.error("Failed to load test file: "+me.message)}},ue.readAsText(B),U.target.value=""},[]),ce=Re.useCallback(()=>{const U={id:`test-${Date.now()}`,name:`Test Case ${i.testFile.tests.length+1}`,description:"",steps:[],tags:[]};a(B=>({...B,testFile:{...B.testFile,tests:[...B.testFile.tests,U]},selectedTestIndex:B.testFile.tests.length}))},[i.testFile.tests.length]),xe=Re.useCallback(U=>{if(i.testFile.tests.length<=1){Rt.warning("Cannot delete the last test case");return}a(B=>{const ue=B.testFile.tests.filter((q,Ue)=>Ue!==U);return{...B,testFile:{...B.testFile,tests:ue},selectedTestIndex:Math.min(B.selectedTestIndex,ue.length-1)}})},[i.testFile.tests.length]),se=Re.useCallback(U=>{a(B=>{const ue=[...B.testFile.tests];return ue[U]={...ue[U],disabled:!ue[U].disabled},{...B,testFile:{...B.testFile,tests:ue}}})},[]),ee=Re.useCallback(async()=>{a(U=>({...U,isRunning:!0,runningTestId:null,results:[]}));try{const U=rf(i.projectRoot,i.selectedFilePath),B=await fetch(`/api/run?headless=${i.headless}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:i.testFile,folderHooks:U.length>0?U:void 0})}),ue=await B.json();if(!B.ok||!Array.isArray(ue)){const ge=ue.message||ue.error||"Unknown error";console.error("Test run failed:",ge),a(me=>({...me,isRunning:!1})),Rt.error(`Test run failed: ${ge}`);return}const q=ue.some(ge=>ge.status!=="passed"),Ue=new Set;ue.forEach(ge=>{ge.status!=="passed"&&Ue.add(ge.testId)}),a(ge=>{const me=new Set(ge.failedFiles),$e=new Map(ge.failedTestsMap);return ge.selectedFilePath&&(q?(me.add(ge.selectedFilePath),$e.set(ge.selectedFilePath,Ue)):(me.delete(ge.selectedFilePath),$e.delete(ge.selectedFilePath))),{...ge,results:ue,isRunning:!1,failedFiles:me,failedTestsMap:$e}})}catch(U){console.error("Failed to run tests:",U),a(B=>({...B,isRunning:!1})),Rt.error("Failed to run tests. Make sure the server is running.")}},[i.testFile,i.headless,i.projectRoot,i.selectedFilePath]),ae=Re.useCallback(async(U,B)=>{B&&B.stopPropagation(),a(ue=>({...ue,isRunning:!0,runningTestId:U,results:[]}));try{const ue=rf(i.projectRoot,i.selectedFilePath),q=await fetch(`/api/run?headless=${i.headless}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:{...i.testFile,tests:i.testFile.tests.filter(ge=>ge.id===U)},folderHooks:ue.length>0?ue:void 0})}),Ue=await q.json();if(!q.ok||!Array.isArray(Ue)){const ge=Ue.message||Ue.error||"Unknown error";console.error("Test run failed:",ge),a(me=>({...me,isRunning:!1,runningTestId:null})),Rt.error(`Test run failed: ${ge}`);return}a(ge=>{const me=new Set(ge.failedFiles),$e=new Map(ge.failedTestsMap);if(ge.selectedFilePath){const he=new Set($e.get(ge.selectedFilePath)||[]);Ue.forEach(tt=>{tt.status!=="passed"?he.add(tt.testId):he.delete(tt.testId)}),he.size>0?($e.set(ge.selectedFilePath,he),me.add(ge.selectedFilePath)):($e.delete(ge.selectedFilePath),me.delete(ge.selectedFilePath))}return{...ge,results:Ue,isRunning:!1,runningTestId:null,failedFiles:me,failedTestsMap:$e}})}catch(ue){console.error("Failed to run test:",ue),a(q=>({...q,isRunning:!1,runningTestId:null})),Rt.error("Failed to run test. Make sure the server is running.")}},[i.testFile,i.headless,i.projectRoot,i.selectedFilePath]),et=Re.useCallback(async U=>{const B=$e=>{const he=[];if($e.type==="file"&&$e.testFile&&he.push($e),$e.children)for(const tt of $e.children)he.push(...B(tt));return he},ue=B(U);if(ue.length===0){Rt.warning("No test files found in this folder");return}a($e=>({...$e,isRunning:!0,runningTestId:null,results:[]})),Rt.info(`Running ${ue.length} test file(s)...`);const q=[],Ue=new Map,ge=new Map;let me=0;try{for(const tt of ue){if(!tt.testFile)continue;me++,Rt.info(`Running ${tt.name} (${me}/${ue.length})...`);const D=rf(i.projectRoot,tt.path),h=await fetch(`/api/run?headless=${i.headless}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:tt.testFile,folderHooks:D.length>0?D:void 0})}),w=await h.json();if(h.ok&&Array.isArray(w)){const J=w.map(ye=>({...ye,fileName:tt.name.replace(".testblocks.json","")}));q.push(...J);const j=new Set;w.forEach(ye=>{ye.status!=="passed"&&j.add(ye.testId)});const be=j.size>0;Ue.set(tt.path,be),be&&ge.set(tt.path,j)}else q.push({testId:`file-error-${tt.path}`,testName:`${tt.name} (Error)`,status:"error",duration:0,steps:[],error:{message:w.message||w.error||"Unknown error"},startedAt:new Date().toISOString(),finishedAt:new Date().toISOString(),fileName:tt.name.replace(".testblocks.json","")}),Ue.set(tt.path,!0);a(J=>({...J,results:[...q]}))}const $e=q.filter(tt=>tt.status==="passed").length,he=q.filter(tt=>tt.status==="failed"||tt.status==="error").length;he>0?Rt.error(`Completed: ${$e} passed, ${he} failed`):Rt.success(`All ${$e} tests passed!`),a(tt=>{const D=new Set(tt.failedFiles),h=new Map(tt.failedTestsMap);for(const[w,J]of Ue)if(J){D.add(w);const j=ge.get(w);j&&h.set(w,j)}else D.delete(w),h.delete(w);return{...tt,isRunning:!1,failedFiles:D,failedTestsMap:h}})}catch($e){console.error("Failed to run folder tests:",$e),a(he=>({...he,isRunning:!1,results:q})),Rt.error("Failed to run tests. Make sure the server is running.")}},[i.headless,i.projectRoot]),Ae=Re.useCallback(U=>{a(B=>{const ue=[...B.testFile.tests];return ue[B.selectedTestIndex]={...ue[B.selectedTestIndex],name:U},{...B,testFile:{...B.testFile,tests:ue}}})},[]);Re.useCallback(U=>{a(B=>({...B,testFile:{...B.testFile,name:U}}))},[]),Re.useCallback(()=>{R("Add Variable",[{name:"name",label:"Variable name",placeholder:"Enter variable name",required:!0},{name:"defaultValue",label:"Default value",placeholder:"Enter default value"}],U=>{const B=U.name;B&&a(ue=>({...ue,testFile:{...ue.testFile,variables:{...ue.testFile.variables,[B]:{type:"string",default:U.defaultValue||""}}}}))})},[R]),Re.useCallback((U,B)=>{a(ue=>{var q;return{...ue,testFile:{...ue.testFile,variables:{...ue.testFile.variables,[U]:{...(q=ue.testFile.variables)==null?void 0:q[U],type:"string",default:B}}}}})},[]),Re.useCallback(U=>{a(B=>{const ue={...B.testFile.variables};return delete ue[U],{...B,testFile:{...B.testFile,variables:ue}}})},[]),Re.useCallback((U,B)=>{a(ue=>{if(!ue.globalVariables)return ue;const q=Zy(ue.globalVariables,U,B),Ue={...ue.globalsFileContent,variables:q};return Z.current&&(async()=>{try{const ge=await Z.current.createWritable();await ge.write(JSON.stringify(Ue,null,2)),await ge.close(),console.log("[handleUpdateGlobalVariable] Saved globals.json")}catch(ge){console.error("[handleUpdateGlobalVariable] Failed to save globals.json:",ge)}})(),{...ue,globalVariables:q,globalsFileContent:Ue}})},[]);const lt=Re.useCallback(U=>{const B=Ky(U);a(ue=>{const q={...ue.globalsFileContent,variables:B};return Z.current&&(async()=>{try{const Ue=await Z.current.createWritable();await Ue.write(JSON.stringify(q,null,2)),await Ue.close(),console.log("[handleGlobalVariablesChange] Saved globals.json")}catch(Ue){console.error("[handleGlobalVariablesChange] Failed to save globals.json:",Ue)}})(),{...ue,globalVariables:B,globalsFileContent:q}})},[]),Ye=Re.useCallback(U=>{a(B=>{const ue={};for(const q of U)if(q.name.trim()){let Ue=q.value;try{(q.value.startsWith("{")||q.value.startsWith("[")||q.value==="true"||q.value==="false"||!isNaN(Number(q.value))&&q.value!=="")&&(Ue=JSON.parse(q.value))}catch{}ue[q.name]={default:Ue}}return{...B,testFile:{...B.testFile,variables:ue}}})},[]),gt=Re.useCallback((U,B)=>{if(B==="append")a(ue=>{const q=[...ue.testFile.tests],Ue=Array.isArray(q[ue.selectedTestIndex].steps)?q[ue.selectedTestIndex].steps:[];q[ue.selectedTestIndex]={...q[ue.selectedTestIndex],steps:[...Ue,...U]};const ge={...ue.testFile,tests:q},me=ip(ue.projectRoot,ue.selectedFilePath);return me!=null&&me.handle&&(async()=>{try{const $e=await me.handle.createWritable();await $e.write(JSON.stringify(ge,null,2)),await $e.close(),console.log("[handleStepsRecorded] Saved recorded steps to:",ue.selectedFilePath)}catch($e){console.error("[handleStepsRecorded] Failed to save:",$e)}})(),{...ue,testFile:ge,showRecordDialog:!1}});else{const q=`recorded-${new Date().toISOString().replace(/[:.]/g,"-").slice(0,19)}.testblocks.json`,Ue={id:`test-${Date.now()}`,name:"Recorded Test",description:"Test recorded from browser actions",steps:U,tags:["recorded"]},ge={version:"1.0.0",name:"Recorded Test Suite",description:`Recorded on ${new Date().toLocaleString()}`,variables:{},tests:[Ue]};a(me=>{var tt;let $e=null,he="";if(me.selectedFilePath&&me.projectRoot){const D=me.selectedFilePath.split("/");D.pop();const h=D.join("/");if(h){const w=ip(me.projectRoot,h);w!=null&&w.folderHandle&&($e=w.folderHandle,he=h)}}return!$e&&((tt=me.projectRoot)!=null&&tt.folderHandle)&&($e=me.projectRoot.folderHandle,he=me.projectRoot.path),$e?(async()=>{try{const D=await $e.getFileHandle(q,{create:!0}),h=await D.createWritable();await h.write(JSON.stringify(ge,null,2)),await h.close(),console.log("[handleStepsRecorded] Created new file:",q);const w=he?`${he}/${q}`:q,J={name:q,path:w,type:"file",testFile:ge,handle:D},j=ye=>{const He={...ye};return ye.children&&(He.children=ye.children.map(j)),He},be=me.projectRoot?j(me.projectRoot):null;if(be){const ye=He=>{if(He.path===he||He.path===He.name&&he===He.name)return He.children||(He.children=[]),He.children.push(J),He.children.sort((Ke,C)=>Ke.type!==C.type?Ke.type==="folder"?-1:1:Ke.name.localeCompare(C.name)),!0;if(He.children){for(const Ke of He.children)if(ye(Ke))return!0}return!1};ye(be),a(He=>({...He,projectRoot:be,selectedFilePath:w,testFile:ge,selectedTestIndex:0,editorTab:"test"}))}Rt.success(`Created new test file: ${q}`)}catch(D){console.error("[handleStepsRecorded] Failed to create file:",D),Rt.error("Failed to create test file")}})():Rt.warning("No project folder open. Please open a folder first."),{...me,showRecordDialog:!1}})}},[]),nt=U=>i.results.find(B=>B.testId===U),Ot=Re.useCallback(async()=>{if(i.results.length!==0)try{const U=await fetch("/api/reports/html",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:i.testFile,results:i.results})});if(!U.ok)throw new Error("Failed to generate report");const B=await U.blob(),ue=U.headers.get("Content-Disposition"),q=ue==null?void 0:ue.match(/filename="(.+)"/),Ue=q?q[1]:"report.html",ge=URL.createObjectURL(B),me=document.createElement("a");me.href=ge,me.download=Ue,me.click(),URL.revokeObjectURL(ge)}catch(U){console.error("Failed to download HTML report:",U),Rt.error("Failed to download HTML report")}},[i.testFile,i.results]),Je=Re.useCallback(async()=>{if(i.results.length!==0)try{const U=await fetch("/api/reports/junit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({testFile:i.testFile,results:i.results})});if(!U.ok)throw new Error("Failed to generate report");const B=await U.blob(),ue=U.headers.get("Content-Disposition"),q=ue==null?void 0:ue.match(/filename="(.+)"/),Ue=q?q[1]:"junit.xml",ge=URL.createObjectURL(B),me=document.createElement("a");me.href=ge,me.download=Ue,me.click(),URL.revokeObjectURL(ge)}catch(U){console.error("Failed to download JUnit report:",U),Rt.error("Failed to download JUnit report")}},[i.testFile,i.results]),Ut=Re.useCallback((U,B,ue)=>{if(B==="global"){const q={...i.globalVariables||{},[ue]:U},Ue={...i.globalsFileContent,variables:q};Z.current&&(async()=>{try{const ge=await Z.current.createWritable();await ge.write(JSON.stringify(Ue,null,2)),await ge.close(),console.log("[handleCreateVariable] Saved globals.json")}catch(ge){console.error("[handleCreateVariable] Failed to save globals.json:",ge)}})(),a(ge=>({...ge,globalVariables:q,globalsFileContent:Ue})),Rt.success(`Created global variable: ${ue}`)}else a(q=>({...q,testFile:{...q.testFile,variables:{...q.testFile.variables,[ue]:{default:U}}}})),Rt.success(`Created file variable: ${ue}`)},[i.globalVariables,i.globalsFileContent]);return E.jsxs("div",{className:"app",children:[E.jsxs("header",{className:"header",children:[E.jsxs("h1",{children:[E.jsx("span",{children:"TestBlocks"}),i.version&&E.jsxs("span",{className:"header-version",children:["v",i.version]}),(i.selectedFilePath||i.editingFolderHooks)&&E.jsxs("span",{className:"header-file-path",children:[i.editingFolderHooks?E.jsxs(E.Fragment,{children:[E.jsx("span",{className:"folder-hooks-label",children:"Folder Hooks:"})," ",i.editingFolderHooks.path]}):i.selectedFilePath,i.autoSaveStatus==="saving"&&E.jsx("span",{className:"auto-save-indicator saving",children:"Saving..."}),i.autoSaveStatus==="saved"&&E.jsx("span",{className:"auto-save-indicator saved",children:"Saved"})]})]}),E.jsxs("div",{className:"header-actions",children:[E.jsx("button",{className:"btn btn-secondary",onClick:()=>a(U=>({...U,showHelpDialog:!0})),title:"Help",children:"Help"}),E.jsx("button",{className:"btn btn-secondary",onClick:Me,children:"Open Folder"}),E.jsx("button",{className:"btn btn-secondary",onClick:Le,children:"Open File"}),E.jsx("button",{className:"btn btn-secondary",onClick:Ze,children:"Save"}),E.jsx("button",{className:"btn btn-secondary",onClick:()=>a(U=>({...U,showRecordDialog:!0})),title:"Record browser actions",children:"Record"}),((Oe=i.globalsFileContent)==null?void 0:Oe.testIdAttribute)&&E.jsxs("span",{className:"test-id-indicator",title:"Test ID Attribute (from globals.json)",children:["TestID: ",E.jsx("code",{children:i.globalsFileContent.testIdAttribute})]}),E.jsxs("label",{className:"headless-toggle",children:[E.jsx("input",{type:"checkbox",checked:i.headless,onChange:U=>{localStorage.setItem("testblocks-headless",String(U.target.checked)),a(B=>({...B,headless:U.target.checked}))}}),E.jsx("span",{children:"Headless"})]}),E.jsx("button",{className:"btn btn-primary",onClick:ee,disabled:i.isRunning,children:i.isRunning&&!i.runningTestId?"Running...":"Run All Tests"})]})]}),E.jsx("input",{ref:H,type:"file",accept:".json,.testblocks.json",style:{display:"none"},onChange:_e}),E.jsx("input",{ref:re,type:"file",accept:".json,.testblocks.json",style:{display:"none"},onChange:Pe,webkitdirectory:"",directory:"",multiple:!0}),E.jsxs("main",{className:"main-content",children:[E.jsxs("aside",{className:`sidebar${i.sidebarCollapsed?" collapsed":""}`,children:[E.jsx("div",{className:"sidebar-toggle-header",children:E.jsx("button",{className:"panel-toggle-btn",onClick:()=>{a(U=>({...U,sidebarCollapsed:!U.sidebarCollapsed})),setTimeout(()=>window.dispatchEvent(new Event("resize")),250)},title:i.sidebarCollapsed?"Expand panel":"Collapse panel",children:i.sidebarCollapsed?"▶":"◀"})}),!i.sidebarCollapsed&&E.jsxs(E.Fragment,{children:[E.jsxs("div",{className:"sidebar-tabs",children:[E.jsx("button",{className:`sidebar-tab ${i.sidebarTab==="files"?"active":""}`,onClick:()=>a(U=>({...U,sidebarTab:"files"})),children:"Files"}),E.jsx("button",{className:`sidebar-tab ${i.sidebarTab==="tests"?"active":""}`,onClick:()=>a(U=>({...U,sidebarTab:"tests"})),children:"Tests"})]}),i.sidebarTab==="files"?E.jsxs("div",{className:"sidebar-section file-tree-section",children:[!i.projectRoot&&i.lastFolderName&&E.jsxs("div",{className:"reopen-folder-prompt",children:[E.jsxs("span",{children:["Last folder: ",E.jsx("strong",{children:i.lastFolderName})]}),E.jsx("button",{className:"btn btn-small btn-primary",onClick:De,children:"Reopen"})]}),E.jsx(Uy,{root:i.projectRoot,selectedPath:i.selectedFilePath,onSelectFile:ot,onSelectFolder:$t,onRefresh:i.projectRoot?ut:void 0,onCreateFile:Ve,onCreateFolder:ne,onRename:Ie,onDelete:Be,onMove:ze,onRunFolder:et,isRunning:i.isRunning,failedFiles:i.failedFiles})]}):E.jsxs(E.Fragment,{children:[E.jsxs("div",{className:"sidebar-section",children:[E.jsx("div",{className:"sidebar-header clickable",onClick:()=>a(U=>({...U,showGlobalVariables:!U.showGlobalVariables})),children:E.jsxs("h2",{children:[E.jsx("span",{style:{marginRight:"8px"},children:i.showGlobalVariables?"▼":"▶"}),"Global Variables",E.jsx("span",{className:"global-badge",title:"From globals.json",children:"🌐"})]})}),i.showGlobalVariables&&E.jsx("div",{className:"variables-list global-variables",style:{padding:"8px"},children:E.jsx(bE,{variables:Yy(i.globalVariables),onChange:lt,title:"",emptyMessage:"No global variables. Add variables here to use across all test files."})})]}),E.jsxs("div",{className:"sidebar-section",children:[E.jsx("div",{className:"sidebar-header clickable",onClick:()=>a(U=>({...U,showVariables:!U.showVariables})),children:E.jsxs("h2",{children:[E.jsx("span",{style:{marginRight:"8px"},children:i.showVariables?"▼":"▶"}),"File Variables"]})}),i.showVariables&&E.jsx("div",{className:"variables-list",style:{padding:"8px"},children:E.jsx(bE,{variables:Object.entries(i.testFile.variables||{}).map(([U,B])=>({name:U,value:typeof B.default=="string"?B.default:JSON.stringify(B.default)})),onChange:Ye,title:"",emptyMessage:"No file variables. Add variables specific to this test file."})})]}),E.jsxs("div",{className:"sidebar-section",children:[E.jsx("div",{className:"sidebar-header",children:E.jsx("h2",{children:"Test Cases"})}),E.jsx("div",{className:"test-list",children:i.testFile.tests.map((U,B)=>{var ge;const ue=nt(U.id),q=i.runningTestId===U.id,Ue=U.disabled===!0;return E.jsxs("div",{className:`test-item ${B===i.selectedTestIndex?"active":""} ${(ue==null?void 0:ue.status)||""} ${Ue?"disabled":""}`,onClick:()=>a(me=>({...me,selectedTestIndex:B,editorTab:"test"})),children:[E.jsxs("div",{className:"test-item-content",children:[E.jsxs("div",{className:"test-item-name",children:[Ue?E.jsx("span",{className:"status-dot skipped",title:"Test is disabled"}):ue?E.jsx("span",{className:`status-dot ${ue.status}`}):i.selectedFilePath&&((ge=i.failedTestsMap.get(i.selectedFilePath))!=null&&ge.has(U.id))?E.jsx("span",{className:"status-dot failed",title:"Failed in previous run"}):null,E.jsx("span",{className:Ue?"test-name-disabled":"",children:U.name}),U.data&&U.data.length>0&&E.jsxs("span",{className:"data-driven-badge",title:`Data-driven: ${U.data.length} iterations`,children:["×",U.data.length]})]}),E.jsxs("div",{className:"test-item-steps",children:[Array.isArray(U.steps)?U.steps.length:0," steps"]})]}),E.jsx("button",{className:"btn-run-test",onClick:me=>ae(U.id,me),disabled:i.isRunning||Ue,title:Ue?"Test is disabled":"Run this test",children:q?"...":"▶"})]},U.id)})}),E.jsx("button",{className:"add-test-btn",onClick:ce,children:"+ Add Test Case"})]})]})]})]}),E.jsxs("div",{className:"editor-area",children:[E.jsxs("div",{className:"editor-tabs",children:[E.jsxs("button",{className:`editor-tab ${i.editorTab==="beforeAll"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"beforeAll"})),children:["Before All",i.editingFolderHooks?i.folderHooks.beforeAll&&i.folderHooks.beforeAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.beforeAll.length}):i.testFile.beforeAll&&i.testFile.beforeAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.beforeAll.length})]}),E.jsxs("button",{className:`editor-tab ${i.editorTab==="beforeEach"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"beforeEach"})),children:["Before Each",i.editingFolderHooks?i.folderHooks.beforeEach&&i.folderHooks.beforeEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.beforeEach.length}):i.testFile.beforeEach&&i.testFile.beforeEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.beforeEach.length})]}),!i.editingFolderHooks&&E.jsx("button",{className:`editor-tab test-tab ${i.editorTab==="test"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"test"})),children:"Test Steps"}),E.jsxs("button",{className:`editor-tab ${i.editorTab==="afterEach"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"afterEach"})),children:["After Each",i.editingFolderHooks?i.folderHooks.afterEach&&i.folderHooks.afterEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.afterEach.length}):i.testFile.afterEach&&i.testFile.afterEach.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.afterEach.length})]}),E.jsxs("button",{className:`editor-tab ${i.editorTab==="afterAll"?"active":""}`,onClick:()=>a(U=>({...U,editorTab:"afterAll"})),children:["After All",i.editingFolderHooks?i.folderHooks.afterAll&&i.folderHooks.afterAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.folderHooks.afterAll.length}):i.testFile.afterAll&&i.testFile.afterAll.length>0&&E.jsx("span",{className:"tab-badge",children:i.testFile.afterAll.length})]})]}),E.jsx("div",{className:"editor-toolbar",children:i.editorTab==="test"&&!i.editingFolderHooks?E.jsxs(E.Fragment,{children:[E.jsx("input",{type:"text",className:"test-name-input",value:(te==null?void 0:te.name)||"",onChange:U=>Ae(U.target.value),placeholder:"Test name"}),E.jsx("button",{className:"btn btn-success",onClick:()=>ae(te==null?void 0:te.id),disabled:i.isRunning||(te==null?void 0:te.disabled),style:{padding:"6px 12px",fontSize:"12px",marginRight:"8px"},title:te!=null&&te.disabled?"Test is disabled":"Run this test",children:i.runningTestId===(te==null?void 0:te.id)?"Running...":"Run Test"}),E.jsx("button",{className:`btn ${te!=null&&te.disabled?"btn-success":"btn-warning"}`,onClick:()=>se(i.selectedTestIndex),style:{padding:"6px 12px",fontSize:"12px",marginRight:"8px"},title:te!=null&&te.disabled?"Enable this test":"Disable this test",children:te!=null&&te.disabled?"Enable":"Disable"}),E.jsx("button",{className:"btn btn-danger",onClick:()=>xe(i.selectedTestIndex),style:{padding:"6px 12px",fontSize:"12px"},children:"Delete"})]}):E.jsxs("div",{className:"lifecycle-toolbar-info",children:[E.jsx("span",{className:"lifecycle-icon",children:i.editingFolderHooks?"📁":"⚡"}),E.jsx("span",{children:we()}),E.jsx("span",{className:"lifecycle-hint",children:i.editingFolderHooks?E.jsxs(E.Fragment,{children:["— Applies to all tests in this folder",i.editorTab==="beforeAll"||i.editorTab==="afterAll"?"":" and subfolders"]}):E.jsxs(E.Fragment,{children:[i.editorTab==="beforeAll"&&"— Runs once before all tests",i.editorTab==="afterAll"&&"— Runs once after all tests",i.editorTab==="beforeEach"&&"— Runs before each test",i.editorTab==="afterEach"&&"— Runs after each test"]})})]})}),E.jsx("div",{className:"blockly-container",children:E.jsx(xy,{onWorkspaceChange:dt,onReplaceMatches:it,onCreateVariable:Ut,initialSteps:qe(),testName:i.editorTab==="test"?te==null?void 0:te.name:we(),lifecycleType:i.editorTab!=="test"?i.editorTab:void 0,testData:i.editorTab==="test"?te==null?void 0:te.data:void 0,softAssertions:i.editorTab==="test"?te==null?void 0:te.softAssertions:void 0,projectRoot:i.projectRoot,currentFilePath:i.selectedFilePath||void 0},`${i.editorTab}-${i.editorTab==="test"?te==null?void 0:te.id:"lifecycle"}-${i.selectedFilePath||((We=i.editingFolderHooks)==null?void 0:We.path)}-${i.pluginsLoaded}`)})]}),E.jsxs("aside",{className:`results-panel${i.resultsPanelCollapsed?" collapsed":""}`,children:[E.jsxs("div",{className:"results-header",children:[E.jsx("button",{className:"panel-toggle-btn",onClick:()=>{a(U=>({...U,resultsPanelCollapsed:!U.resultsPanelCollapsed})),setTimeout(()=>window.dispatchEvent(new Event("resize")),250)},title:i.resultsPanelCollapsed?"Expand panel":"Collapse panel",children:i.resultsPanelCollapsed?"◀":"▶"}),!i.resultsPanelCollapsed&&E.jsxs(E.Fragment,{children:[E.jsx("h2",{children:"Results"}),i.results.length>0&&E.jsxs("div",{className:"results-summary",children:[E.jsxs("span",{className:"passed-count",children:[i.results.filter(U=>U.status==="passed").length," passed"]}),i.results.filter(U=>U.status==="failed"||U.status==="error").length>0&&E.jsxs("span",{className:"failed-count",children:[i.results.filter(U=>U.status==="failed"||U.status==="error").length," failed"]}),i.results.filter(U=>U.status==="skipped").length>0&&E.jsxs("span",{className:"skipped-count",children:[i.results.filter(U=>U.status==="skipped").length," skipped"]})]}),i.results.length>0&&E.jsxs("div",{className:"results-actions",children:[E.jsx("button",{className:"btn-report",onClick:Ot,title:"Download HTML Report",children:"HTML"}),E.jsx("button",{className:"btn-report",onClick:Je,title:"Download JUnit XML Report",children:"xUnit"})]})]})]}),!i.resultsPanelCollapsed&&E.jsx("div",{className:"results-content",children:i.results.length===0?E.jsxs("div",{className:"empty-state",children:[E.jsx("h3",{children:"No results yet"}),E.jsx("p",{children:"Run your tests to see results here"})]}):i.results.map(U=>E.jsxs("div",{className:`result-item ${U.status}${U.isLifecycle?" lifecycle":""}`,children:[E.jsxs("div",{className:"result-test-header",children:[E.jsx("span",{className:`status-indicator ${U.status}`}),U.isLifecycle&&E.jsx("span",{className:"lifecycle-badge",children:U.lifecycleType}),U.fileName&&E.jsxs("span",{className:"result-file-name",children:[U.fileName," /"]}),E.jsx("span",{className:"result-test-name",children:U.testName}),E.jsxs("span",{className:"result-duration",children:[U.duration,"ms"]})]}),U.error&&E.jsx("div",{className:"result-error",children:U.error.message}),U.steps&&U.steps.length>0&&E.jsx("div",{className:"result-steps",children:U.steps.map((B,ue)=>E.jsx(Fy,{step:B},B.stepId||ue))})]},U.testId))})]})]}),E.jsx(Vy,{isOpen:i.showHelpDialog,onClose:()=>a(U=>({...U,showHelpDialog:!1}))}),E.jsx(Gy,{isOpen:i.showRecordDialog,onClose:()=>a(U=>({...U,showRecordDialog:!1})),onStepsRecorded:gt}),v&&E.jsx(jy,{isOpen:v.isOpen,title:v.title,fields:v.fields,onSubmit:U=>{v.onSubmit(U),V()},onCancel:V}),E.jsx(yy,{toasts:f,onDismiss:g})]})}function rf(f,g){if(!f||!g)return[];function i(a,v,A){const R=a.folderHooks?[...A,a.folderHooks]:A;if(a.children)for(const V of a.children){if(V.type==="file"&&V.path===v)return R;if(V.type==="folder"){const H=i(V,v,R);if(H!==null)return H}}return null}return i(f,g,[])||[]}xT.createRoot(document.getElementById("root")).render(E.jsx(CT.StrictMode,{children:E.jsx(tv,{})}));
2195
+ //# sourceMappingURL=index-BLBBQ6Rn.js.map