diff --git a/docs/frontend-testing-roadmap.md b/docs/frontend-testing-roadmap.md index 50025cb7..952b5af8 100644 --- a/docs/frontend-testing-roadmap.md +++ b/docs/frontend-testing-roadmap.md @@ -9,8 +9,8 @@ This roadmap tracks the planned rollout of automated testing for the ComfyUI LoR | Phase 0 | Establish baseline tooling | Add Node test runner, jsdom environment, and seed smoke tests | ✅ Complete | Vitest + jsdom configured, example state tests committed | | Phase 1 | Cover state management logic | Unit test selectors, derived data helpers, and storage utilities under `static/js/state` and `static/js/utils` | ✅ Complete | Storage helpers and state selectors now exercised via deterministic suites | | Phase 2 | Test AppCore orchestration | Simulate page bootstrapping, infinite scroll hooks, and manager registration using JSDOM DOM fixtures | ✅ Complete | AppCore initialization + page feature suites now validate manager wiring, infinite scroll hooks, and onboarding gating | -| Phase 3 | Validate page-specific managers | Add focused suites for `loras`, `checkpoints`, `embeddings`, and `recipes` managers covering filtering, sorting, and bulk actions | 🚧 In Progress | LoRA + checkpoints smoke suites landed; filter/sort scenario matrix drafted to guide upcoming specs | -| Phase 4 | Interaction-level regression tests | Exercise template fragments, modals, and menus to ensure UI wiring remains intact | ⚪ Not Started | Evaluate Playwright component testing or happy-path DOM snapshots | +| Phase 3 | Validate page-specific managers | Add focused suites for `loras`, `checkpoints`, `embeddings`, and `recipes` managers covering filtering, sorting, and bulk actions | ✅ Complete | LoRA/checkpoint suites expanded; embeddings + recipes managers now covered with initialization, filtering, and duplicate workflows | +| Phase 4 | Interaction-level regression tests | Exercise template fragments, modals, and menus to ensure UI wiring remains intact | ✅ Complete | Vitest DOM suites cover NSFW selector, recipe modal editing, and global context menus | | Phase 5 | Continuous integration & coverage | Integrate frontend tests into CI workflow and track coverage metrics | ⚪ Not Started | Align reporting directories with backend coverage for unified reporting | ## Next Steps Checklist @@ -20,6 +20,7 @@ This roadmap tracks the planned rollout of automated testing for the ComfyUI LoR - [x] Prototype AppCore initialization test that verifies manager bootstrapping with stubbed dependencies. - [x] Add AppCore page feature suite exercising context menu creation and infinite scroll registration via DOM fixtures. - [x] Extend AppCore orchestration tests to cover manager wiring, bulk menu setup, and onboarding gating scenarios. +- [x] Add interaction regression suites for context menus and recipe modals to complete Phase 4. - [ ] Evaluate integrating coverage reporting once test surface grows (> 20 specs). - [x] Create shared fixtures for the loras and checkpoints pages once dedicated manager suites are added. - [x] Draft focused test matrix for loras/checkpoints manager filtering and sorting paths ahead of Phase 3. diff --git a/static/js/embeddings.js b/static/js/embeddings.js index 1352471c..654d250c 100644 --- a/static/js/embeddings.js +++ b/static/js/embeddings.js @@ -36,12 +36,18 @@ class EmbeddingsPageManager { } } -// Initialize everything when DOM is ready -document.addEventListener('DOMContentLoaded', async () => { +async function initializeEmbeddingsPage() { // Initialize core application await appCore.initialize(); - + // Initialize embeddings page const embeddingsPage = new EmbeddingsPageManager(); await embeddingsPage.initialize(); -}); + + return embeddingsPage; +} + +// Initialize everything when DOM is ready +document.addEventListener('DOMContentLoaded', initializeEmbeddingsPage); + +export { EmbeddingsPageManager, initializeEmbeddingsPage }; diff --git a/tests/frontend/components/contextMenu.interactions.test.js b/tests/frontend/components/contextMenu.interactions.test.js new file mode 100644 index 00000000..7dd18ad5 --- /dev/null +++ b/tests/frontend/components/contextMenu.interactions.test.js @@ -0,0 +1,319 @@ +import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; + +const showToastMock = vi.fn(); +const copyToClipboardMock = vi.fn(); +const getNSFWLevelNameMock = vi.fn((level) => { + if (level >= 16) return 'XXX'; + if (level >= 8) return 'X'; + if (level >= 4) return 'R'; + if (level >= 2) return 'PG13'; + if (level >= 1) return 'PG'; + return 'Unknown'; +}); +const copyLoraSyntaxMock = vi.fn(); +const sendLoraToWorkflowMock = vi.fn(); +const buildLoraSyntaxMock = vi.fn((fileName) => `lora:${fileName}`); +const openExampleImagesFolderMock = vi.fn(); + +const modalManagerMock = { + showModal: vi.fn(), + closeModal: vi.fn(), + registerModal: vi.fn(), + getModal: vi.fn(() => ({ element: { style: { display: 'none' } }, isOpen: false })), + isAnyModalOpen: vi.fn(), +}; + +const loadingManagerStub = { + showSimpleLoading: vi.fn(), + hide: vi.fn(), + show: vi.fn(), +}; + +const stateStub = { + global: { settings: {}, loadingManager: loadingManagerStub }, + loadingManager: loadingManagerStub, + virtualScroller: { updateSingleItem: vi.fn() }, +}; + +const saveModelMetadataMock = vi.fn(); +const downloadExampleImagesApiMock = vi.fn(); +const replaceModelPreviewMock = vi.fn(); +const refreshSingleModelMetadataMock = vi.fn(); +const resetAndReloadMock = vi.fn(); + +const getModelApiClientMock = vi.fn(() => ({ + saveModelMetadata: saveModelMetadataMock, + downloadExampleImages: downloadExampleImagesApiMock, + replaceModelPreview: replaceModelPreviewMock, + refreshSingleModelMetadata: refreshSingleModelMetadataMock, +})); + +const updateRecipeMetadataMock = vi.fn(() => Promise.resolve({ success: true })); + +vi.mock('../../../static/js/utils/uiHelpers.js', () => ({ + showToast: showToastMock, + copyToClipboard: copyToClipboardMock, + getNSFWLevelName: getNSFWLevelNameMock, + copyLoraSyntax: copyLoraSyntaxMock, + sendLoraToWorkflow: sendLoraToWorkflowMock, + buildLoraSyntax: buildLoraSyntaxMock, + openExampleImagesFolder: openExampleImagesFolderMock, +})); + +vi.mock('../../../static/js/managers/ModalManager.js', () => ({ + modalManager: modalManagerMock, +})); + +vi.mock('../../../static/js/utils/storageHelpers.js', () => ({ + setSessionItem: vi.fn(), + removeSessionItem: vi.fn(), + getSessionItem: vi.fn(), +})); + +vi.mock('../../../static/js/api/modelApiFactory.js', () => ({ + getModelApiClient: getModelApiClientMock, + resetAndReload: resetAndReloadMock, +})); + +vi.mock('../../../static/js/state/index.js', () => ({ + state: stateStub, +})); + +vi.mock('../../../static/js/utils/modalUtils.js', () => ({ + showExcludeModal: vi.fn(), + showDeleteModal: vi.fn(), +})); + +vi.mock('../../../static/js/managers/MoveManager.js', () => ({ + moveManager: { showMoveModal: vi.fn() }, +})); + +vi.mock('../../../static/js/api/recipeApi.js', () => ({ + updateRecipeMetadata: updateRecipeMetadataMock, +})); + +async function flushAsyncTasks() { + await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 0)); +} + +describe('Interaction-level regression coverage', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useRealTimers(); + document.body.innerHTML = ''; + stateStub.global.settings = {}; + saveModelMetadataMock.mockResolvedValue(undefined); + downloadExampleImagesApiMock.mockResolvedValue(undefined); + updateRecipeMetadataMock.mockResolvedValue({ success: true }); + global.modalManager = modalManagerMock; + }); + + afterEach(() => { + vi.useRealTimers(); + document.body.innerHTML = ''; + delete window.exampleImagesManager; + delete global.fetch; + delete global.modalManager; + }); + + it('opens the NSFW selector from the LoRA context menu and persists the new rating', async () => { + document.body.innerHTML = ` +
+
+
+ + `; + + const card = document.createElement('div'); + card.className = 'model-card'; + card.dataset.filepath = '/models/test.safetensors'; + card.dataset.meta = JSON.stringify({ preview_nsfw_level: 1 }); + document.body.appendChild(card); + + const { LoraContextMenu } = await import('../../../static/js/components/ContextMenu/LoraContextMenu.js'); + const helpers = await import('../../../static/js/utils/uiHelpers.js'); + expect(helpers.showToast).toBe(showToastMock); + const contextMenu = new LoraContextMenu(); + + contextMenu.showMenu(120, 140, card); + + const nsfwMenuItem = document.querySelector('#loraContextMenu .context-menu-item[data-action="set-nsfw"]'); + nsfwMenuItem.dispatchEvent(new Event('click', { bubbles: true })); + + const selector = document.getElementById('nsfwLevelSelector'); + expect(selector.style.display).toBe('block'); + expect(selector.dataset.cardPath).toBe('/models/test.safetensors'); + expect(document.getElementById('currentNSFWLevel').textContent).toBe('PG'); + + const levelButton = selector.querySelector('.nsfw-level-btn[data-level="4"]'); + levelButton.dispatchEvent(new Event('click', { bubbles: true })); + + expect(saveModelMetadataMock).toHaveBeenCalledWith('/models/test.safetensors', { preview_nsfw_level: 4 }); + expect(saveModelMetadataMock).toHaveBeenCalledTimes(1); + await saveModelMetadataMock.mock.results[0].value; + await flushAsyncTasks(); + expect(selector.style.display).toBe('none'); + expect(document.getElementById('loraContextMenu').style.display).toBe('none'); + }); + + it('wires recipe modal title editing to update metadata and UI state', async () => { + document.body.innerHTML = ` + + `; + + const { RecipeModal } = await import('../../../static/js/components/RecipeModal.js'); + const recipeModal = new RecipeModal(); + + const recipe = { + id: 'recipe-1', + file_path: '/recipes/test.json', + title: 'Original Title', + tags: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6'], + file_url: '', + preview_url: '', + source_path: '', + gen_params: { + prompt: 'Prompt text', + negative_prompt: 'Negative prompt', + steps: '30', + }, + loras: [], + }; + + recipeModal.showRecipeDetails(recipe); + await new Promise((resolve) => setTimeout(resolve, 60)); + await flushAsyncTasks(); + + expect(modalManagerMock.showModal).toHaveBeenCalledWith('recipeModal'); + + const editIcon = document.querySelector('#recipeModalTitle .edit-icon'); + editIcon.dispatchEvent(new Event('click', { bubbles: true })); + + const titleInput = document.querySelector('#recipeTitleEditor .title-input'); + titleInput.value = 'Updated Title'; + + recipeModal.saveTitleEdit(); + + expect(updateRecipeMetadataMock).toHaveBeenCalledWith('/recipes/test.json', { title: 'Updated Title' }); + expect(updateRecipeMetadataMock).toHaveBeenCalledTimes(1); + await updateRecipeMetadataMock.mock.results[0].value; + await flushAsyncTasks(); + + const titleContainer = document.getElementById('recipeModalTitle'); + expect(titleContainer.querySelector('.content-text').textContent).toBe('Updated Title'); + expect(titleContainer.querySelector('#recipeTitleEditor').classList.contains('active')).toBe(false); + expect(recipeModal.currentRecipe.title).toBe('Updated Title'); + }); + + it('processes global context menu actions for downloads and cleanup', async () => { + document.body.innerHTML = ` +
+
+
+
+ `; + + const { GlobalContextMenu } = await import('../../../static/js/components/ContextMenu/GlobalContextMenu.js'); + const menu = new GlobalContextMenu(); + + stateStub.global.settings.example_images_path = '/tmp/examples'; + window.exampleImagesManager = { + handleDownloadButton: vi.fn().mockResolvedValue(undefined), + }; + + menu.showMenu(100, 200); + const downloadItem = document.querySelector('[data-action="download-example-images"]'); + downloadItem.dispatchEvent(new Event('click', { bubbles: true })); + expect(downloadItem.classList.contains('disabled')).toBe(true); + + expect(window.exampleImagesManager.handleDownloadButton).toHaveBeenCalledTimes(1); + await window.exampleImagesManager.handleDownloadButton.mock.results[0].value; + await flushAsyncTasks(); + expect(downloadItem.classList.contains('disabled')).toBe(false); + expect(document.getElementById('globalContextMenu').style.display).toBe('none'); + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ success: true, moved_total: 2 }), + }); + + menu.showMenu(240, 320); + const cleanupItem = document.querySelector('[data-action="cleanup-example-images-folders"]'); + cleanupItem.dispatchEvent(new Event('click', { bubbles: true })); + expect(cleanupItem.classList.contains('disabled')).toBe(true); + + expect(global.fetch).toHaveBeenCalledWith('/api/lm/cleanup-example-image-folders', { method: 'POST' }); + expect(global.fetch).toHaveBeenCalledTimes(1); + const responsePromise = global.fetch.mock.results[0].value; + const response = await responsePromise; + await response.json(); + await flushAsyncTasks(); + expect(cleanupItem.classList.contains('disabled')).toBe(false); + expect(menu._cleanupInProgress).toBe(false); + }); +}); diff --git a/tests/frontend/pages/embeddingsPage.test.js b/tests/frontend/pages/embeddingsPage.test.js new file mode 100644 index 00000000..4d0f754f --- /dev/null +++ b/tests/frontend/pages/embeddingsPage.test.js @@ -0,0 +1,99 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { renderEmbeddingsPage } from '../utils/pageFixtures.js'; + +const initializeAppMock = vi.fn(); +const initializePageFeaturesMock = vi.fn(); +const createPageControlsMock = vi.fn(); +const confirmDeleteMock = vi.fn(); +const closeDeleteModalMock = vi.fn(); +const confirmExcludeMock = vi.fn(); +const closeExcludeModalMock = vi.fn(); +const duplicatesManagerMock = vi.fn(); + +vi.mock('../../../static/js/core.js', () => ({ + appCore: { + initialize: initializeAppMock, + initializePageFeatures: initializePageFeaturesMock, + }, +})); + +vi.mock('../../../static/js/components/controls/index.js', () => ({ + createPageControls: createPageControlsMock, +})); + +vi.mock('../../../static/js/utils/modalUtils.js', () => ({ + confirmDelete: confirmDeleteMock, + closeDeleteModal: closeDeleteModalMock, + confirmExclude: confirmExcludeMock, + closeExcludeModal: closeExcludeModalMock, +})); + +vi.mock('../../../static/js/api/apiConfig.js', () => ({ + MODEL_TYPES: { + EMBEDDING: 'embeddings', + }, +})); + +vi.mock('../../../static/js/components/ModelDuplicatesManager.js', () => ({ + ModelDuplicatesManager: duplicatesManagerMock, +})); + +describe('EmbeddingsPageManager', () => { + let EmbeddingsPageManager; + let initializeEmbeddingsPage; + let duplicatesManagerInstance; + + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + + duplicatesManagerInstance = { + checkDuplicatesCount: vi.fn(), + }; + + duplicatesManagerMock.mockReturnValue(duplicatesManagerInstance); + createPageControlsMock.mockReturnValue({ destroy: vi.fn() }); + initializeAppMock.mockResolvedValue(undefined); + + renderEmbeddingsPage(); + + ({ EmbeddingsPageManager, initializeEmbeddingsPage } = await import('../../../static/js/embeddings.js')); + }); + + afterEach(() => { + delete window.confirmDelete; + delete window.closeDeleteModal; + delete window.confirmExclude; + delete window.closeExcludeModal; + delete window.modelDuplicatesManager; + }); + + it('wires page controls and exposes modal helpers during construction', () => { + const manager = new EmbeddingsPageManager(); + + expect(createPageControlsMock).toHaveBeenCalledWith('embeddings'); + expect(duplicatesManagerMock).toHaveBeenCalledWith(manager, 'embeddings'); + + expect(window.confirmDelete).toBe(confirmDeleteMock); + expect(window.closeDeleteModal).toBe(closeDeleteModalMock); + expect(window.confirmExclude).toBe(confirmExcludeMock); + expect(window.closeExcludeModal).toBe(closeExcludeModalMock); + expect(window.modelDuplicatesManager).toBe(duplicatesManagerInstance); + }); + + it('initializes shared page features', async () => { + const manager = new EmbeddingsPageManager(); + + await manager.initialize(); + + expect(initializePageFeaturesMock).toHaveBeenCalledTimes(1); + }); + + it('boots the embeddings page through the initializer', async () => { + const manager = await initializeEmbeddingsPage(); + + expect(initializeAppMock).toHaveBeenCalledTimes(1); + expect(manager).toBeInstanceOf(EmbeddingsPageManager); + expect(window.modelDuplicatesManager).toBe(duplicatesManagerInstance); + }); +}); diff --git a/tests/frontend/pages/recipesPage.test.js b/tests/frontend/pages/recipesPage.test.js new file mode 100644 index 00000000..74add4e0 --- /dev/null +++ b/tests/frontend/pages/recipesPage.test.js @@ -0,0 +1,209 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { renderRecipesPage } from '../utils/pageFixtures.js'; + +const initializeAppMock = vi.fn(); +const initializePageFeaturesMock = vi.fn(); +const getCurrentPageStateMock = vi.fn(); +const getSessionItemMock = vi.fn(); +const removeSessionItemMock = vi.fn(); +const RecipeContextMenuMock = vi.fn(); +const refreshVirtualScrollMock = vi.fn(); +const refreshRecipesMock = vi.fn(); + +let importManagerInstance; +let recipeModalInstance; +let duplicatesManagerInstance; + +const ImportManagerMock = vi.fn(() => importManagerInstance); +const RecipeModalMock = vi.fn(() => recipeModalInstance); +const DuplicatesManagerMock = vi.fn(() => duplicatesManagerInstance); + +vi.mock('../../../static/js/core.js', () => ({ + appCore: { + initialize: initializeAppMock, + initializePageFeatures: initializePageFeaturesMock, + }, +})); + +vi.mock('../../../static/js/managers/ImportManager.js', () => ({ + ImportManager: ImportManagerMock, +})); + +vi.mock('../../../static/js/components/RecipeModal.js', () => ({ + RecipeModal: RecipeModalMock, +})); + +vi.mock('../../../static/js/state/index.js', () => ({ + getCurrentPageState: getCurrentPageStateMock, +})); + +vi.mock('../../../static/js/utils/storageHelpers.js', () => ({ + getSessionItem: getSessionItemMock, + removeSessionItem: removeSessionItemMock, +})); + +vi.mock('../../../static/js/components/ContextMenu/index.js', () => ({ + RecipeContextMenu: RecipeContextMenuMock, +})); + +vi.mock('../../../static/js/components/DuplicatesManager.js', () => ({ + DuplicatesManager: DuplicatesManagerMock, +})); + +vi.mock('../../../static/js/utils/infiniteScroll.js', () => ({ + refreshVirtualScroll: refreshVirtualScrollMock, +})); + +vi.mock('../../../static/js/api/recipeApi.js', () => ({ + refreshRecipes: refreshRecipesMock, +})); + +describe('RecipeManager', () => { + let RecipeManager; + let pageState; + + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + + importManagerInstance = { + showImportModal: vi.fn(), + }; + recipeModalInstance = { + showRecipeDetails: vi.fn(), + }; + duplicatesManagerInstance = { + findDuplicates: vi.fn(), + selectLatestDuplicates: vi.fn(), + deleteSelectedDuplicates: vi.fn(), + confirmDeleteDuplicates: vi.fn(), + exitDuplicateMode: vi.fn(), + }; + + pageState = { + sortBy: 'date', + searchOptions: undefined, + customFilter: undefined, + duplicatesMode: false, + }; + + getCurrentPageStateMock.mockImplementation(() => pageState); + initializeAppMock.mockResolvedValue(undefined); + initializePageFeaturesMock.mockResolvedValue(undefined); + refreshVirtualScrollMock.mockReset(); + refreshVirtualScrollMock.mockImplementation(() => {}); + refreshRecipesMock.mockResolvedValue('refreshed'); + + getSessionItemMock.mockImplementation((key) => { + const map = { + lora_to_recipe_filterLoraName: 'Flux Dream', + lora_to_recipe_filterLoraHash: 'abc123', + viewRecipeId: '42', + }; + return map[key] ?? null; + }); + removeSessionItemMock.mockImplementation(() => {}); + + renderRecipesPage(); + + ({ RecipeManager } = await import('../../../static/js/recipes.js')); + }); + + afterEach(() => { + delete window.recipeManager; + delete window.importManager; + }); + + it('initializes page controls, restores filters, and wires sort interactions', async () => { + const sortSelectElement = document.createElement('select'); + sortSelectElement.id = 'sortSelect'; + sortSelectElement.innerHTML = ` + + + `; + document.body.appendChild(sortSelectElement); + + const manager = new RecipeManager(); + await manager.initialize(); + + expect(ImportManagerMock).toHaveBeenCalledTimes(1); + expect(RecipeModalMock).toHaveBeenCalledTimes(1); + expect(DuplicatesManagerMock).toHaveBeenCalledWith(manager); + expect(RecipeContextMenuMock).toHaveBeenCalledTimes(1); + + expect(window.recipeManager).toBe(manager); + expect(window.importManager).toBe(importManagerInstance); + + expect(pageState.searchOptions).toEqual({ + title: true, + tags: true, + loraName: true, + loraModel: true, + }); + + expect(pageState.customFilter).toEqual({ + active: true, + loraName: 'Flux Dream', + loraHash: 'abc123', + recipeId: '42', + }); + + const indicator = document.getElementById('customFilterIndicator'); + expect(indicator.classList.contains('hidden')).toBe(false); + + const clearButton = indicator.querySelector('.clear-filter'); + clearButton.dispatchEvent(new Event('click', { bubbles: true })); + + expect(removeSessionItemMock).toHaveBeenCalledWith('lora_to_recipe_filterLoraName'); + expect(removeSessionItemMock).toHaveBeenCalledWith('lora_to_recipe_filterLoraHash'); + expect(removeSessionItemMock).toHaveBeenCalledWith('viewRecipeId'); + expect(pageState.customFilter.active).toBe(false); + expect(indicator.classList.contains('hidden')).toBe(true); + expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(1); + + const sortSelect = document.getElementById('sortSelect'); + sortSelect.value = 'name'; + sortSelect.dispatchEvent(new Event('change', { bubbles: true })); + + expect(pageState.sortBy).toBe('name'); + expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(2); + expect(initializePageFeaturesMock).toHaveBeenCalledTimes(1); + }); + + it('skips loading when duplicates mode is active and refreshes otherwise', async () => { + const manager = new RecipeManager(); + + pageState.duplicatesMode = true; + await manager.loadRecipes(); + expect(refreshVirtualScrollMock).not.toHaveBeenCalled(); + + pageState.duplicatesMode = false; + await manager.loadRecipes(); + expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(1); + }); + + it('proxies duplicate management and refresh helpers', async () => { + const manager = new RecipeManager(); + + await manager.findDuplicateRecipes(); + expect(duplicatesManagerInstance.findDuplicates).toHaveBeenCalledTimes(1); + + manager.selectLatestDuplicates(); + expect(duplicatesManagerInstance.selectLatestDuplicates).toHaveBeenCalledTimes(1); + + manager.deleteSelectedDuplicates(); + expect(duplicatesManagerInstance.deleteSelectedDuplicates).toHaveBeenCalledTimes(1); + + manager.confirmDeleteDuplicates(); + expect(duplicatesManagerInstance.confirmDeleteDuplicates).toHaveBeenCalledTimes(1); + + const grid = document.getElementById('recipeGrid'); + grid.innerHTML = '
content
'; + manager.exitDuplicateMode(); + expect(grid.innerHTML).toBe(''); + expect(duplicatesManagerInstance.exitDuplicateMode).toHaveBeenCalledTimes(1); + + await manager.refreshRecipes(); + expect(refreshRecipesMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/frontend/utils/pageFixtures.js b/tests/frontend/utils/pageFixtures.js index 7b4ed1b4..465ea8b3 100644 --- a/tests/frontend/utils/pageFixtures.js +++ b/tests/frontend/utils/pageFixtures.js @@ -23,3 +23,27 @@ export function renderCheckpointsPage() { }, }); } + +/** + * Renders the Embeddings page template with expected dataset attributes. + * @returns {Element} + */ +export function renderEmbeddingsPage() { + return renderTemplate('embeddings.html', { + dataset: { + page: 'embeddings', + }, + }); +} + +/** + * Renders the Recipes page template with expected dataset attributes. + * @returns {Element} + */ +export function renderRecipesPage() { + return renderTemplate('recipes.html', { + dataset: { + page: 'recipes', + }, + }); +}