Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const STATUS_COLORS = {
Released: 'badge-released',
};

// Ordered list of statuses used for deterministic status-column sorting.
// A status not in this list sorts after all known statuses.
const STATUS_ORDER = Object.keys(STATUS_COLORS);

let invoices = [];
let sortField = 'id';
let sortAsc = true;
Expand Down Expand Up @@ -68,6 +72,12 @@ function render() {
if (sortField === 'id' || sortField === 'amount') {
va = Number(va);
vb = Number(vb);
} else if (sortField === 'status') {
// Sort by the defined STATUS_ORDER; unknown statuses sort to the end
const ia = STATUS_ORDER.indexOf(va);
const ib = STATUS_ORDER.indexOf(vb);
va = ia === -1 ? STATUS_ORDER.length : ia;
vb = ib === -1 ? STATUS_ORDER.length : ib;
}
if (va < vb) return sortAsc ? -1 : 1;
if (va > vb) return sortAsc ? 1 : -1;
Expand Down
114 changes: 114 additions & 0 deletions dashboard/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,117 @@ describe('fetchInvoices() error handling', () => {
expect(summary.querySelectorAll('.summary-card').length).toBe(7);
});
});

// ── Issue 2: Sortable Amount and Status columns ──────────────────────────────

/**
* Helper: click a column header by its data-sort value.
* The first click on a new field sorts ascending; a second click reverses.
* Requires loadAppScriptWithHandlers() to register th click listeners.
*/
function clickSortHeader(field) {
const th = document.querySelector(`thead th[data-sort="${field}"]`);
if (th) th.click();
}

/**
* Load app.js and dispatch DOMContentLoaded so that the header click handlers
* registered inside the DOMContentLoaded callback are wired up to the current
* DOM elements. Must be called after setupDOM().
*/
function loadAppScriptWithHandlers() {
loadAppScript();
// readyState is 'complete' in jsdom, so DOMContentLoaded never fires
// automatically; dispatch it manually to register th click listeners.
document.dispatchEvent(new Event('DOMContentLoaded'));
}

describe('sort by amount (numeric)', () => {
beforeEach(async () => {
setupDOM();
loadAppScriptWithHandlers();
await populateInvoices();
});

it('sorts invoices by amount ascending (numeric, not lexicographic)', () => {
clickSortHeader('amount');
const rows = document.querySelectorAll('#invoiceBody tr');
const amounts = Array.from(rows).map(r => {
const cell = r.querySelectorAll('td')[3];
return parseFloat(cell.textContent);
});
for (let i = 1; i < amounts.length; i++) {
expect(amounts[i]).toBeGreaterThanOrEqual(amounts[i - 1]);
}
});

it('sorts invoices by amount descending', () => {
clickSortHeader('amount'); // ascending
clickSortHeader('amount'); // toggle to descending
const rows = document.querySelectorAll('#invoiceBody tr');
const amounts = Array.from(rows).map(r => {
const cell = r.querySelectorAll('td')[3];
return parseFloat(cell.textContent);
});
for (let i = 1; i < amounts.length; i++) {
expect(amounts[i]).toBeLessThanOrEqual(amounts[i - 1]);
}
});

it('correctly orders amounts numerically: smallest first is 1200, largest is 8000', () => {
clickSortHeader('amount');
const rows = document.querySelectorAll('#invoiceBody tr');
const first = parseFloat(rows[0].querySelectorAll('td')[3].textContent);
const last = parseFloat(rows[rows.length - 1].querySelectorAll('td')[3].textContent);
expect(first).toBeLessThan(last);
expect(first).toBe(1200);
expect(last).toBe(8000);
});
});

describe('sort by status (defined STATUS_ORDER)', () => {
beforeEach(async () => {
setupDOM();
loadAppScriptWithHandlers();
await populateInvoices();
});

it('sorts invoices by status following STATUS_ORDER ascending', () => {
clickSortHeader('status');
const rows = document.querySelectorAll('#invoiceBody tr');
const statuses = Array.from(rows).map(r => {
const cell = r.querySelectorAll('td')[4];
const badge = cell.querySelector('.badge');
const text = badge ? badge.textContent.trim() : '';
return text === 'Refund Requested' ? 'RefundRequested' : text;
});
const ORDER = ['Pending', 'Paid', 'Expired', 'Cancelled', 'RefundRequested', 'Released'];
for (let i = 1; i < statuses.length; i++) {
const prev = ORDER.indexOf(statuses[i - 1]);
const curr = ORDER.indexOf(statuses[i]);
if (prev !== -1 && curr !== -1) {
expect(curr).toBeGreaterThanOrEqual(prev);
}
}
});

it('sorts invoices by status descending', () => {
clickSortHeader('status'); // ascending
clickSortHeader('status'); // toggle to descending
const rows = document.querySelectorAll('#invoiceBody tr');
const statuses = Array.from(rows).map(r => {
const cell = r.querySelectorAll('td')[4];
const badge = cell.querySelector('.badge');
const text = badge ? badge.textContent.trim() : '';
return text === 'Refund Requested' ? 'RefundRequested' : text;
});
const ORDER = ['Pending', 'Paid', 'Expired', 'Cancelled', 'RefundRequested', 'Released'];
for (let i = 1; i < statuses.length; i++) {
const prev = ORDER.indexOf(statuses[i - 1]);
const curr = ORDER.indexOf(statuses[i]);
if (prev !== -1 && curr !== -1) {
expect(curr).toBeLessThanOrEqual(prev);
}
}
});
});