skopix 2.0.76 → 2.0.78

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.
@@ -1999,12 +1999,47 @@ export async function dashboardCommand(options) {
1999
1999
  sendJSON(res, 200, step);
2000
2000
  return;
2001
2001
  }
2002
+ if (pathname === '/api/step-library/recalc-usage' && method === 'POST') {
2003
+ // Recalculate usage counts from actual tests
2004
+ const library = await listLibrarySteps(suitesDir);
2005
+ const allTests = await listAllTests(suitesDir);
2006
+ for (const step of library) {
2007
+ const sel = (step.stableSelector || step.selector || '').toLowerCase();
2008
+ const keyTokens = sel.match(/\.[a-z][a-z0-9_-]+|\[[\w-]+=["'][^"']+["']\]|#[a-z][a-z0-9_-]+/g) || [];
2009
+ step.usageCount = allTests.reduce((count, t) => {
2010
+ return count + (t.steps || []).filter(s => {
2011
+ const sSel = (s.stableSelector||s.selector||'').toLowerCase();
2012
+ if (!sSel) return false;
2013
+ if (sSel === sel) return true;
2014
+ if (keyTokens.some(tok => tok.length > 5 && sSel.includes(tok))) return true;
2015
+ return stepSimilarity({ selector: sSel }, { selector: sel }) >= 0.7;
2016
+ }).length > 0 ? 1 : 0;
2017
+ }, 0);
2018
+ }
2019
+ await saveLibrarySteps(library);
2020
+ sendJSON(res, 200, { recalculated: library.length });
2021
+ return;
2022
+ }
2002
2023
  if (pathname.match(/^\/api\/step-library\/[^/]+$/) && method === 'PUT') {
2003
2024
  const id = decodeURIComponent(pathname.split('/')[3]);
2004
2025
  const data = JSON.parse(await readBody(req));
2026
+ // Get old step to detect selector/name changes
2027
+ const oldLibrary = await listLibrarySteps(suitesDir);
2028
+ const oldStep = oldLibrary.find(s => s.id === id);
2005
2029
  const step = await updateLibraryStep(suitesDir, id, data);
2006
- if (!step) sendJSON(res, 404, { error: 'Not found' });
2007
- else sendJSON(res, 200, step);
2030
+ if (!step) { sendJSON(res, 404, { error: 'Not found' }); return; }
2031
+ // Auto-sync tests if selector or name changed
2032
+ if (oldStep) {
2033
+ const oldSel = (oldStep.stableSelector || oldStep.selector || '').toLowerCase();
2034
+ const newSel = data.stableSelector || data.selector || '';
2035
+ const nameChanged = data.name && data.name !== oldStep.name;
2036
+ const selChanged = newSel && newSel.toLowerCase() !== oldSel;
2037
+ if (selChanged || nameChanged) {
2038
+ const allFromSelectors = [oldSel];
2039
+ await syncSelectorsInternal(suitesDir, allFromSelectors, newSel || oldSel, data.name || oldStep.name);
2040
+ }
2041
+ }
2042
+ sendJSON(res, 200, step);
2008
2043
  return;
2009
2044
  }
2010
2045
  if (pathname.match(/^\/api\/step-library\/[^/]+$/) && method === 'DELETE') {
@@ -2021,29 +2056,7 @@ export async function dashboardCommand(options) {
2021
2056
  }
2022
2057
  if (pathname === '/api/step-library/sync-selectors' && method === 'POST') {
2023
2058
  const { fromSelectors, toSelector, toName } = JSON.parse(await readBody(req));
2024
- const allTests = await listAllTests(suitesDir);
2025
- let updated = 0;
2026
- // Build key tokens from all fromSelectors for smart matching
2027
- const allFromTokens = fromSelectors.flatMap(sel =>
2028
- (sel.match(/\.[a-z][a-z0-9_-]+|\[[\w-]+=["'][^"']+["']\]|#[a-z][a-z0-9_-]+/g) || []).filter(t => t.length > 5)
2029
- );
2030
- for (const test of allTests) {
2031
- if (!test.steps || !test.steps.length) continue;
2032
- let changed = false;
2033
- const newSteps = test.steps.map(step => {
2034
- const sSel = (step.stableSelector||step.selector||'').toLowerCase();
2035
- const isMatch = fromSelectors.includes(sSel) ||
2036
- allFromTokens.some(tok => sSel.includes(tok)) ||
2037
- fromSelectors.some(fSel => stepSimilarity({ selector: sSel }, { selector: fSel }) >= 0.6);
2038
- if (isMatch) {
2039
- changed = true;
2040
- updated++;
2041
- return { ...step, stableSelector: toSelector, selector: toSelector, description: toName || step.description };
2042
- }
2043
- return step;
2044
- });
2045
- if (changed) await updateTest(suitesDir, test.scope, test.id, { ...test, steps: newSteps });
2046
- }
2059
+ const updated = await syncSelectorsInternal(suitesDir, fromSelectors, toSelector, toName);
2047
2060
  sendJSON(res, 200, { updated });
2048
2061
  return;
2049
2062
  }
@@ -2767,6 +2780,7 @@ function cleanTest(t) {
2767
2780
  if (t.headless) out.headless = true;
2768
2781
  if (t.generateReport === false) out.generateReport = false;
2769
2782
  if (t.browserZoom && t.browserZoom !== 1) out.browserZoom = t.browserZoom;
2783
+ if (t.folder) out.folder = t.folder;
2770
2784
  return out;
2771
2785
  }
2772
2786
 
@@ -3885,6 +3899,34 @@ async function saveSavedUrls(urls) {
3885
3899
  await fs.ensureDir(path.dirname(file));
3886
3900
  await fs.writeFile(file, yaml.stringify(urls));
3887
3901
  }
3902
+ // Internal helper — sync selectors across all tests
3903
+ async function syncSelectorsInternal(suitesDir, fromSelectors, toSelector, toName) {
3904
+ const allTests = await listAllTests(suitesDir);
3905
+ let updated = 0;
3906
+ const fromLower = fromSelectors.map(s => s.toLowerCase());
3907
+ const allFromTokens = fromLower.flatMap(sel =>
3908
+ (sel.match(/\.[a-z][a-z0-9_-]+|\[[\w-]+=["'][^"']+["']\]|#[a-z][a-z0-9_-]+/g) || []).filter(t => t.length > 5)
3909
+ );
3910
+ for (const test of allTests) {
3911
+ if (!test.steps || !test.steps.length) continue;
3912
+ let changed = false;
3913
+ const newSteps = test.steps.map(step => {
3914
+ const sSel = (step.stableSelector||step.selector||'').toLowerCase();
3915
+ const isMatch = fromLower.includes(sSel) ||
3916
+ allFromTokens.some(tok => sSel.includes(tok)) ||
3917
+ fromLower.some(fSel => stepSimilarity({ selector: sSel }, { selector: fSel }) >= 0.7);
3918
+ if (isMatch && sSel) {
3919
+ changed = true;
3920
+ updated++;
3921
+ return { ...step, stableSelector: toSelector, selector: toSelector, description: toName || step.description };
3922
+ }
3923
+ return step;
3924
+ });
3925
+ if (changed) await updateTest(suitesDir, test.scope, test.id, { ...test, steps: newSteps });
3926
+ }
3927
+ return updated;
3928
+ }
3929
+
3888
3930
  const LIBRARY_FILE = () => path.join(os.homedir(), '.skopix', 'step-library.yaml');
3889
3931
  const LIBRARY_PENDING_FILE = () => path.join(os.homedir(), '.skopix', 'step-library-pending.yaml');
3890
3932
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.76",
3
+ "version": "2.0.78",
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": {
@@ -6069,7 +6069,12 @@ async function loadLibraryView() {
6069
6069
  renderFolderTree('library-folder-tree', 'library', selectedLibraryFolder);
6070
6070
  renderPendingSteps(pending);
6071
6071
  populateLibraryTagFilter();
6072
- filterLibrarySteps(); // uses selectedLibraryFolder to filter
6072
+ filterLibrarySteps();
6073
+ // Recalculate usage counts in background
6074
+ fetch(API_BASE + '/api/step-library/recalc-usage', { method: 'POST' })
6075
+ .then(() => fetchLibrarySteps())
6076
+ .then(() => filterLibrarySteps())
6077
+ .catch(() => {});
6073
6078
  }
6074
6079
 
6075
6080
  async function approvePendingUI(id) {
@@ -6525,22 +6530,30 @@ async function saveLibraryStep() {
6525
6530
 
6526
6531
  try {
6527
6532
  if (isPending) {
6528
- // Dismiss pending and add directly to library with edits
6529
6533
  await fetch(API_BASE + '/api/step-library/pending/' + encodeURIComponent(id) + '/dismiss', { method: 'POST' });
6530
6534
  const body = { name, selector, stableSelector: selector, defaultAction: defaultAction || null, tags };
6531
6535
  await fetch(API_BASE + '/api/step-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
6536
+ closeLibraryStepModal();
6537
+ await loadLibraryView();
6538
+ showToast('Element added to library');
6532
6539
  } else if (id) {
6533
6540
  const body = { name, selector, stableSelector: selector, defaultAction: defaultAction || null, tags };
6534
6541
  const res = await fetch(API_BASE + '/api/step-library/' + encodeURIComponent(id), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
6535
6542
  if (!res.ok) throw new Error(await res.text());
6543
+ const updated = await res.json();
6544
+ closeLibraryStepModal();
6545
+ await loadLibraryView();
6546
+ // Show how many tests were updated
6547
+ const usages = await fetch(API_BASE + '/api/step-library/' + encodeURIComponent(id) + '/usages').then(r => r.json()).catch(() => []);
6548
+ showToast(`Element updated — ${usages.length} test${usages.length !== 1 ? 's' : ''} synced`);
6536
6549
  } else {
6537
6550
  const body = { name, selector, stableSelector: selector, defaultAction: defaultAction || null, tags };
6538
6551
  const res = await fetch(API_BASE + '/api/step-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
6539
6552
  if (!res.ok) throw new Error(await res.text());
6553
+ closeLibraryStepModal();
6554
+ await loadLibraryView();
6555
+ showToast('Element added to library');
6540
6556
  }
6541
- closeLibraryStepModal();
6542
- await loadLibraryView();
6543
- showToast(isPending ? 'Element added to library' : id ? 'Element updated' : 'Element added to library');
6544
6557
  } catch (err) { showToast('Error: ' + err.message); }
6545
6558
  }
6546
6559