mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(other): guide standalone users to settings.json from the no-paths empty state
The standalone empty state showed the folder_paths keys but not where to put them, and its Open Settings button led to a modal that cannot edit primary folder paths. Now the page shows the real settings.json path and an Open Settings Folder button backed by the existing open-location API. Also stop open_settings_location from claiming success on headless Linux sessions: with no DISPLAY/WAYLAND_DISPLAY, xdg-open cannot work, so the handler now returns clipboard mode and the browser copies/shows the path instead.
This commit is contained in:
@@ -25,6 +25,7 @@ describe('Other Models disabled page', () => {
|
||||
document.body.innerHTML = [
|
||||
'<button id="enableOtherModelsBtn"></button>',
|
||||
'<button id="openOtherModelsSettingsBtn"></button>',
|
||||
'<button id="openSettingsFolderBtn"></button>',
|
||||
].join('');
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
@@ -64,6 +65,52 @@ describe('Other Models disabled page', () => {
|
||||
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
||||
});
|
||||
|
||||
it('reveals the settings.json location from the standalone no-folders state', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ success: true, message: 'Opened settings folder' }),
|
||||
});
|
||||
|
||||
const button = document.getElementById('openSettingsFolderBtn');
|
||||
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await vi.waitFor(() => expect(showToastMock).toHaveBeenCalled());
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'/api/lm/settings/open-location',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
);
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'settings.openSettingsFileLocation.success',
|
||||
{},
|
||||
'success',
|
||||
);
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('copies the settings path to the clipboard in Docker mode', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText },
|
||||
configurable: true,
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ success: true, mode: 'clipboard', path: '/data/settings.json' }),
|
||||
});
|
||||
|
||||
document.getElementById('openSettingsFolderBtn').dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true }),
|
||||
);
|
||||
await vi.waitFor(() => expect(showToastMock).toHaveBeenCalled());
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith('/data/settings.json');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'settings.openSettingsFileLocation.copied',
|
||||
{ path: '/data/settings.json' },
|
||||
'success',
|
||||
);
|
||||
});
|
||||
|
||||
it('enables Other Models through the settings API and reloads', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -532,6 +532,62 @@ async def test_open_backup_location_uses_settings_directory(tmp_path, monkeypatc
|
||||
assert calls == [["xdg-open", str(backup_dir)]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_settings_location_headless_returns_clipboard_mode(tmp_path, monkeypatch):
|
||||
"""Without a GUI session xdg-open cannot work; the handler must hand the
|
||||
path to the browser instead of reporting a success that never happened."""
|
||||
settings_file = tmp_path / "settings" / "settings.json"
|
||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
settings_file.write_text("{}", encoding="utf-8")
|
||||
|
||||
handler = FileSystemHandler(settings_service=SimpleNamespace(settings_file=str(settings_file)))
|
||||
|
||||
monkeypatch.delenv("DISPLAY", raising=False)
|
||||
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
|
||||
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
|
||||
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
|
||||
|
||||
popen_calls = []
|
||||
monkeypatch.setattr(subprocess, "Popen", lambda *args, **kwargs: popen_calls.append(args))
|
||||
|
||||
response = await handler.open_settings_location(FakeRequest()) # pyright: ignore[reportArgumentType]
|
||||
payload = _json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["success"] is True
|
||||
assert payload["mode"] == "clipboard"
|
||||
assert payload["path"] == str(settings_file)
|
||||
assert popen_calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_settings_location_with_display_opens_folder(tmp_path, monkeypatch):
|
||||
settings_file = tmp_path / "settings" / "settings.json"
|
||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
settings_file.write_text("{}", encoding="utf-8")
|
||||
|
||||
handler = FileSystemHandler(settings_service=SimpleNamespace(settings_file=str(settings_file)))
|
||||
|
||||
monkeypatch.setenv("DISPLAY", ":0")
|
||||
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
|
||||
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_popen(args):
|
||||
calls.append(args)
|
||||
return MagicMock()
|
||||
|
||||
monkeypatch.setattr(subprocess, "Popen", fake_popen)
|
||||
|
||||
response = await handler.open_settings_location(FakeRequest()) # pyright: ignore[reportArgumentType]
|
||||
payload = _json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["success"] is True
|
||||
assert calls == [["xdg-open", str(settings_file.parent)]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_wildcards_location_creates_and_opens_directory(tmp_path, monkeypatch):
|
||||
wildcards_dir = tmp_path / "settings" / "wildcards"
|
||||
|
||||
@@ -121,6 +121,25 @@ def test_page_context_reports_feature_state(monkeypatch):
|
||||
assert provider(None) == {"other_disabled": True, "other_no_paths": False}
|
||||
|
||||
|
||||
def test_page_context_exposes_settings_file_in_standalone(monkeypatch):
|
||||
"""Standalone users must edit settings.json by hand; the empty state
|
||||
needs the real file path to point them at."""
|
||||
from py.config import config
|
||||
from py.services.settings_manager import get_settings_manager
|
||||
|
||||
manager = get_settings_manager()
|
||||
handler = OtherRoutes()
|
||||
provider = handler._get_page_context_provider()
|
||||
|
||||
monkeypatch.setattr(config, "other_roots", [], raising=False)
|
||||
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
||||
|
||||
context = provider(None)
|
||||
assert context["other_no_paths"] is True
|
||||
assert context["standalone_mode"] is True
|
||||
assert context["settings_file"] == manager.settings_file
|
||||
|
||||
|
||||
def test_get_expected_model_types_mentions_supported_types():
|
||||
expected = OtherRoutes()._get_expected_model_types()
|
||||
for name in ("VAE", "Upscaler", "TextEncoder", "CLIPVision", "Controlnet"):
|
||||
|
||||
Reference in New Issue
Block a user