skopix 2.0.84 → 2.0.86

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.
@@ -1036,11 +1036,13 @@ export async function dashboardCommand(options) {
1036
1036
  const { scope, name, url, steps, playwrightJs, playwrightTs, tags } = JSON.parse(body);
1037
1037
  if (!name) { sendJSON(res, 400, { error: 'name is required' }); return; }
1038
1038
  try {
1039
- const testData = { name, type: 'recorded', url: url || '', steps: steps || [], playwrightJs: playwrightJs || '', playwrightTs: playwrightTs || '', tags: tags || [] };
1039
+ // Match steps against library before saving
1040
+ const matched = await matchStepsToLibrary(suitesDir, steps || []);
1041
+ const testData = { name, type: 'recorded', url: url || '', steps: matched.steps, playwrightJs: playwrightJs || '', playwrightTs: playwrightTs || '', tags: tags || [] };
1040
1042
  const result = await createTest(suitesDir, scope || 'saved', testData);
1041
1043
  if (teamMode && currentUser) { teamMode.db.logAudit({ userId: currentUser.id, action: 'test.created', targetType: 'test', targetId: result.id, metadata: { scope: scope || 'saved', type: 'recorded' } }); }
1042
- // Auto-extract steps to library in background (non-blocking)
1043
- extractStepsToLibrary(suitesDir, steps || [], name).catch(() => {});
1044
+ // Auto-extract unmatched steps to pending review
1045
+ extractStepsToLibrary(suitesDir, matched.steps, name).catch(() => {});
1044
1046
  sendJSON(res, 200, result);
1045
1047
  } catch (err) { sendJSON(res, 400, { error: err.message }); }
1046
1048
  return;
@@ -1575,8 +1577,11 @@ export async function dashboardCommand(options) {
1575
1577
  const body = await readBody(req);
1576
1578
  const data = JSON.parse(body);
1577
1579
  try {
1580
+ // Match steps against library before saving
1581
+ const matched = await matchStepsToLibrary(suitesDir, data.steps || []);
1582
+ data.steps = matched.steps;
1578
1583
  const result = await updateTest(suitesDir, scope, testId, data);
1579
- // Extract any new steps to pending review queue
1584
+ // Extract any new unmatched steps to pending review queue
1580
1585
  extractStepsToLibrary(suitesDir, data.steps || [], data.name || testId).catch(() => {});
1581
1586
  sendJSON(res, 200, result);
1582
1587
  } catch (err) {
@@ -3878,21 +3883,40 @@ async function matchStepsToLibrary(suitesDir, steps) {
3878
3883
 
3879
3884
  let count = 0;
3880
3885
  const matched = steps.map(step => {
3881
- const sSel = (step.stableSelector || step.selector || '').toLowerCase();
3886
+ const sSel = (step.stableSelector || step.selector || '').toLowerCase().trim();
3882
3887
  if (!sSel) return step;
3883
3888
 
3884
- // Find matching library element
3885
3889
  const match = library.find(lib => {
3886
- const lSel = (lib.stableSelector || lib.selector || '').toLowerCase();
3890
+ const lSel = (lib.stableSelector || lib.selector || '').toLowerCase().trim();
3887
3891
  if (!lSel) return false;
3888
- // Exact match
3892
+
3893
+ // 1. Exact match
3889
3894
  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;
3895
+
3896
+ // 2. Extract pi-test-identifier values from both and compare
3897
+ const piFrom = (sSel.match(/pi-test-identifier[*^]?=["']([^"']+)["']/) || [])[1];
3898
+ const piLib = (lSel.match(/pi-test-identifier[*^]?=["']([^"']+)["']/) || [])[1];
3899
+ if (piFrom && piLib) {
3900
+ if (piFrom === piLib) return true;
3901
+ // One starts with the other (handles *= vs =)
3902
+ if (piFrom.startsWith(piLib) || piLib.startsWith(piFrom)) return true;
3903
+ }
3904
+
3905
+ // 3. Both use same has-text value (e.g. :has-text("Mariadb"))
3906
+ const textFrom = (sSel.match(/has-text\(["']([^"']+)["']\)/) || [])[1];
3907
+ const textLib = (lSel.match(/has-text\(["']([^"']+)["']\)/) || [])[1];
3908
+ if (textFrom && textLib && textFrom.toLowerCase() === textLib.toLowerCase()) {
3909
+ // Only match if same element type or one contains the other
3910
+ const tagFrom = sSel.split(':')[0].split(' ').pop();
3911
+ const tagLib = lSel.split(':')[0].split(' ').pop();
3912
+ if (tagFrom === tagLib || sSel.includes(lSel) || lSel.includes(sSel)) return true;
3913
+ }
3914
+
3915
+ // 4. Step selector is a child/descendant of library selector
3916
+ // e.g. "a[title='x'] i.fa-plus" should match "a[title='x']"
3917
+ if (lSel.length > 10 && sSel.includes(lSel)) return true;
3918
+ if (sSel.length > 10 && lSel.includes(sSel)) return true;
3919
+
3896
3920
  return false;
3897
3921
  });
3898
3922
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.84",
3
+ "version": "2.0.86",
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": {