mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
feat(recipes): show a summary modal after rematch runs
Replace the post-run toast cascade and the standalone L4 results modal with a summary modal modeled on the batch download summary: 3-state header, stat cards (matched / needs review / unresolved / errors), an L4 review table with per-entry undo, and a copyable report. Wired into the global, bulk and single-recipe rematch entries; complete no-op runs keep the lightweight toast. Obsolete results-modal code, styles and i18n keys are removed.
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||
|
||||
const showToastMock = vi.fn();
|
||||
const translateMock = vi.fn((key, params, fallback) => {
|
||||
if (typeof fallback === 'string') {
|
||||
// Apply {param} interpolation so counts remain assertable.
|
||||
return Object.entries(params || {}).reduce(
|
||||
(text, [name, value]) => text.replaceAll(`{${name}}`, String(value)),
|
||||
fallback
|
||||
);
|
||||
}
|
||||
return key;
|
||||
});
|
||||
|
||||
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
|
||||
translate: translateMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||
showToast: showToastMock,
|
||||
}));
|
||||
|
||||
async function getShowRematchSummary() {
|
||||
const { showRematchSummary } = await import(
|
||||
'../../../static/js/components/RematchSummaryModal.js'
|
||||
);
|
||||
return showRematchSummary;
|
||||
}
|
||||
|
||||
const L4_LORA = { recipe_id: 'r1', type: 'lora', entry: 'old.safetensors', file_name: 'new.safetensors', lora_index: 2 };
|
||||
const L4_CHECKPOINT = { recipe_id: 'r2', type: 'checkpoint', entry: 'cp-old', file_name: 'cp-new.safetensors' };
|
||||
|
||||
describe('RematchSummaryModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
document.body.innerHTML = '';
|
||||
global.fetch = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
delete global.fetch;
|
||||
delete navigator.clipboard;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders a success header when everything matched cleanly', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
showRematchSummary({ scope: 'global', total: 10, matchedRecipes: 2, matchedEntries: 3 });
|
||||
|
||||
const modal = document.getElementById('rematchSummaryModal');
|
||||
expect(modal).not.toBeNull();
|
||||
expect(modal.querySelector('.summary-header').classList.contains('success')).toBe(true);
|
||||
expect(modal.querySelector('.summary-title').textContent).toBe('Matched 3 entries');
|
||||
expect(modal.querySelector('.failure-table')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders an error header when nothing matched and errors occurred', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
showRematchSummary({ scope: 'global', total: 3, errors: 3 });
|
||||
|
||||
const modal = document.getElementById('rematchSummaryModal');
|
||||
expect(modal.querySelector('.summary-header').classList.contains('error')).toBe(true);
|
||||
expect(modal.querySelector('.summary-title').textContent).toBe('Rematch failed');
|
||||
});
|
||||
|
||||
it('renders a warning header for unresolved entries, L4 matches, or cancellations', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
|
||||
showRematchSummary({ scope: 'bulk', total: 2, matchedEntries: 1, unresolvedEntries: 1, unresolvedRecipes: 1 });
|
||||
expect(document.querySelector('#rematchSummaryModal .summary-header').classList.contains('warning')).toBe(true);
|
||||
|
||||
showRematchSummary({ scope: 'bulk', total: 2, matchedEntries: 2, l4Matches: [L4_LORA] });
|
||||
expect(document.querySelector('#rematchSummaryModal .summary-header').classList.contains('warning')).toBe(true);
|
||||
|
||||
showRematchSummary({ scope: 'global', total: 5, matchedEntries: 2, cancelled: true });
|
||||
const modal = document.getElementById('rematchSummaryModal');
|
||||
expect(modal.querySelector('.summary-header').classList.contains('warning')).toBe(true);
|
||||
expect(modal.querySelector('.rematch-cancelled-note')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders the four stat cards in order', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
showRematchSummary({
|
||||
scope: 'bulk',
|
||||
total: 4,
|
||||
matchedRecipes: 1,
|
||||
matchedEntries: 2,
|
||||
unresolvedEntries: 3,
|
||||
errors: 1,
|
||||
l4Matches: [L4_LORA],
|
||||
});
|
||||
|
||||
const modal = document.getElementById('rematchSummaryModal');
|
||||
const values = Array.from(modal.querySelectorAll('.stat-card-value')).map(el => el.textContent);
|
||||
expect(values).toEqual(['2', '1', '3', '1']);
|
||||
const labels = Array.from(modal.querySelectorAll('.stat-card-label')).map(el => el.textContent);
|
||||
expect(labels).toEqual(['Matched entries', 'Needs review', 'Unresolved', 'Errors']);
|
||||
});
|
||||
|
||||
it('renders the L4 review table only when matches exist', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
showRematchSummary({ scope: 'bulk', total: 2, matchedEntries: 2, l4Matches: [L4_LORA, L4_CHECKPOINT] });
|
||||
|
||||
const modal = document.getElementById('rematchSummaryModal');
|
||||
const rows = modal.querySelectorAll('.failure-table tbody tr');
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0].textContent).toContain('r1');
|
||||
expect(rows[0].textContent).toContain('old.safetensors');
|
||||
expect(rows[0].textContent).toContain('new.safetensors');
|
||||
expect(rows[1].textContent).toContain('cp-new.safetensors');
|
||||
expect(modal.querySelectorAll('.rematch-undo-btn')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('undo posts to the lora restore endpoint, then strikes and disables the row', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
global.fetch.mockResolvedValue({ ok: true, json: async () => ({ success: true }) });
|
||||
|
||||
showRematchSummary({ scope: 'bulk', total: 1, matchedEntries: 1, l4Matches: [L4_LORA] });
|
||||
|
||||
const modal = document.getElementById('rematchSummaryModal');
|
||||
const row = modal.querySelector('tr[data-l4-index="0"]');
|
||||
const button = row.querySelector('.rematch-undo-btn');
|
||||
button.click();
|
||||
await vi.waitFor(() => expect(button.disabled).toBe(true));
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/lm/recipe/lora/restore', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ recipe_id: 'r1', lora_index: 2 }),
|
||||
});
|
||||
expect(row.classList.contains('undone')).toBe(true);
|
||||
expect(button.textContent).toBe('Undone');
|
||||
});
|
||||
|
||||
it('undo posts to the checkpoint restore endpoint with recipe_id only', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
global.fetch.mockResolvedValue({ ok: true, json: async () => ({ success: true }) });
|
||||
|
||||
showRematchSummary({ scope: 'bulk', total: 1, matchedEntries: 1, l4Matches: [L4_CHECKPOINT] });
|
||||
|
||||
const button = document.querySelector('.rematch-undo-btn');
|
||||
button.click();
|
||||
await vi.waitFor(() => expect(button.disabled).toBe(true));
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/lm/recipe/checkpoint/restore', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ recipe_id: 'r2' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the row actionable and toasts when undo fails', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
global.fetch.mockResolvedValue({ ok: true, json: async () => ({ success: false, error: 'no snapshot' }) });
|
||||
|
||||
showRematchSummary({ scope: 'bulk', total: 1, matchedEntries: 1, l4Matches: [L4_LORA] });
|
||||
|
||||
const row = document.querySelector('tr[data-l4-index="0"]');
|
||||
const button = row.querySelector('.rematch-undo-btn');
|
||||
button.click();
|
||||
await vi.waitFor(() => expect(showToastMock).toHaveBeenCalled());
|
||||
|
||||
expect(button.disabled).toBe(false);
|
||||
expect(row.classList.contains('undone')).toBe(false);
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'modals.rematchResults.undoFailed',
|
||||
{ message: 'no snapshot' },
|
||||
'error'
|
||||
);
|
||||
});
|
||||
|
||||
it('copy report includes scope, counts and the L4 list with undo status', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
global.fetch.mockResolvedValue({ ok: true, json: async () => ({ success: true }) });
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
navigator.clipboard = { writeText };
|
||||
|
||||
showRematchSummary({
|
||||
scope: 'bulk',
|
||||
total: 2,
|
||||
matchedRecipes: 1,
|
||||
matchedEntries: 2,
|
||||
unresolvedEntries: 1,
|
||||
unresolvedRecipes: 1,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
l4Matches: [L4_LORA, L4_CHECKPOINT],
|
||||
});
|
||||
|
||||
// Undo the first row before copying so the report carries its status.
|
||||
const undoButton = document.querySelector('tr[data-l4-index="0"] .rematch-undo-btn');
|
||||
undoButton.click();
|
||||
await vi.waitFor(() => expect(undoButton.disabled).toBe(true));
|
||||
|
||||
document.querySelector('[data-action="copy-report"]').click();
|
||||
await vi.waitFor(() => expect(writeText).toHaveBeenCalled());
|
||||
|
||||
const report = writeText.mock.calls[0][0];
|
||||
expect(report).toContain('Scope: Selected recipes');
|
||||
expect(report).toContain('Total recipes: 2');
|
||||
expect(report).toContain('Matched entries: 2');
|
||||
expect(report).toContain('Needs review (filename matches): 2');
|
||||
expect(report).toContain('Unresolved entries: 1 (in 1 recipes)');
|
||||
expect(report).toContain('[r1] old.safetensors -> new.safetensors [undone]');
|
||||
expect(report).toContain('[r2] cp-old -> cp-new.safetensors');
|
||||
// The success toast fires in the writeText .then() microtask.
|
||||
await vi.waitFor(() => expect(showToastMock).toHaveBeenCalledWith('toast.api.copiedToClipboard', {}, 'success'));
|
||||
});
|
||||
|
||||
it('close removes the modal from the DOM', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
showRematchSummary({ scope: 'single', total: 1, matchedEntries: 1 });
|
||||
|
||||
expect(document.getElementById('rematchSummaryModal')).not.toBeNull();
|
||||
document.querySelector('[data-action="close-modal"].cancel-btn').click();
|
||||
expect(document.getElementById('rematchSummaryModal')).toBeNull();
|
||||
});
|
||||
|
||||
it('escapes HTML in L4 row fields', async () => {
|
||||
const showRematchSummary = await getShowRematchSummary();
|
||||
showRematchSummary({
|
||||
scope: 'bulk',
|
||||
total: 1,
|
||||
matchedEntries: 1,
|
||||
l4Matches: [{ recipe_id: 'r<x>', type: 'lora', entry: '<img src=x>', file_name: 'f.safetensors', lora_index: 0 }],
|
||||
});
|
||||
|
||||
const modal = document.getElementById('rematchSummaryModal');
|
||||
expect(modal.querySelector('.failure-table img')).toBeNull();
|
||||
expect(modal.querySelector('.failure-table tbody tr').textContent).toContain('<img src=x>');
|
||||
});
|
||||
});
|
||||
@@ -2233,7 +2233,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
expect(downloadExampleImagesApiMock).toHaveBeenCalledWith(['abc123hash'], null, { force: true });
|
||||
});
|
||||
|
||||
it('runs global recipe rematch with polling and toasts the rematched count', async () => {
|
||||
it('runs global recipe rematch with polling and opens the summary modal', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
@@ -2298,13 +2298,22 @@ describe('Interaction-level regression coverage', () => {
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(progressUI.showCancelButton).toHaveBeenCalledTimes(1);
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Matched 5 entries across 2 recipes.');
|
||||
// Oracle R4-F1 pin: count comes from `rematched`, a blind `repaired` mirror renders undefined
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
// A non-noop run opens the summary modal instead of toasting; the
|
||||
// progress overlay completes without a message.
|
||||
expect(progressUI.complete).toHaveBeenCalledWith();
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.success',
|
||||
{ count: 2, recipes: 2, entries: 5, failures: 0 },
|
||||
'success'
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
// unresolved_entries > 0 forces the warning header
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('warning')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-success .stat-card-value').textContent).toBe('5');
|
||||
expect(summaryModal.querySelector('.stat-card-skipped .stat-card-value').textContent).toBe('0');
|
||||
expect(summaryModal.querySelector('.stat-card-total .stat-card-value').textContent).toBe('1');
|
||||
expect(summaryModal.querySelector('.stat-card-failure .stat-card-value').textContent).toBe('0');
|
||||
expect(window.recipesPage.refresh).toHaveBeenCalledTimes(1);
|
||||
expect(rematchItem.classList.contains('disabled')).toBe(false);
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
@@ -2313,7 +2322,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('uses the warning toast variant when a global rematch completes with failures', async () => {
|
||||
it('opens the summary modal with a warning header when a global rematch completes with failures', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
@@ -2359,19 +2368,18 @@ describe('Interaction-level regression coverage', () => {
|
||||
}
|
||||
await runPromise;
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Matched 5 entries across 2 recipes, 2 failed.');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.successErrors',
|
||||
{ count: 2, recipes: 2, entries: 5, failures: 2 },
|
||||
'warning'
|
||||
);
|
||||
expect(progressUI.complete).toHaveBeenCalledWith();
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('warning')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-failure .stat-card-value').textContent).toBe('2');
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
delete window.recipesPage;
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('toasts an error when every recipe in a global rematch failed', async () => {
|
||||
it('opens the summary modal with an error header when every recipe in a global rematch failed', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
@@ -2417,19 +2425,18 @@ describe('Interaction-level regression coverage', () => {
|
||||
}
|
||||
await runPromise;
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Rematch failed for 3 of 3 recipes.');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.allFailed',
|
||||
{ total: 3, recipes: 0, entries: 0, failures: 3 },
|
||||
'error'
|
||||
);
|
||||
expect(progressUI.complete).toHaveBeenCalledWith();
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('error')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-failure .stat-card-value').textContent).toBe('3');
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
delete window.recipesPage;
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('toasts an info message when a global rematch found no local matches', async () => {
|
||||
it('opens the summary modal listing unresolved entries when a global rematch found no local matches', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
@@ -2475,19 +2482,18 @@ describe('Interaction-level regression coverage', () => {
|
||||
}
|
||||
await runPromise;
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('No local match found for 2 entries in 1 recipes.');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.noMatch',
|
||||
{ entries: 2, recipes: 1, total: 3, failures: 0 },
|
||||
'info'
|
||||
);
|
||||
expect(progressUI.complete).toHaveBeenCalledWith();
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('warning')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-total .stat-card-value').textContent).toBe('2');
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
delete window.recipesPage;
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('toasts the rematched count when a global rematch is cancelled', async () => {
|
||||
it('opens the summary modal marked as cancelled when a global rematch is cancelled', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
@@ -2533,11 +2539,11 @@ describe('Interaction-level regression coverage', () => {
|
||||
await runPromise;
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Rematch cancelled. 1 recipes updated (2 entries).');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.cancelled',
|
||||
{ count: 1, recipes: 1, entries: 2 },
|
||||
'info'
|
||||
);
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
expect(summaryModal.querySelector('.rematch-cancelled-note')).not.toBeNull();
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('warning')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-success .stat-card-value').textContent).toBe('2');
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
delete stateStub.currentPageType;
|
||||
|
||||
@@ -60,13 +60,6 @@ async function cancelRematchOptions() {
|
||||
rematchModalManager.cancelOptions();
|
||||
}
|
||||
|
||||
async function getRematchModalManager() {
|
||||
const { rematchModalManager } = await import(
|
||||
'../../../static/js/managers/RematchModalManager.js'
|
||||
);
|
||||
return rematchModalManager;
|
||||
}
|
||||
|
||||
describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -92,8 +85,8 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
}
|
||||
|
||||
// Oracle R4-F1 pin: branches on `result.rematched > 0` — a blind `repaired`
|
||||
// mirror would fire the skipped toast here.
|
||||
it('posts to the per-recipe rematch endpoint and toasts the rematched count', async () => {
|
||||
// mirror would render 0 matched entries in the summary modal here.
|
||||
it('posts to the per-recipe rematch endpoint and opens the summary modal', async () => {
|
||||
const menu = await createMenu();
|
||||
const card = document.getElementById('card');
|
||||
menu.showMenu(100, 100, card);
|
||||
@@ -125,16 +118,22 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ relaxed: false }),
|
||||
});
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
// Non-noop runs open the summary modal instead of toasting.
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchComplete',
|
||||
{ rematched: 2, skipped: 0, total: 1, entries: 2, recipes: 1, failures: 0 },
|
||||
'success'
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchSkipped',
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('success')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-success .stat-card-value').textContent).toBe('2');
|
||||
expect(summaryModal.querySelector('.stat-card-failure .stat-card-value').textContent).toBe('0');
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(2, '/api/lm/recipe/recipe-1');
|
||||
expect(updateSingleItemMock).toHaveBeenCalledWith('/recipes/recipe-1.webp', {
|
||||
id: 'recipe-1',
|
||||
@@ -142,7 +141,7 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('toasts an info message when the entries had no local match', async () => {
|
||||
it('opens the summary modal when the entries had no local match', async () => {
|
||||
const menu = await createMenu();
|
||||
const card = document.getElementById('card');
|
||||
menu.showMenu(100, 100, card);
|
||||
@@ -160,11 +159,11 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
await confirmRematchOptions();
|
||||
await flushAsyncTasks();
|
||||
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchUnmatched',
|
||||
{ entries: 2, recipes: 1, total: 1 },
|
||||
'info'
|
||||
);
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('warning')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-success .stat-card-value').textContent).toBe('0');
|
||||
expect(summaryModal.querySelector('.stat-card-total .stat-card-value').textContent).toBe('2');
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchSkipped',
|
||||
expect.anything(),
|
||||
@@ -303,7 +302,7 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the results modal when the result carries l4_matches', async () => {
|
||||
it('lists L4 filename matches in the summary modal with undo buttons', async () => {
|
||||
const menu = await createMenu();
|
||||
const card = document.getElementById('card');
|
||||
menu.showMenu(100, 100, card);
|
||||
@@ -321,11 +320,6 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
json: async () => ({ id: 'recipe-1', title: 'Updated Recipe' }),
|
||||
});
|
||||
|
||||
const rematchModalManager = await getRematchModalManager();
|
||||
const showResultsSpy = vi
|
||||
.spyOn(rematchModalManager, 'showResultsModal')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
document
|
||||
.querySelector('[data-action="rematch"]')
|
||||
.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
@@ -334,7 +328,15 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
await confirmRematchOptions();
|
||||
await flushAsyncTasks();
|
||||
|
||||
expect(showResultsSpy).toHaveBeenCalledWith(l4Matches);
|
||||
showResultsSpy.mockRestore();
|
||||
const summaryModal = document.getElementById('rematchSummaryModal');
|
||||
expect(summaryModal).not.toBeNull();
|
||||
// L4 matches to review force the warning header
|
||||
expect(summaryModal.querySelector('.summary-header').classList.contains('warning')).toBe(true);
|
||||
expect(summaryModal.querySelector('.stat-card-skipped .stat-card-value').textContent).toBe('1');
|
||||
const rows = summaryModal.querySelectorAll('tr[data-l4-index]');
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].textContent).toContain('old.safetensors');
|
||||
expect(rows[0].textContent).toContain('new.safetensors');
|
||||
expect(rows[0].querySelector('.rematch-undo-btn[data-action="undo-match"][data-index="0"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user