skopix 2.0.82 → 2.0.84
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 +47 -0
- package/core/llm.js +9 -0
- package/package.json +1 -1
|
@@ -1022,6 +1022,10 @@ export async function dashboardCommand(options) {
|
|
|
1022
1022
|
const userEnv = await resolveUserSecretsEnv(currentUser && currentUser.id, teamMode);
|
|
1023
1023
|
Object.assign(process.env, userEnv || {});
|
|
1024
1024
|
const result = await processRecording({ steps, testName, url, provider: provider || 'gemini' });
|
|
1025
|
+
// Match processed steps against library — substitute known elements
|
|
1026
|
+
const matched = await matchStepsToLibrary(suitesDir, result.steps || []);
|
|
1027
|
+
result.steps = matched.steps;
|
|
1028
|
+
result.matchedCount = matched.count;
|
|
1025
1029
|
sendJSON(res, 200, result);
|
|
1026
1030
|
} catch (err) { sendJSON(res, 500, { error: err.message }); }
|
|
1027
1031
|
return;
|
|
@@ -1572,6 +1576,8 @@ export async function dashboardCommand(options) {
|
|
|
1572
1576
|
const data = JSON.parse(body);
|
|
1573
1577
|
try {
|
|
1574
1578
|
const result = await updateTest(suitesDir, scope, testId, data);
|
|
1579
|
+
// Extract any new steps to pending review queue
|
|
1580
|
+
extractStepsToLibrary(suitesDir, data.steps || [], data.name || testId).catch(() => {});
|
|
1575
1581
|
sendJSON(res, 200, result);
|
|
1576
1582
|
} catch (err) {
|
|
1577
1583
|
sendJSON(res, 400, { error: err.message });
|
|
@@ -3865,6 +3871,47 @@ async function syncIssuesStatus() {
|
|
|
3865
3871
|
// STEP LIBRARY — persistent store of reusable UI interactions
|
|
3866
3872
|
// ═══════════════════════════════════════════════════════════════
|
|
3867
3873
|
|
|
3874
|
+
// Match recorded steps against library — substitute known elements automatically
|
|
3875
|
+
async function matchStepsToLibrary(suitesDir, steps) {
|
|
3876
|
+
const library = await listLibrarySteps(suitesDir);
|
|
3877
|
+
if (!library.length || !steps.length) return { steps, count: 0 };
|
|
3878
|
+
|
|
3879
|
+
let count = 0;
|
|
3880
|
+
const matched = steps.map(step => {
|
|
3881
|
+
const sSel = (step.stableSelector || step.selector || '').toLowerCase();
|
|
3882
|
+
if (!sSel) return step;
|
|
3883
|
+
|
|
3884
|
+
// Find matching library element
|
|
3885
|
+
const match = library.find(lib => {
|
|
3886
|
+
const lSel = (lib.stableSelector || lib.selector || '').toLowerCase();
|
|
3887
|
+
if (!lSel) return false;
|
|
3888
|
+
// Exact match
|
|
3889
|
+
if (sSel === lSel) return true;
|
|
3890
|
+
// One contains the other (handles i.fa-plus vs a[title="x"] i.fa-plus)
|
|
3891
|
+
if (sSel.includes(lSel) || lSel.includes(sSel)) return true;
|
|
3892
|
+
// Shared key tokens
|
|
3893
|
+
const keyTokens = lSel.match(/\.[a-z][a-z0-9_-]+|\[[\w-]+=["'][^"']+["']\]|#[a-z][a-z0-9_-]+/g) || [];
|
|
3894
|
+
const strongTokens = keyTokens.filter(t => t.length > 8);
|
|
3895
|
+
if (strongTokens.length > 0 && strongTokens.some(tok => sSel.includes(tok))) return true;
|
|
3896
|
+
return false;
|
|
3897
|
+
});
|
|
3898
|
+
|
|
3899
|
+
if (match) {
|
|
3900
|
+
count++;
|
|
3901
|
+
return {
|
|
3902
|
+
...step,
|
|
3903
|
+
stableSelector: match.stableSelector || match.selector,
|
|
3904
|
+
selector: match.selector || match.stableSelector,
|
|
3905
|
+
description: match.name || step.description,
|
|
3906
|
+
libraryId: match.id,
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
return step;
|
|
3910
|
+
});
|
|
3911
|
+
|
|
3912
|
+
return { steps: matched, count };
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3868
3915
|
const FOLDERS_FILE = () => path.join(os.homedir(), '.skopix', 'folders.yaml');
|
|
3869
3916
|
|
|
3870
3917
|
async function getFolders() {
|
package/core/llm.js
CHANGED
|
@@ -509,6 +509,13 @@ export async function processRecording({ steps, testName, url, provider, apiKey,
|
|
|
509
509
|
type: s.element.type,
|
|
510
510
|
text: s.element.text,
|
|
511
511
|
classes: s.element.classes,
|
|
512
|
+
title: s.element.title || null,
|
|
513
|
+
ariaLabel: s.element.ariaLabel || null,
|
|
514
|
+
piTestId: s.element.piTestId || null,
|
|
515
|
+
dataTestId: s.element.dataTestId || null,
|
|
516
|
+
parentTitle: s.element.parentTitle || null,
|
|
517
|
+
parentTestId: s.element.parentTestId || null,
|
|
518
|
+
parentClasses: s.element.parentClasses || null,
|
|
512
519
|
} : null,
|
|
513
520
|
value: s.action === 'type' && s.isPassword ? '[password - use process.env.TEST_PASSWORD]' : (s.value || null),
|
|
514
521
|
isPassword: s.isPassword || false,
|
|
@@ -547,6 +554,8 @@ export async function processRecording({ steps, testName, url, provider, apiKey,
|
|
|
547
554
|
+ '\n\nYour jobs:\n'
|
|
548
555
|
+ '1. For each step, write a STABLE SELECTOR. Priority order:\n'
|
|
549
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'
|
|
550
559
|
+ ' - title attribute — if the element has a title, use a[title="..."] or button[title="..."] — this is VERY reliable\n'
|
|
551
560
|
+ ' - Unique meaningful id (NOT random/generated IDs) use #id\n'
|
|
552
561
|
+ ' - Semantic selector e.g. button:has-text("Login"), input[name="email"]\n'
|
package/package.json
CHANGED