diff --git a/src/components/common/DataTable.test.mjs b/src/components/common/DataTable.test.mjs index 4466aa9..47d7d96 100644 --- a/src/components/common/DataTable.test.mjs +++ b/src/components/common/DataTable.test.mjs @@ -1,9 +1,76 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' +import { parse, compileScript } from 'vue/compiler-sfc' +import ts from 'typescript' +import * as vue from 'vue' const source = readFileSync(new URL('./DataTable.vue', import.meta.url), 'utf8') +function paginationHarness(t) { + const descriptor = parse(source).descriptor + const compiled = compileScript(descriptor, { id: 'data-table-pagination-test' }) + const code = ts.transpileModule(compiled.content, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText + const exports = {} + const deps = { vue, '@lucide/vue': { ChevronLeft: {}, ChevronRight: {} }, './tableActions': {} } + new Function('require', 'exports', code)(id => { assert.ok(id in deps, id); return deps[id] }, exports) + const component = exports.default + component.render = () => null + const state = vue.reactive({ page: 5, total: 5542, loading: false, error: false }) + const emitted = [] + const renderer = vue.createRenderer({ + createElement: tag => ({ tag }), createText: text => ({ text }), createComment: text => ({ text }), + insert() {}, remove() {}, setElementText() {}, setText() {}, patchProp() {}, + parentNode: () => null, nextSibling: () => null, + }) + const app = renderer.createApp({ render: () => vue.h(component, { + data: [], columns: [], page: state.page, pageSize: 50, total: state.total, loading: state.loading, + 'onUpdate:page': value => { emitted.push(value); state.page = value }, + }, state.error ? { error: () => vue.h('div', 'Request failed') } : {}) }) + app.mount({}) + t.after(() => app.unmount()) + return { state, emitted } +} + +test('server-page loading must not reset the requested page when total temporarily clears', async t => { + const { state, emitted } = paginationHarness(t) + state.loading = true; state.total = 0 + await vue.nextTick() + assert.equal(state.page, 5) + assert.deepEqual(emitted, []) + state.total = 5542; state.loading = false + await vue.nextTick() + assert.equal(state.page, 5) + assert.deepEqual(emitted, []) +}) + +test('failed server page retains its page for explicit retry and emits no automatic fallback request', async t => { + const { state, emitted } = paginationHarness(t) + state.loading = true; state.total = 0 + await vue.nextTick() + state.error = true; state.loading = false + await vue.nextTick() + assert.equal(state.page, 5) + assert.deepEqual(emitted, []) + state.error = false; state.loading = true + await vue.nextTick() + state.total = 5542; state.loading = false + await vue.nextTick() + assert.equal(state.page, 5) + assert.deepEqual(emitted, []) +}) + +test('settled successful totals still clamp out-of-range and empty pages', async t => { + const { state, emitted } = paginationHarness(t) + state.total = 120 + await vue.nextTick() + assert.equal(state.page, 3) + state.total = 0 + await vue.nextTick() + assert.equal(state.page, 1) + assert.deepEqual(emitted, [3, 1]) +}) + test('keeps action-only table toolbars on the left', () => { assert.match(source, /'data-table__toolbar--actions-only': !\$slots\.toolbar/) assert.match(source, /v-if="\$slots\.toolbar" class="data-table__toolbar-heading"/) diff --git a/src/components/common/DataTable.vue b/src/components/common/DataTable.vue index 3ef382c..cc318d5 100644 --- a/src/components/common/DataTable.vue +++ b/src/components/common/DataTable.vue @@ -88,7 +88,9 @@ watch([pageSize, normalizedPageSizes], ([value, sizes]) => { page.value = 1 }, { immediate: true }) -watch([page, pageCount], ([value, count]) => { +watch([page, pageCount, () => props.loading, () => Boolean(slots.error)], ([value, count, loading, hasError]) => { + // A cleared total is not an authoritative page count while loading or showing a request error. + if (loading || hasError) return const nextValue = normalizePage(value, count) if (value !== nextValue) page.value = nextValue }, { immediate: true })