skopix 2.0.77 → 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.
- package/cli/commands/dashboard.js +66 -25
- package/package.json +1 -1
- package/web/app/index.html +18 -5
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -3886,6 +3899,34 @@ async function saveSavedUrls(urls) {
|
|
|
3886
3899
|
await fs.ensureDir(path.dirname(file));
|
|
3887
3900
|
await fs.writeFile(file, yaml.stringify(urls));
|
|
3888
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
|
+
|
|
3889
3930
|
const LIBRARY_FILE = () => path.join(os.homedir(), '.skopix', 'step-library.yaml');
|
|
3890
3931
|
const LIBRARY_PENDING_FILE = () => path.join(os.homedir(), '.skopix', 'step-library-pending.yaml');
|
|
3891
3932
|
|
package/package.json
CHANGED
package/web/app/index.html
CHANGED
|
@@ -6069,7 +6069,12 @@ async function loadLibraryView() {
|
|
|
6069
6069
|
renderFolderTree('library-folder-tree', 'library', selectedLibraryFolder);
|
|
6070
6070
|
renderPendingSteps(pending);
|
|
6071
6071
|
populateLibraryTagFilter();
|
|
6072
|
-
filterLibrarySteps();
|
|
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
|
|