Fix some webapp tests
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
parent
6c46cf9be1
commit
bab85171ee
67 changed files with 355 additions and 175 deletions
|
|
@ -904,7 +904,7 @@ It is not meant to be used to serve files outside of the needs of GARM and it do
|
|||
title="Delete Object"
|
||||
message="Are you sure you want to delete the object '{selectedObject.name}'? This action cannot be undone."
|
||||
on:confirm={handleDeleteObject}
|
||||
on:cancel={() => {
|
||||
on:close={() => {
|
||||
showDeleteModal = false;
|
||||
selectedObject = null;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -424,6 +424,6 @@
|
|||
title="Delete Object"
|
||||
message="Are you sure you want to delete the object '{object.name}'? This action cannot be undone."
|
||||
on:confirm={handleDelete}
|
||||
on:cancel={() => showDeleteModal = false}
|
||||
on:close={() => showDeleteModal = false}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { createMockFileObject } from '../../../test/factories.js';
|
|||
vi.mock('$lib/api/client.js', () => ({
|
||||
garmApi: {
|
||||
getFileObject: vi.fn(),
|
||||
deleteFileObject: vi.fn()
|
||||
deleteFileObject: vi.fn(),
|
||||
updateFileObject: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
@ -17,6 +18,18 @@ vi.mock('$lib/stores/toast.js', () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/stores/websocket.js', () => ({
|
||||
websocketStore: {
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
subscribeToEntity: vi.fn(() => () => {})
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/utils/format', () => ({
|
||||
formatFileSize: vi.fn((size) => `${(size / 1024).toFixed(1)} KB`),
|
||||
formatDateTime: vi.fn((date) => date || 'N/A')
|
||||
}));
|
||||
|
||||
vi.mock('$app/stores', () => ({
|
||||
page: {
|
||||
subscribe: vi.fn((callback) => {
|
||||
|
|
@ -50,16 +63,13 @@ describe('Object Detail Page - Integration Tests', () => {
|
|||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate back when back button is clicked', async () => {
|
||||
const { goto } = await import('$app/navigation');
|
||||
|
||||
it('should have breadcrumb navigation link to objects page', async () => {
|
||||
render(ObjectDetailPage);
|
||||
await waitFor(() => screen.getByText('test-file.bin'));
|
||||
await waitFor(() => screen.getAllByText('test-file.bin'));
|
||||
|
||||
const backButton = screen.getByText('Back');
|
||||
await fireEvent.click(backButton);
|
||||
|
||||
expect(goto).toHaveBeenCalledWith('/objects');
|
||||
const objectStorageLink = screen.getByText('Object Storage');
|
||||
expect(objectStorageLink).toBeInTheDocument();
|
||||
expect(objectStorageLink.getAttribute('href')).toBe('/objects');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { createMockFileObject } from '../../../test/factories.js';
|
|||
vi.mock('$lib/api/client.js', () => ({
|
||||
garmApi: {
|
||||
getFileObject: vi.fn(),
|
||||
deleteFileObject: vi.fn()
|
||||
deleteFileObject: vi.fn(),
|
||||
updateFileObject: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
@ -17,6 +18,18 @@ vi.mock('$lib/stores/toast.js', () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/stores/websocket.js', () => ({
|
||||
websocketStore: {
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
subscribeToEntity: vi.fn(() => () => {})
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/utils/format', () => ({
|
||||
formatFileSize: vi.fn((size) => `${(size / 1024).toFixed(1)} KB`),
|
||||
formatDateTime: vi.fn((date) => date || 'N/A')
|
||||
}));
|
||||
|
||||
vi.mock('$app/stores', () => ({
|
||||
page: {
|
||||
subscribe: vi.fn((callback) => {
|
||||
|
|
@ -53,22 +66,23 @@ describe('Object Detail Page - Render Tests', () => {
|
|||
|
||||
describe('Basic Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(ObjectDetailPage);
|
||||
const { container} = render(ObjectDetailPage);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set page title', async () => {
|
||||
it('should render breadcrumb navigation', async () => {
|
||||
render(ObjectDetailPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(document.title).toBe('test-file.bin - GARM');
|
||||
expect(screen.getByText('Object Storage')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('test-file.bin').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render page header', async () => {
|
||||
it('should render file information section', async () => {
|
||||
render(ObjectDetailPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'test-file.bin' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'File Information' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -93,7 +107,7 @@ describe('Object Detail Page - Render Tests', () => {
|
|||
render(ObjectDetailPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(screen.getByText('test-file.bin')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('test-file.bin').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display formatted file size', async () => {
|
||||
|
|
@ -129,13 +143,6 @@ describe('Object Detail Page - Render Tests', () => {
|
|||
});
|
||||
|
||||
describe('Action Buttons', () => {
|
||||
it('should render back button', async () => {
|
||||
render(ObjectDetailPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(screen.getByText('Back')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render download button', async () => {
|
||||
render(ObjectDetailPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
|
@ -143,6 +150,13 @@ describe('Object Detail Page - Render Tests', () => {
|
|||
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render edit button', async () => {
|
||||
render(ObjectDetailPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render delete button', async () => {
|
||||
render(ObjectDetailPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
|
@ -171,7 +185,7 @@ describe('Object Detail Page - Render Tests', () => {
|
|||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Should show error message
|
||||
expect(screen.getByText(/Failed to load object/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/API Error/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle invalid object ID', async () => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ vi.mock('$lib/stores/toast.js', () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/stores/websocket.js', () => ({
|
||||
websocketStore: {
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
subscribeToEntity: vi.fn(() => () => {})
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$app/stores', () => ({}));
|
||||
vi.mock('$app/navigation', () => ({
|
||||
goto: vi.fn()
|
||||
|
|
@ -26,6 +33,11 @@ vi.mock('$app/paths', () => ({
|
|||
resolve: vi.fn((path: string) => path)
|
||||
}));
|
||||
|
||||
vi.mock('$lib/utils/format', () => ({
|
||||
formatFileSize: vi.fn((size) => `${(size / 1024).toFixed(1)} KB`),
|
||||
formatDateTime: vi.fn((date) => date || 'N/A')
|
||||
}));
|
||||
|
||||
const mockObject1 = createMockFileObject({
|
||||
id: 1,
|
||||
name: 'file1.bin',
|
||||
|
|
@ -53,22 +65,20 @@ describe('Objects Page - Integration Tests', () => {
|
|||
});
|
||||
|
||||
describe('Search Functionality', () => {
|
||||
it('should search objects when search button is clicked', async () => {
|
||||
it('should search objects when search term is entered', async () => {
|
||||
const { garmApi } = await import('$lib/api/client.js');
|
||||
|
||||
render(ObjectsPage);
|
||||
await waitFor(() => expect(garmApi.listFileObjects).toHaveBeenCalledTimes(1));
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search by tags/i);
|
||||
const searchButton = screen.getByRole('button', { name: 'Search' });
|
||||
const searchInput = screen.getByPlaceholderText(/Search by name or tags/i);
|
||||
|
||||
await fireEvent.input(searchInput, { target: { value: 'binary linux' } });
|
||||
await fireEvent.click(searchButton);
|
||||
|
||||
// Should call API with comma-separated tags
|
||||
// Should call API with comma-separated tags after debounce (500ms)
|
||||
await waitFor(() => {
|
||||
expect(garmApi.listFileObjects).toHaveBeenCalledWith('binary,linux', 1, 25);
|
||||
});
|
||||
}, { timeout: 1000 });
|
||||
});
|
||||
|
||||
it('should search when Enter key is pressed', async () => {
|
||||
|
|
@ -77,14 +87,14 @@ describe('Objects Page - Integration Tests', () => {
|
|||
render(ObjectsPage);
|
||||
await waitFor(() => expect(garmApi.listFileObjects).toHaveBeenCalledTimes(1));
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search by tags/i);
|
||||
const searchInput = screen.getByPlaceholderText(/Search by name or tags/i);
|
||||
|
||||
await fireEvent.input(searchInput, { target: { value: 'test' } });
|
||||
await fireEvent.keyDown(searchInput, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(garmApi.listFileObjects).toHaveBeenCalledWith('test', 1, 25);
|
||||
});
|
||||
}, { timeout: 1000 });
|
||||
});
|
||||
|
||||
it('should reset to page 1 when searching', async () => {
|
||||
|
|
@ -93,18 +103,16 @@ describe('Objects Page - Integration Tests', () => {
|
|||
render(ObjectsPage);
|
||||
await waitFor(() => expect(garmApi.listFileObjects).toHaveBeenCalled());
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search by tags/i);
|
||||
const searchButton = screen.getByRole('button', { name: 'Search' });
|
||||
const searchInput = screen.getByPlaceholderText(/Search by name or tags/i);
|
||||
|
||||
await fireEvent.input(searchInput, { target: { value: 'test' } });
|
||||
await fireEvent.click(searchButton);
|
||||
|
||||
// Should call with page 1
|
||||
// Should call with page 1 after debounce
|
||||
await waitFor(() => {
|
||||
const calls = (garmApi.listFileObjects as any).mock.calls;
|
||||
const lastCall = calls[calls.length - 1];
|
||||
expect(lastCall[1]).toBe(1); // page parameter
|
||||
});
|
||||
}, { timeout: 1000 });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -147,7 +155,7 @@ describe('Objects Page - Integration Tests', () => {
|
|||
await waitFor(() => screen.getByText('file1.bin'));
|
||||
|
||||
// Find and click delete button
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete object' });
|
||||
await fireEvent.click(deleteButtons[0]);
|
||||
|
||||
// Delete modal should appear
|
||||
|
|
@ -164,7 +172,7 @@ describe('Objects Page - Integration Tests', () => {
|
|||
await waitFor(() => screen.getByText('file1.bin'));
|
||||
|
||||
// Click delete button
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete object' });
|
||||
await fireEvent.click(deleteButtons[0]);
|
||||
|
||||
// Confirm deletion
|
||||
|
|
@ -188,7 +196,7 @@ describe('Objects Page - Integration Tests', () => {
|
|||
const initialCallCount = (garmApi.listFileObjects as any).mock.calls.length;
|
||||
|
||||
// Delete object
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete object' });
|
||||
await fireEvent.click(deleteButtons[0]);
|
||||
await waitFor(() => screen.getByText(/Are you sure/i));
|
||||
|
||||
|
|
@ -208,7 +216,7 @@ describe('Objects Page - Integration Tests', () => {
|
|||
await waitFor(() => screen.getByText('file1.bin'));
|
||||
|
||||
// Click update button
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update' });
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update object' });
|
||||
await fireEvent.click(updateButtons[0]);
|
||||
|
||||
// Update modal should appear
|
||||
|
|
@ -222,7 +230,7 @@ describe('Objects Page - Integration Tests', () => {
|
|||
await waitFor(() => screen.getByText('file1.bin'));
|
||||
|
||||
// Click update button for first object
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update' });
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update object' });
|
||||
await fireEvent.click(updateButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -239,12 +247,12 @@ describe('Objects Page - Integration Tests', () => {
|
|||
await waitFor(() => screen.getByText('file1.bin'));
|
||||
|
||||
// Open update modal
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update' });
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update object' });
|
||||
await fireEvent.click(updateButtons[0]);
|
||||
await waitFor(() => screen.getByText('Update Object'));
|
||||
|
||||
// Submit form
|
||||
const submitButton = screen.getAllByRole('button', { name: 'Update' })[1]; // Second one is in modal
|
||||
const submitButton = screen.getByRole('button', { name: 'Update' });
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
// Should call update API
|
||||
|
|
@ -303,7 +311,7 @@ describe('Objects Page - Integration Tests', () => {
|
|||
await waitFor(() => screen.getByText('file1.bin'));
|
||||
|
||||
// Try to delete
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
|
||||
const deleteButtons = screen.getAllByRole('button', { name: 'Delete object' });
|
||||
await fireEvent.click(deleteButtons[0]);
|
||||
await waitFor(() => screen.getByText(/Are you sure/i));
|
||||
|
||||
|
|
@ -331,11 +339,11 @@ describe('Objects Page - Integration Tests', () => {
|
|||
await waitFor(() => screen.getByText('file1.bin'));
|
||||
|
||||
// Open update modal and submit
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update' });
|
||||
const updateButtons = screen.getAllByRole('button', { name: 'Update object' });
|
||||
await fireEvent.click(updateButtons[0]);
|
||||
await waitFor(() => screen.getByText('Update Object'));
|
||||
|
||||
const submitButton = screen.getAllByRole('button', { name: 'Update' })[1];
|
||||
const submitButton = screen.getByRole('button', { name: 'Update' });
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
// Should show error toast
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { render, screen, waitFor } from '@testing-library/svelte';
|
||||
import ObjectsPage from './+page.svelte';
|
||||
import { createMockFileObject } from '../../test/factories.js';
|
||||
|
||||
|
|
@ -18,6 +18,13 @@ vi.mock('$lib/stores/toast.js', () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/stores/websocket.js', () => ({
|
||||
websocketStore: {
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
subscribeToEntity: vi.fn(() => () => {})
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock app stores
|
||||
vi.mock('$app/stores', () => ({}));
|
||||
vi.mock('$app/navigation', () => ({
|
||||
|
|
@ -52,40 +59,31 @@ describe('Objects Page - Render Tests', () => {
|
|||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set page title', () => {
|
||||
render(ObjectsPage);
|
||||
expect(document.title).toBe('Object Storage - GARM');
|
||||
});
|
||||
|
||||
it('should render page header with correct title', async () => {
|
||||
render(ObjectsPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
const { getByRole, getByText } = render(ObjectsPage);
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Object Storage' })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Manage files stored in GARM/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(getByRole('heading', { name: 'Object Storage' })).toBeInTheDocument();
|
||||
});
|
||||
expect(getByText(/Manage files stored in GARM/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render upload button', async () => {
|
||||
render(ObjectsPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(screen.getByText('Upload New Object')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload New Object')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search Functionality', () => {
|
||||
it('should render search input', async () => {
|
||||
render(ObjectsPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(screen.getByPlaceholderText(/Search by tags/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render search button', async () => {
|
||||
render(ObjectsPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(/Search by name or tags/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -101,9 +99,10 @@ describe('Objects Page - Render Tests', () => {
|
|||
|
||||
it('should display object name in table', async () => {
|
||||
render(ObjectsPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(screen.getByText('test-file.bin')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test-file.bin')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show empty state when no objects', async () => {
|
||||
|
|
@ -115,17 +114,20 @@ describe('Objects Page - Render Tests', () => {
|
|||
});
|
||||
|
||||
render(ObjectsPage);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(screen.getByText(/No objects found/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No objects found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modals', () => {
|
||||
it('should not show upload modal initially', () => {
|
||||
it('should not show upload modal initially', async () => {
|
||||
render(ObjectsPage);
|
||||
|
||||
expect(screen.queryByText('Upload New Object')).toBeInTheDocument(); // button
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload New Object')).toBeInTheDocument(); // button
|
||||
});
|
||||
expect(screen.queryByLabelText('File Name')).not.toBeInTheDocument(); // modal input
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ vi.mock('$lib/components/UpdateEntityModal.svelte', () => ({
|
|||
}));
|
||||
|
||||
vi.mock('$lib/components/DeleteModal.svelte', () => ({
|
||||
default: function MockDeleteModal(options: any) {
|
||||
const target = options.target;
|
||||
default: function MockDeleteModal(anchor: any, props: any) {
|
||||
const target = anchor?.parentNode;
|
||||
if (target) {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('data-testid', 'delete-modal');
|
||||
|
|
@ -75,21 +75,28 @@ vi.mock('$lib/components/DeleteModal.svelte', () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
// PageHeader is NOT mocked - use real component to support slots
|
||||
// Slots don't work properly with mocked Svelte components
|
||||
/*
|
||||
vi.mock('$lib/components/PageHeader.svelte', () => ({
|
||||
default: function MockPageHeader(options: any) {
|
||||
const target = options.target;
|
||||
default: function MockPageHeader(anchor: any, propsObj: any) {
|
||||
const target = anchor?.parentNode;
|
||||
if (target) {
|
||||
const div = document.createElement('div');
|
||||
// Extract title from props or use generic title
|
||||
const props = options.props || {};
|
||||
const title = props.title || 'Runner Instances';
|
||||
const showAction = props.showAction !== false;
|
||||
const actionText = props.actionText || 'Add';
|
||||
|
||||
let html = `<h1>${title}</h1>`;
|
||||
if (showAction) {
|
||||
html += `<button data-testid="add-button">${actionText}</button>`;
|
||||
}
|
||||
div.setAttribute('data-testid', 'page-header');
|
||||
|
||||
// Extract props
|
||||
const title = propsObj?.title || 'Runner Instances';
|
||||
const description = propsObj?.description || '';
|
||||
|
||||
let html = `
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">${title}</h1>
|
||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">${description}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
div.innerHTML = html;
|
||||
target.appendChild(div);
|
||||
}
|
||||
|
|
@ -100,31 +107,160 @@ vi.mock('$lib/components/PageHeader.svelte', () => ({
|
|||
};
|
||||
}
|
||||
}));
|
||||
*/
|
||||
|
||||
// NOTE: DataTable is commented out to allow real component in tests
|
||||
// This is necessary because reactive props don't work well with mocks
|
||||
/*
|
||||
vi.mock('$lib/components/DataTable.svelte', () => ({
|
||||
default: function MockDataTable(options: any) {
|
||||
const target = options.target;
|
||||
if (target) {
|
||||
const div = document.createElement('div');
|
||||
default: function MockDataTable(anchor: any, propsArg: any) {
|
||||
const target = anchor?.parentNode;
|
||||
const props = propsArg || {};
|
||||
let searchInput: HTMLInputElement | null = null;
|
||||
const eventListeners: Record<string, Array<(event: any) => void>> = {};
|
||||
|
||||
// Cache resolved prop values to detect changes
|
||||
let cachedData: any[] = [];
|
||||
let cachedLoading = false;
|
||||
let cachedError = '';
|
||||
|
||||
const resolveProps = () => {
|
||||
// Access getters to get current values
|
||||
const data = props.data || [];
|
||||
const loading = props.loading || false;
|
||||
const error = props.error || '';
|
||||
|
||||
// Check if props changed
|
||||
if (data !== cachedData || data.length !== cachedData.length ||
|
||||
loading !== cachedLoading || error !== cachedError) {
|
||||
cachedData = data;
|
||||
cachedLoading = loading;
|
||||
cachedError = error;
|
||||
return { changed: true, data, loading, error };
|
||||
}
|
||||
return { changed: false, data, loading, error };
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (!target) return;
|
||||
|
||||
const div = target.querySelector('[data-testid="data-table"]') || document.createElement('div');
|
||||
div.setAttribute('data-testid', 'data-table');
|
||||
|
||||
// Extract search placeholder from props
|
||||
const props = options.props || {};
|
||||
|
||||
// Access getters to get current values
|
||||
const { data, loading, error } = resolveProps();
|
||||
const searchPlaceholder = props.searchPlaceholder || 'Search...';
|
||||
|
||||
div.innerHTML = `
|
||||
<div>DataTable Component</div>
|
||||
<input type="search" placeholder="${searchPlaceholder}" />
|
||||
`;
|
||||
target.appendChild(div);
|
||||
}
|
||||
const columns = props.columns || [];
|
||||
const showSearch = props.showSearch !== false;
|
||||
const searchType = props.searchType || 'client';
|
||||
|
||||
// Create table structure
|
||||
let html = '<div>';
|
||||
|
||||
// Search bar
|
||||
if (showSearch) {
|
||||
html += `<div data-testid="${searchType}-search-bar">`;
|
||||
html += `<input type="search" placeholder="${searchPlaceholder}" data-testid="search-input" class="search-input" />`;
|
||||
if (searchType === 'backend') {
|
||||
html += `<button class="search-button">Search</button>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
html += '<div data-testid="loading-state">Loading...</div>';
|
||||
}
|
||||
// Error state
|
||||
else if (error) {
|
||||
html += `<div data-testid="error-state">${error}</div>`;
|
||||
}
|
||||
// Table with data
|
||||
else if (data.length > 0 && columns.length > 0) {
|
||||
html += '<table data-testid="data-table-table"><thead><tr>';
|
||||
columns.forEach((col: any) => {
|
||||
html += `<th>${col.title || col.key}</th>`;
|
||||
});
|
||||
html += '</tr></thead><tbody>';
|
||||
data.forEach((item: any, index: number) => {
|
||||
html += `<tr data-testid="table-row-${index}">`;
|
||||
columns.forEach((col: any) => {
|
||||
const value = item[col.key] || '';
|
||||
html += `<td>${typeof value === 'object' ? JSON.stringify(value) : value}</td>`;
|
||||
});
|
||||
// Add action buttons if column has actions
|
||||
if (columns.some((c: any) => c.key === 'actions')) {
|
||||
html += `<td><button data-action="delete" data-index="${index}">Delete</button></td>`;
|
||||
}
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
// Empty state
|
||||
else if (!loading && !error) {
|
||||
html += '<div data-testid="empty-state">No objects found</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
div.innerHTML = html;
|
||||
|
||||
if (!div.parentNode) {
|
||||
target.appendChild(div);
|
||||
}
|
||||
|
||||
// Attach event listeners to search input
|
||||
searchInput = div.querySelector('.search-input') as HTMLInputElement;
|
||||
if (searchInput && eventListeners.search) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const term = (e.target as HTMLInputElement).value;
|
||||
eventListeners.search?.forEach(cb => cb({ detail: { term } }));
|
||||
});
|
||||
}
|
||||
|
||||
// Attach event listeners to delete buttons
|
||||
const deleteButtons = div.querySelectorAll('[data-action="delete"]');
|
||||
deleteButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const index = parseInt((e.target as HTMLElement).getAttribute('data-index') || '0');
|
||||
const item = data[index];
|
||||
eventListeners.delete?.forEach(cb => cb({ detail: { item } }));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial render
|
||||
renderContent();
|
||||
|
||||
return {
|
||||
$destroy: vi.fn(),
|
||||
$set: vi.fn(),
|
||||
$on: vi.fn()
|
||||
$destroy: vi.fn(() => {
|
||||
if (target) {
|
||||
const div = target.querySelector('[data-testid="data-table"]');
|
||||
if (div) div.remove();
|
||||
}
|
||||
}),
|
||||
$set: vi.fn((newProps: any) => {
|
||||
console.log('[DataTable $set] newProps:', newProps);
|
||||
console.log('[DataTable $set] newProps.data:', newProps?.data);
|
||||
console.log('[DataTable $set] newProps.loading:', newProps?.loading);
|
||||
Object.assign(props, newProps);
|
||||
renderContent();
|
||||
}),
|
||||
$on: vi.fn((event: string, callback: (e: any) => void) => {
|
||||
if (!eventListeners[event]) {
|
||||
eventListeners[event] = [];
|
||||
}
|
||||
eventListeners[event].push(callback);
|
||||
return () => {
|
||||
const index = eventListeners[event]?.indexOf(callback);
|
||||
if (index !== undefined && index > -1) {
|
||||
eventListeners[event].splice(index, 1);
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}));
|
||||
*/
|
||||
|
||||
// Mock cell components
|
||||
vi.mock('$lib/components/cells', () => ({
|
||||
|
|
@ -187,5 +323,15 @@ vi.mock('$lib/components/cells', () => ({
|
|||
target.appendChild(div);
|
||||
}
|
||||
return { $destroy: vi.fn(), $set: vi.fn(), $on: vi.fn() };
|
||||
},
|
||||
TagsCell: function MockTagsCell(options: any) {
|
||||
const target = options.target;
|
||||
if (target) {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('data-testid', 'tags-cell');
|
||||
div.textContent = 'Tags Cell';
|
||||
target.appendChild(div);
|
||||
}
|
||||
return { $destroy: vi.fn(), $set: vi.fn(), $on: vi.fn() };
|
||||
}
|
||||
}));
|
||||
Loading…
Add table
Add a link
Reference in a new issue