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.
- package/cli/commands/dashboard.js +16 -0
- package/core/llm.js +19 -19
- package/package.json +1 -1
- package/web/app/index.html +41 -5
|
@@ -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
|
|
556
|
-
+ '
|
|
557
|
-
+ '
|
|
558
|
-
+ '
|
|
559
|
-
+ '
|
|
560
|
-
+ '
|
|
561
|
-
+ '
|
|
562
|
-
+ '
|
|
563
|
-
+ '
|
|
564
|
-
+ '
|
|
565
|
-
+ '
|
|
566
|
-
+ '
|
|
567
|
-
+ '
|
|
568
|
-
+ '
|
|
569
|
-
+ ' - NEVER use
|
|
570
|
-
+ ' -
|
|
571
|
-
+ ' -
|
|
572
|
-
+ ' -
|
|
573
|
-
+ ' - For
|
|
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
package/web/app/index.html
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|