Skip to content

Commit

Permalink
SPSH-1368 (#350)
Browse files Browse the repository at this point in the history
* Tab navigation for the result table

* Added a unit test
  • Loading branch information
YoussefBouch authored Nov 28, 2024
1 parent 9a157eb commit 67bcb1c
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 11 deletions.
99 changes: 89 additions & 10 deletions src/components/admin/ResultTable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
import { expect, test } from 'vitest';
import { VueWrapper, mount } from '@vue/test-utils';
import { expect, test, describe, beforeEach } from 'vitest';
import { DOMWrapper, VueWrapper, mount } from '@vue/test-utils';
import ResultTable from './ResultTable.vue';
import type { VDataTableServer } from 'vuetify/lib/components/index.mjs';
import type { VueNode } from '@vue/test-utils/dist/types';

let wrapper: VueWrapper | null = null;

const mockItems: {
id: number;
name: string;
}[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
];

beforeEach(() => {
document.body.innerHTML = `
<div>
<div id="app"></div>
</div>
`;
`;

type ReadonlyHeaders = InstanceType<typeof VDataTableServer>['headers'];
const headers: ReadonlyHeaders = [{ title: 'title', key: 'key', align: 'start' }];
const headers: ReadonlyHeaders = [{ title: 'Name', key: 'name', align: 'start' }];

wrapper = mount(ResultTable, {
attachTo: document.getElementById('app') || '',
props: {
items: [],
items: mockItems,
itemsPerPage: 10,
loading: false,
password: 'qwertzuiop',
totalItems: 25,
headers: headers,
header: 'header',
itemValuePath: 'id',
disableRowClick: false,
},
Expand All @@ -35,8 +44,78 @@ beforeEach(() => {
});
});

describe('result table', () => {
test('it renders the result table', () => {
expect(wrapper?.get('[data-testid="result-table"]')).not.toBeNull();
describe('Row Index and Item Retrieval', () => {
test('manually find row index in DOM and match corresponding item', () => {
// Simulate getting rows and finding index
const tbody: DOMWrapper<HTMLTableSectionElement> | undefined = wrapper?.find('tbody');
const rows: DOMWrapper<HTMLTableRowElement>[] | undefined = tbody?.findAll('tr');

// Verify we have correct number of rows matching mock items
expect(rows?.length).toBe(mockItems.length);

rows?.forEach((row: DOMWrapper<HTMLTableRowElement>, domIndex: number) => {
// Convert row to raw DOM element
const rowElement: VueNode<HTMLTableRowElement> = row.element;

// Simulate finding parent and converting to array
const rowsArray: Element[] = Array.from(rowElement.parentElement!.children);

// Find index of current row
const calculatedIndex: number = rowsArray.indexOf(rowElement);

// Verify calculated index matches expected DOM index
expect(calculatedIndex).toBe(domIndex);

// Verify item retrieval matches our mock data
// This simulates the logic in the component
const retrievedItem:
| {
id: number;
name: string;
}
| undefined = mockItems[calculatedIndex];
expect(retrievedItem).toEqual(mockItems[domIndex]);
});
});

test('row index matches item array order', () => {
const tbody: DOMWrapper<HTMLTableSectionElement> | undefined = wrapper?.find('tbody');
const rows: DOMWrapper<HTMLTableRowElement>[] | undefined = tbody?.findAll('tr');

rows?.forEach((row: DOMWrapper<HTMLTableRowElement>, index: number) => {
const rowElement: VueNode<HTMLTableRowElement> = row.element;
const rowsArray: Element[] = Array.from(rowElement.parentElement!.children);
const calculatedIndex: number = rowsArray.indexOf(rowElement);

expect(calculatedIndex).toBe(index);
});
});
test('handles empty items array', () => {
// Remount with empty items
wrapper = mount(ResultTable, {
props: {
items: [],
itemsPerPage: 10,
loading: false,
totalItems: 0,
headers: [{ title: 'Name', key: 'name', align: 'start' }],
itemValuePath: 'id',
disableRowClick: false,
},
});

const tbody: DOMWrapper<HTMLTableSectionElement> = wrapper.find('tbody');
const rows: DOMWrapper<HTMLTableRowElement>[] = tbody.findAll('tr');

// Use the prop passed to the component to check for no data
const noDataText: string | undefined = wrapper.find('[data-testid="result-table"]').attributes('no-data-text');

// Either no rows or only a single "no data" row
expect(rows.length).toBeLessThanOrEqual(1);

// Optional: Check for specific no data text if needed
if (rows.length === 1) {
expect(rows[0]?.text()).toContain(noDataText || 'Keine Daten gefunden.');
}
});
});
40 changes: 39 additions & 1 deletion src/components/admin/ResultTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import { SortOrder } from '@/stores/PersonStore';
import { useSearchFilterStore, type SearchFilterStore } from '@/stores/SearchFilterStore';
import { onMounted } from 'vue';
import { onMounted, onUnmounted } from 'vue';
import type { VDataTableServer } from 'vuetify/lib/components/index.mjs';
const searchFilterStore: SearchFilterStore = useSearchFilterStore();
Expand Down Expand Up @@ -46,6 +46,39 @@
const emit: Emits = defineEmits<Emits>();
function handleKeyDown(event: KeyboardEvent): void {
// Check if the pressed key is Enter
if (event.key === 'Enter') {
const target: HTMLElement = event.target as HTMLElement;
// Check if the target or its closest parent is a header checkbox, if so then we shouldn't trigger anything when "Enter" is pressed as it's not a table row.
const isHeaderCheckbox: boolean =
target.closest('thead')?.querySelector('input[type="checkbox"]') === target ||
target.querySelector('input[type="checkbox"]') !== null;
// If it's a header checkbox, do nothing
if (isHeaderCheckbox) {
return;
}
const row: HTMLTableRowElement | null = target.closest('tr');
if (row) {
// Find the corresponding item for this row.
const rowIndex: number = Array.from(row.parentElement!.children).indexOf(row);
const item: TableItem | undefined = props.items[rowIndex];
if (item) {
// Prevent default Enter key behavior
event.preventDefault();
// Emit the same event as handleRowClick
emit('onHandleRowClick', event as unknown as PointerEvent, { item });
}
}
}
}
function handleRowClick(event: PointerEvent, item: TableRow<unknown>): void {
if (!props.disableRowClick) {
emit('onHandleRowClick', event, item);
Expand Down Expand Up @@ -94,6 +127,11 @@
});
}
}
window.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown);
});
</script>

Expand Down

0 comments on commit 67bcb1c

Please sign in to comment.