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));