You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/09/15 14:53:12 UTC
(juneau) branch master updated: Call draw(false) on paging-pill first/previous/next/last so DataTables 2.1.8 does not reset the page (WORK-J0537).
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 50803695b4 Call draw(false) on paging-pill first/previous/next/last so DataTables 2.1.8 does not reset the page (WORK-J0537).
50803695b4 is described below
commit 50803695b4988a55ca3ef21071ee9b15e6667fea
Author: James Bognar <ja...@apache.org>
AuthorDate: Tue Sep 15 10:53:06 2026 -0400
Call draw(false) on paging-pill first/previous/next/last so DataTables 2.1.8 does not reset the page (WORK-J0537).
---
.../org/apache/juneau/views/juneau-views.js | 10 +-
.../rest/server/views/PagingPill_Wiring_Test.java | 24 +++
.../server/views/ViewsJs_PagingPillDraw_Test.java | 180 +++++++++++++++++++++
.../src/test/js/paging-pill-draw.cjs | 164 +++++++++++++++++++
4 files changed, 374 insertions(+), 4 deletions(-)
diff --git a/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.js b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.js
index 5c723c3f7a..2039958690 100644
--- a/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.js
+++ b/juneau-rest/juneau-rest-server-views/src/main/resources/org/apache/juneau/views/juneau-views.js
@@ -1077,11 +1077,13 @@
pill.className = "juneau-view-pagingpill";
pill.dataset.testid = "paging";
- const firstBtn = pagingPillButton("First page", "first_page", function () { ctx.dataTable.page("first").draw(); });
- const prevBtn = pagingPillButton("Previous page", "chevron_left", function () { ctx.dataTable.page("previous").draw(); });
+ // draw() defaults to resetPaging=true (DataTables 2.1.8), which would jump back to page 0 after
+ // an explicit page("next") / first / previous / last. false holds the page those calls just set.
+ const firstBtn = pagingPillButton("First page", "first_page", function () { ctx.dataTable.page("first").draw(false); });
+ const prevBtn = pagingPillButton("Previous page", "chevron_left", function () { ctx.dataTable.page("previous").draw(false); });
const sizeMenu = buildPageSizeMenu(ctx);
- const nextBtn = pagingPillButton("Next page", "chevron_right", function () { ctx.dataTable.page("next").draw(); });
- const lastBtn = pagingPillButton("Last page", "last_page", function () { ctx.dataTable.page("last").draw(); });
+ const nextBtn = pagingPillButton("Next page", "chevron_right", function () { ctx.dataTable.page("next").draw(false); });
+ const lastBtn = pagingPillButton("Last page", "last_page", function () { ctx.dataTable.page("last").draw(false); });
pill.appendChild(firstBtn);
pill.appendChild(prevBtn);
pill.appendChild(sizeMenu.el);
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java
index 9b437dcdbf..3bcfdb7ec9 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PagingPill_Wiring_Test.java
@@ -127,6 +127,30 @@ class PagingPill_Wiring_Test extends TestBase {
assertFalse(fnBody.contains("btn.title"), fnBody);
}
+ /**
+ * DataTables 2.1.8 {@code draw()} defaults to {@code resetPaging=true}, which jumps {@code _iDisplayStart}
+ * back to 0 after an explicit {@code page("next")} (and first/previous/last). The four nav handlers must
+ * hold the current page with {@code draw(false)}. Page-size changes keep the default {@code draw()} so a
+ * length change still returns to page 0.
+ */
+ @Test void b05_buildPagingPill_navDrawHoldsCurrentPage_pageSizeDrawResets() throws Exception {
+ var body = cWithMixin.get(ViewsMixin.VIEWS_JS_PATH).run().assertStatus(200).getContent().asString();
+ var pillFnBody = functionBody(body, "function buildPagingPill(");
+ assertTrue(pillFnBody.contains(".page(\"first\").draw(false)"), pillFnBody);
+ assertTrue(pillFnBody.contains(".page(\"previous\").draw(false)"), pillFnBody);
+ assertTrue(pillFnBody.contains(".page(\"next\").draw(false)"), pillFnBody);
+ assertTrue(pillFnBody.contains(".page(\"last\").draw(false)"), pillFnBody);
+ // The no-arg form ends `draw();` — `draw(false);` must not satisfy these.
+ assertFalse(pillFnBody.contains(".page(\"first\").draw();"), pillFnBody);
+ assertFalse(pillFnBody.contains(".page(\"previous\").draw();"), pillFnBody);
+ assertFalse(pillFnBody.contains(".page(\"next\").draw();"), pillFnBody);
+ assertFalse(pillFnBody.contains(".page(\"last\").draw();"), pillFnBody);
+
+ var menuFnBody = functionBody(body, "function buildPageSizeMenu(");
+ assertTrue(menuFnBody.contains("ctx.dataTable.page.len(value).draw();"), menuFnBody);
+ assertFalse(menuFnBody.contains("ctx.dataTable.page.len(value).draw(false)"), menuFnBody);
+ }
+
//------------------------------------------------------------------------------------------------------------------
// Top-toolbar-row regression (paging pill must sit ABOVE the table, in the SAME row as search + ribbon)
//------------------------------------------------------------------------------------------------------------------
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsJs_PagingPillDraw_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsJs_PagingPillDraw_Test.java
new file mode 100644
index 0000000000..d230b875d4
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsJs_PagingPillDraw_Test.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.views;
+
+import static java.nio.charset.StandardCharsets.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.*;
+
+import java.io.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Behavioral coverage for {@code buildPagingPill} nav handlers against DataTables 2.1.8
+ * {@code draw(resetPaging)}. A Node fake models the default {@code resetPaging=true} (jump to page 0)
+ * versus {@code draw(false)} (hold the page {@code page("next")} just set). Source-shape pins live in
+ * {@link PagingPill_Wiring_Test}. Gated on {@code node} being on {@code PATH}.
+ */
+class ViewsJs_PagingPillDraw_Test extends TestBase {
+
+ private static String viewsJs() throws IOException {
+ try (var in = ViewsMixin.class.getResourceAsStream(ViewsMixin.VIEWS_JS_RESOURCE)) {
+ assertNotNull(in);
+ return new String(in.readAllBytes(), UTF_8);
+ }
+ }
+
+ private static String rendersJs() throws IOException {
+ try (var in = ViewsMixin.class.getResourceAsStream(ViewsMixin.RENDERS_JS_RESOURCE)) {
+ assertNotNull(in);
+ return new String(in.readAllBytes(), UTF_8);
+ }
+ }
+
+ private static Map<?,?> report;
+
+ @BeforeAll
+ static void probeIfNodeAvailable() throws Exception {
+ if (!nodeAvailable())
+ return;
+ var harness = locateHarness();
+ if (harness == null)
+ return;
+ var viewsFile = Files.createTempFile("juneau-views-", ".js");
+ var rendersFile = Files.createTempFile("juneau-renders-", ".js");
+ try {
+ Files.writeString(viewsFile, viewsJs(), UTF_8);
+ Files.writeString(rendersFile, rendersJs(), UTF_8);
+ report = Json.to(runNode(harness, rendersFile, viewsFile), Map.class);
+ } finally {
+ Files.deleteIfExists(viewsFile);
+ Files.deleteIfExists(rendersFile);
+ }
+ }
+
+ private static boolean nodeAvailable() {
+ try {
+ var p = new ProcessBuilder("node", "--version").redirectErrorStream(true).start();
+ if (!p.waitFor(5, TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ return false;
+ }
+ return p.exitValue() == 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private static Path locateHarness() {
+ var basedir = System.getProperty("basedir");
+ if (basedir != null) {
+ var p = Path.of(basedir, "src/test/js/paging-pill-draw.cjs");
+ if (Files.isRegularFile(p)) return p.toAbsolutePath().normalize();
+ }
+ for (var rel : List.of(
+ "src/test/js/paging-pill-draw.cjs",
+ "juneau-rest/juneau-rest-server-views/src/test/js/paging-pill-draw.cjs"
+ )) {
+ var p = Path.of(rel);
+ if (Files.isRegularFile(p)) return p.toAbsolutePath().normalize();
+ }
+ return null;
+ }
+
+ private static String runNode(Path harness, Path rendersJs, Path viewsJs) throws Exception {
+ var stdout = Files.createTempFile("paging-pill-draw-stdout-", ".json");
+ var stderr = Files.createTempFile("paging-pill-draw-stderr-", ".txt");
+ try {
+ var pb = new ProcessBuilder(List.of("node", harness.toString(), rendersJs.toString(), viewsJs.toString()))
+ .redirectOutput(stdout.toFile())
+ .redirectError(stderr.toFile());
+ var p = pb.start();
+ if (!p.waitFor(30, TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ fail("paging-pill-draw.cjs did not finish within 30s; stderr:\n" + quietRead(stderr));
+ }
+ if (p.exitValue() != 0)
+ fail("paging-pill-draw.cjs exited " + p.exitValue() + "; stderr:\n" + quietRead(stderr)
+ + "\nstdout:\n" + quietRead(stdout));
+ return Files.readString(stdout, UTF_8);
+ } finally {
+ Files.deleteIfExists(stdout);
+ Files.deleteIfExists(stderr);
+ }
+ }
+
+ private static String quietRead(Path p) {
+ try { return Files.readString(p, UTF_8); }
+ catch (IOException e) { return "(unreadable: " + e.getMessage() + ")"; }
+ }
+
+ private static Map<?,?> report() {
+ assumeTrue(report != null, "node not available or paging-pill-draw.cjs not found — behavioral layer skipped");
+ return report;
+ }
+
+ private static int num(Map<?,?> r, String key) {
+ return ((Number)r.get(key)).intValue();
+ }
+
+ @Test void a01_fakeModelsDt218ResetVersusHold() {
+ var r = report();
+ assertEquals(true, r.get("hasBuildPagingPill"));
+ // Same probe as Scripts (69 rows, length 25): no-arg draw() jumps back to 0; draw(false) holds.
+ assertEquals(0, num(r, "control_drawNoArg_page"), r::toString);
+ assertEquals(1, num(r, "control_drawFalse_page"), r::toString);
+ assertEquals(0, num(r, "control_lastDrawNoArg_page"), r::toString);
+ assertEquals(2, num(r, "control_lastDrawFalse_page"), r::toString);
+ }
+
+ @Test void b01_nextAndLastHoldThePageTheHandlerJustSet() {
+ var r = report();
+ assertEquals(0, num(r, "initial_page"), r::toString);
+ assertEquals("1-25 of 69", r.get("initial_summary"));
+ assertEquals(false, r.get("initial_nextDisabled"));
+ assertEquals(false, r.get("initial_lastDisabled"));
+ assertEquals(1, num(r, "next_page"), r::toString);
+ assertEquals(true, r.get("next_drawArgIsFalse"));
+ assertEquals("26-50 of 69", r.get("next_summary"));
+ assertEquals(2, num(r, "last_page"), r::toString);
+ assertEquals(true, r.get("last_drawArgIsFalse"));
+ assertEquals("51-69 of 69", r.get("last_summary"));
+ }
+
+ @Test void b02_prevAndFirstWalkBack_allFourNavDrawsHold() {
+ var r = report();
+ assertEquals(1, num(r, "prev_page"), r::toString);
+ assertEquals(true, r.get("prev_drawArgIsFalse"));
+ assertEquals(0, num(r, "first_page"), r::toString);
+ assertEquals(true, r.get("first_drawArgIsFalse"));
+ assertEquals(true, r.get("navDrawArgsAllFalse"));
+ }
+
+ @Test void b03_pageSizeDrawResetsToPageZero() {
+ var r = report();
+ assertEquals(1, num(r, "beforeSizeChange_page"), r::toString);
+ assertEquals(0, num(r, "pageSize_page"), r::toString);
+ assertEquals(false, r.get("pageSize_drawArgIsFalse"));
+ assertEquals(true, r.get("pageSize_drawArgOmitted"));
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/js/paging-pill-draw.cjs b/juneau-rest/juneau-rest-server-views/src/test/js/paging-pill-draw.cjs
new file mode 100644
index 0000000000..6e37e242e6
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/paging-pill-draw.cjs
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * paging-pill-draw.cjs - always-on Node harness for buildPagingPill's DataTables nav handlers.
+ * A fake DataTables API models 2.1.8 draw(resetPaging): omitted/true jumps back to page 0; false holds
+ * the page that page("next"|"last"|"previous"|"first") just set. Every assertion lives in the Java test.
+ *
+ * Usage: node paging-pill-draw.cjs <juneau-renders.js> <juneau-views.js>
+ */
+'use strict';
+
+const path = require('node:path');
+const { loadViews } = require(path.join(__dirname, 'views-dom-shim.cjs'));
+
+const rendersJsPath = process.argv[2];
+const viewsJsPath = process.argv[3];
+if (!rendersJsPath || !viewsJsPath) {
+ console.error('usage: node paging-pill-draw.cjs <juneau-renders.js> <juneau-views.js>');
+ process.exit(2);
+}
+
+const { env, I } = loadViews(rendersJsPath, viewsJsPath);
+const out = {
+ hasBuildPagingPill: !!(I && typeof I.buildPagingPill === 'function')
+};
+if (!out.hasBuildPagingPill) { process.stdout.write(JSON.stringify(out)); process.exit(0); }
+
+/**
+ * DataTables-shaped API: page("next") updates the page, then draw() with the 2.1.8 default
+ * (resetPaging=true when the arg is omitted) snaps _iDisplayStart back to 0.
+ */
+function fakeDt(total, pageLen) {
+ let currentPage = 0;
+ let length = pageLen;
+ const drawArgs = [];
+ let drawHandler = null;
+ const table = env.el('table');
+
+ function pageCount() {
+ if (length < 0) return 1;
+ return Math.max(1, Math.ceil(total / length));
+ }
+
+ function applyPage(cmd) {
+ const pgs = pageCount();
+ if (cmd === 'first') currentPage = 0;
+ else if (cmd === 'previous') currentPage = Math.max(0, currentPage - 1);
+ else if (cmd === 'next') currentPage = Math.min(pgs - 1, currentPage + 1);
+ else if (cmd === 'last') currentPage = Math.max(0, pgs - 1);
+ }
+
+ const api = {};
+ function draw(resetPaging) {
+ drawArgs.push(resetPaging);
+ if (resetPaging !== false && resetPaging !== 'page') currentPage = 0;
+ if (drawHandler) drawHandler({ target: table });
+ return api;
+ }
+ function page(cmd) {
+ applyPage(cmd);
+ return api;
+ }
+ page.info = function () {
+ const size = length < 0 ? total : length;
+ const start = currentPage * size;
+ const end = Math.min(start + size, total);
+ return { page: currentPage, pages: pageCount(), start: start, end: end, recordsDisplay: total, recordsTotal: total };
+ };
+ page.len = function (n) {
+ if (n === undefined) return length;
+ length = n;
+ return api;
+ };
+ api.page = page;
+ api.draw = draw;
+ api.on = function (event, fn) {
+ if (event === 'draw.dt') drawHandler = fn;
+ return api;
+ };
+ api.table = table;
+ api.drawArgs = drawArgs;
+ return api;
+}
+
+function click(el) { el.dispatch('click', {}); }
+
+function btn(pill, label) {
+ return pill.querySelector('[aria-label="' + label + '"]');
+}
+
+function optionEl(pill, label) {
+ const opts = pill.querySelectorAll('[role="option"]');
+ for (const o of opts) if (o.textContent === label) return o;
+ return null;
+}
+
+// --- Control: the fake itself reproduces the 2.1.8 reset vs hold fork ---------------------------------
+(function control() {
+ const dt = fakeDt(69, 25);
+ dt.page('next').draw();
+ out.control_drawNoArg_page = dt.page.info().page;
+ dt.page('next').draw(false);
+ out.control_drawFalse_page = dt.page.info().page;
+ dt.page('last').draw();
+ out.control_lastDrawNoArg_page = dt.page.info().page;
+ dt.page('last').draw(false);
+ out.control_lastDrawFalse_page = dt.page.info().page;
+})();
+
+// --- Pill: 69 rows / length 25 / 3 pages, matching the Scripts catalog reproduction -------------------
+const dt = fakeDt(69, 25);
+const ctx = { dataTable: dt, table: dt.table };
+const pill = I.buildPagingPill({}, ctx);
+env.body.appendChild(pill);
+
+out.initial_page = dt.page.info().page;
+out.initial_summary = pill.querySelector('.juneau-view-pagingpill-info').textContent;
+out.initial_nextDisabled = btn(pill, 'Next page').disabled === true;
+out.initial_lastDisabled = btn(pill, 'Last page').disabled === true;
+
+click(btn(pill, 'Next page'));
+out.next_page = dt.page.info().page;
+out.next_drawArgIsFalse = dt.drawArgs.at(-1) === false;
+out.next_summary = pill.querySelector('.juneau-view-pagingpill-info').textContent;
+
+click(btn(pill, 'Last page'));
+out.last_page = dt.page.info().page;
+out.last_drawArgIsFalse = dt.drawArgs.at(-1) === false;
+out.last_summary = pill.querySelector('.juneau-view-pagingpill-info').textContent;
+
+click(btn(pill, 'Previous page'));
+out.prev_page = dt.page.info().page;
+out.prev_drawArgIsFalse = dt.drawArgs.at(-1) === false;
+
+click(btn(pill, 'First page'));
+out.first_page = dt.page.info().page;
+out.first_drawArgIsFalse = dt.drawArgs.at(-1) === false;
+
+out.navDrawArgsAllFalse = dt.drawArgs.length === 4 && dt.drawArgs.every(function (a) { return a === false; });
+
+// Walk to page 1, then change page size — length change must reset to page 0 (default draw()).
+click(btn(pill, 'Next page'));
+out.beforeSizeChange_page = dt.page.info().page;
+click(optionEl(pill, '100 rows'));
+out.pageSize_page = dt.page.info().page;
+out.pageSize_drawArgIsFalse = dt.drawArgs.at(-1) === false;
+out.pageSize_drawArgOmitted = dt.drawArgs.at(-1) === undefined;
+
+process.stdout.write(JSON.stringify(out));