skopix 2.0.87 → 2.0.89

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.
@@ -3885,6 +3885,9 @@ async function matchStepsToLibrary(suitesDir, steps) {
3885
3885
  const matched = steps.map(step => {
3886
3886
  const sSel = (step.stableSelector || step.selector || '').toLowerCase().trim();
3887
3887
  if (!sSel) return step;
3888
+ // Never replace steps that already have stable selectors
3889
+ const isStable = /pi-test-identifier|aria-label|data-testid|\[title=|has-text\(/.test(sSel);
3890
+ if (isStable) return step;
3888
3891
 
3889
3892
  const match = library.find(lib => {
3890
3893
  const lSel = (lib.stableSelector || lib.selector || '').toLowerCase().trim();
@@ -3981,8 +3984,21 @@ async function syncSelectorsInternal(suitesDir, fromSelectors, toSelector, toNam
3981
3984
  (sel.match(/\.[a-z][a-z0-9_-]+|\[[\w-]+=["'][^"']+["']\]|#[a-z][a-z0-9_-]+/g) || []).filter(t => t.length > 5)
3982
3985
  );
3983
3986
 
3987
+ function isStableSel(sel) {
3988
+ if (!sel) return false;
3989
+ if (/pi-test-identifier/.test(sel)) return true;
3990
+ if (/aria-label/.test(sel)) return true;
3991
+ if (/data-testid/.test(sel)) return true;
3992
+ if (/\[title=/.test(sel)) return true;
3993
+ if (/has-text\(/.test(sel)) return true;
3994
+ if (/^#[a-z][a-z0-9_-]+$/i.test(sel)) return true;
3995
+ return false;
3996
+ }
3997
+
3984
3998
  function matchesSel(sSel) {
3985
3999
  if (!sSel) return false;
4000
+ // Never touch steps that already have stable selectors — prevents false matches
4001
+ if (isStableSel(sSel)) return false;
3986
4002
  const sLower = sSel.toLowerCase();
3987
4003
  if (fromLower.includes(sLower)) return true;
3988
4004
  if (allFromTokens.some(tok => sLower.includes(tok))) return true;
package/core/llm.js CHANGED
@@ -552,25 +552,25 @@ export async function processRecording({ steps, testName, url, provider, apiKey,
552
552
  + JSON.stringify(stepsContext, null, 2)
553
553
  + setupSection
554
554
  + '\n\nYour jobs:\n'
555
- + '1. For each step, write a STABLE SELECTOR. Priority order:\n'
556
- + ' - data-testid, data-test, data-cy, data-qa, pi-test-identifier attributes use [attr="value"]\n'
557
- + ' - MOST IMPORTANT: If the element data contains piTestId, use [pi-test-identifier="VALUE"] as the selector — this is the most reliable selector possible. Always check piTestId first.\n'
558
- + ' - If parentTestId exists and piTestId does not, use [pi-test-identifier="parentTestId"] as anchor\n'
559
- + ' - title attribute if the element has a title, use a[title="..."] or button[title="..."] — this is VERY reliable\n'
560
- + ' - Unique meaningful id (NOT random/generated IDs) use #id\n'
561
- + ' - Semantic selector e.g. button:has-text("Login"), input[name="email"]\n'
562
- + ' - Role + text e.g. [role="button"]:has-text("Submit")\n'
563
- + ' - Class-based selector for well-named classes e.g. .chart-container, .save-btn\n'
564
- + ' - Fall back to the original selector if nothing better\n'
565
- + ' CRITICAL RULES FOR SELECTORS:\n'
566
- + ' - ONLY use attributes that ACTUALLY EXIST on the element from the captured data. NEVER invent aria-label, title, or other attributes that are not in the element info.\n'
567
- + ' - For icon elements (<i>, <span> with fa/icon classes): use the PARENT title or pi-test-identifier as the anchor e.g. a[title="Create new chart"] or [pi-test-identifier="x"] i. Check parentTitle and parentTestId fields.\n'
568
- + ' - NEVER use IDs that look randomly generated (e.g. #highcharts-abc123-58, #ng-view-1, anything with random hex/numbers)\n'
569
- + ' - NEVER use :nth-child or :nth-of-type positional selectors\n'
570
- + ' - For chart/visualization containers: use class-based selectors like .highcharts-container, .chart-wrapper, [class*="chart"]\n'
571
- + ' - CRITICAL: pi-test-identifier values ending in a long number are DYNAMIC (e.g. "ChartColumn.operandOne.sort.option.desc.885249556") — strip the number and use *= contains: [pi-test-identifier*="ChartColumn.operandOne.sort.option.desc"] NOT exact =\n'
572
- + ' - Any attribute value ending in 5+ digits is almost certainly a dynamic runtime ID — use *= instead of =\n'
573
- + ' - For assert steps especially: make sure the selector will match on every run, not just once\n\n'
555
+ + '1. For each step, write a STABLE SELECTOR using this STRICT priority order — always use the HIGHEST priority option available:\n'
556
+ + ' PRIORITY 1 (ALWAYS USE IF AVAILABLE): piTestId field → [pi-test-identifier="VALUE"]\n'
557
+ + ' PRIORITY 2: parentTestId field (when clicking icon inside element) [pi-test-identifier="parentTestId"]\n'
558
+ + ' PRIORITY 3: dataTestId field [data-testid="VALUE"]\n'
559
+ + ' PRIORITY 4: Unique non-random id #id\n'
560
+ + ' PRIORITY 5: title attribute (from element.title field) a[title="VALUE"] or button[title="VALUE"]\n'
561
+ + ' PRIORITY 6: Semantic selector button:has-text("Login"), input[name="email"]\n'
562
+ + ' PRIORITY 7: Class-based .meaningful-class-name\n'
563
+ + ' PRIORITY 8: Original selector as fallback\n'
564
+ + '\n'
565
+ + ' EXAMPLE: If element has piTestId="ChartEditor.saveChartButton" AND title="Save" → use [pi-test-identifier="ChartEditor.saveChartButton"] NOT a[title="Save"]\n'
566
+ + ' EXAMPLE: If element has piTestId=null AND title="Create new chart" use a[title="Create new chart"]\n'
567
+ + '\n'
568
+ + ' HARD RULES:\n'
569
+ + ' - NEVER use IDs with random hex/numbers (e.g. #chart-abc123, #ng-view-1)\n'
570
+ + ' - NEVER use :nth-child or :nth-of-type\n'
571
+ + ' - NEVER invent attributes only use what is in the captured element data\n'
572
+ + ' - pi-test-identifier values ending in 5+ digits are DYNAMIC — use *= e.g. [pi-test-identifier*="ChartColumn.sort.desc"]\n'
573
+ + ' - For icon elements (<i>, <svg>): use piTestId or parentTestId as anchor, never bare i.fa-something\n\n'
574
574
  + '2. For each step, write a SHORT human-readable description (max 10 words). Examples:\n'
575
575
  + ' - "Click the Login button"\n'
576
576
  + ' - "Type username into email field"\n'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.87",
3
+ "version": "2.0.89",
4
4
  "description": "Browser-based QA tool — record tests by using your app, replay them deterministically, generate Playwright code automatically",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -7370,11 +7370,27 @@ function mergeDebugSteps(newSteps) {
7370
7370
  });
7371
7371
  }
7372
7372
 
7373
- function insertAndOpenEditor(stepsToInsert) {
7373
+ async function insertAndOpenEditor(stepsToInsert) {
7374
7374
  const insertAt = debugRecordingState.insertAfterIndex + 1;
7375
- reEditorState.steps.splice(insertAt, 0, ...stepsToInsert);
7376
7375
 
7377
- appendRunLine('<span style="color:var(--green)">\u2713 ' + stepsToInsert.length + ' steps inserted at position ' + (insertAt + 1) + '</span>');
7376
+ // Process new steps through LLM for stable selectors
7377
+ appendRunLine('<span style="color:var(--muted)">Processing new steps...</span>');
7378
+ let processedSteps = stepsToInsert;
7379
+ try {
7380
+ const procRes = await fetch(API_BASE + '/api/record/process', {
7381
+ method: 'POST',
7382
+ headers: { 'Content-Type': 'application/json' },
7383
+ body: JSON.stringify({ steps: stepsToInsert, testName: reEditorState.testName, url: reEditorState.url }),
7384
+ });
7385
+ if (procRes.ok) {
7386
+ const proc = await procRes.json();
7387
+ processedSteps = proc.steps || stepsToInsert;
7388
+ }
7389
+ } catch {}
7390
+
7391
+ reEditorState.steps.splice(insertAt, 0, ...processedSteps);
7392
+
7393
+ appendRunLine('<span style="color:var(--green)">\u2713 ' + processedSteps.length + ' steps inserted at position ' + (insertAt + 1) + '</span>');
7378
7394
  appendRunLine('<span style="color:var(--muted)">Opening editor — review and save when ready</span>');
7379
7395
 
7380
7396
  setTimeout(() => {
@@ -7383,7 +7399,7 @@ function insertAndOpenEditor(stepsToInsert) {
7383
7399
  document.getElementById('re-url').value = reEditorState.url;
7384
7400
  document.getElementById('re-reusable').checked = reEditorState.reusable || false;
7385
7401
  renderReSteps();
7386
- showToast(stepsToInsert.length + ' steps added — review and save');
7402
+ showToast(processedSteps.length + ' steps added — review and save');
7387
7403
  }, 800);
7388
7404
  }
7389
7405
  function reAddAssertion() { reAddAssertionAfter(reEditorState.steps.length-1); }
@@ -7594,7 +7610,27 @@ async function saveRecordedTest() {
7594
7610
  const scope=document.getElementById('results-scope').value;
7595
7611
  if (!name) { showToast('Please enter a test name'); return; }
7596
7612
  try {
7597
- const res=await fetch(API_BASE+'/api/record/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,scope,url:recorderState.startUrl,steps:recorderState.steps,playwrightJs:recorderState.playwrightJs,playwrightTs:recorderState.playwrightTs})});
7613
+ // Process steps through LLM first to get stable selectors, then save
7614
+ let steps = recorderState.steps;
7615
+ let playwrightJs = recorderState.playwrightJs;
7616
+ let playwrightTs = recorderState.playwrightTs;
7617
+ if (steps && steps.length && !playwrightJs) {
7618
+ showToast('Processing steps...');
7619
+ try {
7620
+ const procRes = await fetch(API_BASE + '/api/record/process', {
7621
+ method: 'POST',
7622
+ headers: { 'Content-Type': 'application/json' },
7623
+ body: JSON.stringify({ steps, testName: name, url: recorderState.startUrl }),
7624
+ });
7625
+ if (procRes.ok) {
7626
+ const proc = await procRes.json();
7627
+ steps = proc.steps || steps;
7628
+ playwrightJs = proc.playwrightJs || playwrightJs;
7629
+ playwrightTs = proc.playwrightTs || playwrightTs;
7630
+ }
7631
+ } catch {}
7632
+ }
7633
+ const res=await fetch(API_BASE+'/api/record/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,scope,url:recorderState.startUrl,steps,playwrightJs,playwrightTs})});
7598
7634
  const data=await res.json();
7599
7635
  if (!res.ok) { showToast(data.error||'Failed'); return; }
7600
7636
  showToast('Saved: '+name);