Files
ComfyUI-Lora-Manager/tests/frontend/regression/sidebarFolderContextMenu.test.js
T
Will Miao 12930ce78c feat(sidebar): scan a single folder from the folder context menu (#1108)
The Refresh menu can scope a scan to one model root, but the folder a user is
looking at lives in the sidebar's unified tree, which merges every root into one
relative-path namespace. "Scan this folder" therefore addresses the folder, not a
root: the backend walks that relative path under every root that holds it, which
is also what makes the action safe while another drive is switched off.

Backend:
* GET /scan accepts `folder=<rel>` (alone or with `roots=`), rejected together
  with full_rebuild=true like the roots parameter. Validation reuses
  normalize_relative_folder(), extracted to module scope from ModelMoveService so
  the folder operations and the scan endpoint reject the same input (absolute
  paths, drive letters, `..` climbing) instead of each carrying its own copy.
* The reconcile summary carries `scope_label` (the folder) for a folder scope, so
  the result toast names the folder the user clicked instead of the roots it
  happens to live under; the completed WS payload carries it too.
* `folder` is a scope prefix exactly like a root: only that subtree is re-read or
  pruned, and an unreachable root keeps the entries that fall inside it.

Frontend:
* The sidebar folder context menu gains "Scan this folder" above "Check for
  updates in this folder" (they share the refresh divider); the entry is gated by
  the same supportsFolderManagement flag as the other folder operations.
* SidebarManager.scanFolder() resolves the node through the existing
  _resolveFolderCandidates() before doing anything: a folder no root holds any
  more explains itself ("no longer exists on disk") instead of scanning nothing,
  and an unresolvable multi-root node is refused rather than guessed.
* PageControls.refreshModels() and BaseModelApi.refreshModels() forward the folder
  scope, and _showRefreshSummary() prefers scope_label over the walked roots.

Verified live on the three-root sandbox with one drive switched off:
GET /scan?folder=pack000 walks drive-G and drive-Y, reports scope_label=pack000,
keeps drive-Z's 6 entries under that folder (kept_unreachable=6) and leaves all
420 models cached. 3704 passed, 7 skipped; frontend 1495 passed (150 files); vue
widgets 96 passed. The 2 new sidebar keys are [TODO: Translate] placeholders
pending the feature owner's go-ahead.
2026-10-07 15:15:35 +08:00

47 lines
1.7 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
import path from 'path';
// Regression guard for the sidebar folder context-menu layout: the two refresh
// actions (scan this folder, check for updates) sit on top, the folder
// operations form a single group, and the destructive entry stays last behind
// its own divider. SidebarManager gates those groups per page and collapses the
// dividers when a group is hidden, so a reorder here also changes what the
// recipes page shows.
describe('Sidebar folder context menu layout', () => {
const repoRoot = path.resolve(__dirname, '../../..');
const html = readFileSync(
path.join(repoRoot, 'templates/components/context_menu.html'),
'utf-8'
);
const menuHtml = html.slice(
html.indexOf('id="sidebarFolderContextMenu"'),
html.indexOf('<!-- Sidebar View Options Menu -->')
);
const sequence = [...menuHtml.matchAll(/<div class="([^"]+)"([^>]*)>/g)].map(([, classes, rest]) => {
if (classes.includes('context-menu-separator')) return 'separator';
return /data-action="([^"]+)"/.exec(rest)?.[1] || null;
});
it('keeps the refresh actions first and the folder operations grouped', () => {
expect(sequence).toEqual([
'scan-folder',
'check-folder-updates',
'separator',
'create-subfolder',
'rename-folder',
'separator',
'delete-folder',
]);
});
it('keeps the destructive entry last and visually marked', () => {
const deleteEntry = menuHtml.match(/<div class="([^"]*)"\s+data-action="delete-folder"/);
expect(deleteEntry).not.toBeNull();
expect(deleteEntry[1]).toContain('delete-item');
expect(sequence[sequence.length - 1]).toBe('delete-folder');
});
});