-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
sortable
action use data-sort-value
HTML attributes for sorting w…
…hen present (#16) * sortable action ignores formatted numbers as strings when table cells provide data-sort-value HTML attribute for sorting - Implement data-sort-value attribute support for more robust ordering - Add get_html_sort_value function for recursive sort value extraction - Introduce configurable CSS classes and styles for sorted headers - Improve sorting logic to handle empty values and numeric comparisons * add sortable unit tests * more unit tests in actions/highlight-matches.test.ts * skip eslint in pre-commit.ci + only deploy to GH pages on push to main
- Loading branch information
Showing
6 changed files
with
298 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,6 @@ | ||
name: GitHub Pages | ||
|
||
on: | ||
pull_request: | ||
branches: [main] | ||
push: | ||
branches: [main] | ||
workflow_dispatch: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
ci: | ||
autoupdate_schedule: quarterly | ||
skip: [eslint] | ||
|
||
default_stages: [commit] | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { highlight_matches } from '$lib/actions' | ||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' | ||
|
||
const mockHighlightsSet = vi.fn() | ||
const mockHighlightsClear = vi.fn() | ||
global.CSS = { | ||
highlights: { set: mockHighlightsSet, clear: mockHighlightsClear }, | ||
} as unknown | ||
|
||
const RangeMock = vi | ||
.fn() | ||
.mockImplementation(() => ({ setStart: vi.fn(), setEnd: vi.fn() })) | ||
global.Range = RangeMock as unknown | ||
global.Highlight = class {} as unknown | ||
|
||
describe(`highlight_matches`, () => { | ||
let testNode: HTMLElement | ||
|
||
beforeEach(() => { | ||
testNode = document.createElement(`div`) | ||
testNode.innerHTML = `<p>This is a test paragraph with some text to highlight.</p>` | ||
document.body.appendChild(testNode) | ||
}) | ||
|
||
afterEach(() => { | ||
document.body.removeChild(testNode) | ||
vi.clearAllMocks() | ||
}) | ||
|
||
test.each([ | ||
[`applies highlights correctly`, { query: `test` }, true, 1], | ||
[ | ||
`does not apply highlights when disabled`, | ||
{ query: `test`, disabled: true }, | ||
false, | ||
0, | ||
], | ||
[ | ||
`uses custom CSS class`, | ||
{ query: `test`, css_class: `custom-highlight` }, | ||
true, | ||
1, | ||
`custom-highlight`, | ||
], | ||
[`handles empty query`, { query: `` }, false, 0], | ||
])( | ||
`%s`, | ||
(_, options, shouldSet, setCallCount, cssClass = `highlight-match`) => { | ||
highlight_matches(testNode, options) | ||
expect(mockHighlightsClear).toHaveBeenCalled() | ||
if (shouldSet) { | ||
expect(mockHighlightsSet).toHaveBeenCalledWith( | ||
cssClass, | ||
expect.any(Highlight), | ||
) | ||
} else expect(mockHighlightsSet).not.toHaveBeenCalled() | ||
expect(mockHighlightsSet).toHaveBeenCalledTimes(setCallCount) | ||
}, | ||
) | ||
|
||
test(`handles node_filter`, () => { | ||
const nodeFilter = vi.fn().mockReturnValue(NodeFilter.FILTER_ACCEPT) | ||
highlight_matches(testNode, { query: `test`, node_filter: nodeFilter }) | ||
expect(nodeFilter).toHaveBeenCalled() | ||
expect(mockHighlightsSet).toHaveBeenCalled() | ||
}) | ||
|
||
test(`update function works correctly`, () => { | ||
const action = highlight_matches(testNode, { query: `test` }) | ||
action.update({ query: `paragraph` }) | ||
expect(mockHighlightsClear).toHaveBeenCalledTimes(2) | ||
expect(mockHighlightsSet).toHaveBeenCalledTimes(2) | ||
}) | ||
|
||
test.each([ | ||
[`handles multiple occurrences`, `<p>Test test test</p>`, 3], | ||
[`is case insensitive`, `<p>Test TEST test</p>`, 3], | ||
])(`%s`, (_, innerHTML, expectedRanges) => { | ||
testNode.innerHTML = innerHTML | ||
highlight_matches(testNode, { query: `test` }) | ||
expect(mockHighlightsSet).toHaveBeenCalledWith( | ||
`highlight-match`, | ||
expect.any(Highlight), | ||
) | ||
expect(RangeMock).toHaveBeenCalledTimes(expectedRanges) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import { get_html_sort_value, sortable } from '$lib/actions' | ||
import { afterEach, beforeEach, describe, expect, test } from 'vitest' | ||
|
||
function make_table(rows: string[][]): HTMLTableElement { | ||
const table = document.createElement(`table`) | ||
table.innerHTML = ` | ||
<thead> | ||
<tr>${rows[0].map((cell) => `<th>${cell}</th>`).join(``)}</tr> | ||
</thead> | ||
<tbody> | ||
${rows | ||
.slice(1) | ||
.map( | ||
(row) => ` | ||
<tr>${row.map((cell) => `<td>${cell}</td>`).join(``)}</tr> | ||
`, | ||
) | ||
.join(``)} | ||
</tbody> | ||
` | ||
return table | ||
} | ||
|
||
function get_sorted_values( | ||
table: HTMLTableElement, | ||
column_index: number, | ||
): string[] { | ||
return Array.from(table.querySelectorAll(`tbody tr`)).map( | ||
(row) => row.cells[column_index].textContent || ``, | ||
) | ||
} | ||
|
||
function click_header(table: HTMLTableElement, column_index: number) { | ||
const header = table.querySelector( | ||
`th:nth-child(${column_index + 1})`, | ||
) as HTMLTableCellElement | ||
header.click() | ||
} | ||
|
||
let table: HTMLTableElement | ||
|
||
beforeEach(() => { | ||
table = make_table([ | ||
[`Name`, `Age`, `Score`], | ||
[`Alice`, `30`, `95`], | ||
[`Bob`, `25`, `88`], | ||
[`Charlie`, `35`, `92`], | ||
]) | ||
document.body.appendChild(table) | ||
}) | ||
|
||
afterEach(() => { | ||
document.body.removeChild(table) | ||
}) | ||
|
||
describe(`get_html_sort_value`, () => { | ||
test.each([ | ||
[`<span data-sort-value="10">Ten</span>`, `10`], | ||
[`<span>No sort value</span>`, `No sort value`], | ||
[`<span><em data-sort-value="20">Twenty</em></span>`, `20`], | ||
[``, ``], | ||
])(`returns correct value for %s`, (html, expected) => { | ||
const element = document.createElement(`div`) | ||
element.innerHTML = html | ||
expect(get_html_sort_value(element)).toBe(expected) | ||
}) | ||
}) | ||
|
||
describe(`sortable function`, () => { | ||
test(`sorts table correctly`, () => { | ||
sortable(table) | ||
|
||
click_header(table, 1) | ||
expect(get_sorted_values(table, 1)).toEqual([`25`, `30`, `35`]) | ||
|
||
click_header(table, 1) | ||
expect(get_sorted_values(table, 1)).toEqual([`35`, `30`, `25`]) | ||
}) | ||
|
||
test(`handles empty values correctly`, () => { | ||
table = make_table([ | ||
[`Name`, `Age`], | ||
[`Alice`, `30`], | ||
[`Bob`, ``], | ||
[`Charlie`, `25`], | ||
]) | ||
document.body.replaceChild(table, document.body.firstChild as Node) | ||
|
||
sortable(table) | ||
click_header(table, 1) | ||
|
||
expect(get_sorted_values(table, 1)).toEqual([`25`, `30`, ``]) | ||
}) | ||
|
||
test(`handles data-sort-value attribute`, () => { | ||
table = make_table([ | ||
[`Name`, `Score`], | ||
[`Alice`, `<span data-sort-value="95">A</span>`], | ||
[`Bob`, `<span data-sort-value="88">B</span>`], | ||
[`Charlie`, `<span data-sort-value="92">A-</span>`], | ||
]) | ||
document.body.replaceChild(table, document.body.firstChild as Node) | ||
|
||
sortable(table) | ||
click_header(table, 1) | ||
|
||
expect(get_sorted_values(table, 1)).toEqual([`B`, `A-`, `A`]) | ||
}) | ||
|
||
test(`handles zero values correctly`, () => { | ||
table = make_table([ | ||
[`Name`, `Score`], | ||
[`Alice`, `0`], | ||
[`Bob`, `10`], | ||
[`Charlie`, ``], | ||
]) | ||
document.body.replaceChild(table, document.body.firstChild as Node) | ||
|
||
sortable(table) | ||
click_header(table, 1) | ||
|
||
expect(get_sorted_values(table, 1)).toEqual([`0`, `10`, ``]) | ||
}) | ||
|
||
test(`updates header styles correctly`, () => { | ||
sortable(table) | ||
const header = table.querySelector( | ||
`th:nth-child(2)`, | ||
) as HTMLTableCellElement | ||
|
||
header.click() | ||
expect(header.classList.contains(`table-sort-asc`)).toBe(true) | ||
expect(header.style.backgroundColor).toBe(`rgba(255, 255, 255, 0.1)`) | ||
expect(header.textContent).toContain(`↑`) | ||
|
||
header.click() | ||
expect(header.classList.contains(`table-sort-desc`)).toBe(true) | ||
expect(header.textContent).toContain(`↓`) | ||
}) | ||
|
||
test(`applies custom classes and styles`, () => { | ||
sortable(table, { | ||
asc_class: `custom-asc`, | ||
desc_class: `custom-desc`, | ||
sorted_style: { color: `red`, fontWeight: `bold` }, | ||
}) | ||
const header = table.querySelector( | ||
`th:nth-child(2)`, | ||
) as HTMLTableCellElement | ||
|
||
header.click() | ||
expect(header.classList.contains(`custom-asc`)).toBe(true) | ||
expect(header.style.color).toBe(`red`) | ||
expect(header.style.fontWeight).toBe(`bold`) | ||
|
||
header.click() | ||
expect(header.classList.contains(`custom-desc`)).toBe(true) | ||
expect(header.style.color).toBe(`red`) | ||
expect(header.style.fontWeight).toBe(`bold`) | ||
}) | ||
}) |