This is an automated email from the ASF dual-hosted git repository. reiern70 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/wicket.git
commit ee62ec8c0149d19a9cb7e32d863be103c25cc0c1 Author: reiern70 <[email protected]> AuthorDate: Fri Oct 2 11:07:49 2026 -0500 Show the reviewed veil cases in the examples The review of the veil behaviors found cases where the veil did not hold or disturbed the page it covers, but the examples showed none of them, so the fixes could not be seen or tried out. ajax/veil gets a section for each case an Ajax request can show: one behavior on a list handling the clicks of its rows through a child selector, with only the clicked row veiled; a panel whose markup id is "constructor"; a scrolling box whose veil covers its visible part; an absolutely positioned card that stays in place while veiled; and a panel under a sticky header, whose veil stays behind the header. VeilPageSeleniumTest drives each of them in headless Chrome, with both Ajax engines. websockets/veil pushes a progress update halfway through its long rounds, which redraws the veiled panel while the veil stays on it, and gets a link that sends a slow Ajax request from the panel while the server sends an unveil message no veil message preceded; the panel stays veiled until the request is done. A host replaced by another Ajax request while it is veiled is shown through the WebSocket push only: requests to the same page are serialized by the page lock, so a second Ajax request cannot re-render the host while the first one runs. See GitHub issue #1631. --- .../wicket/examples/ajax/builtin/VeilPage.css | 60 +++++++++ .../wicket/examples/ajax/builtin/VeilPage.html | 67 ++++++++++ .../wicket/examples/ajax/builtin/VeilPage.java | 117 +++++++++++++++- .../examples/ajax/builtin/VeilPage.properties | 2 +- .../examples/websocket/WebSocketVeilDemoPage.html | 6 + .../examples/websocket/WebSocketVeilDemoPage.java | 126 +++++++++++++++--- .../websocket/WebSocketVeilDemoPage.properties | 2 +- .../ajax/builtin/VeilPageSeleniumTest.java | 147 +++++++++++++++++++++ 8 files changed, 503 insertions(+), 24 deletions(-) diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.css b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.css index 54f8963082..09e588d62d 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.css +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.css @@ -28,3 +28,63 @@ margin-top: 1em; background: #f7f7f7; } + +.veil-rows { + max-width: 40em; + padding: 0; + list-style: none; +} + +.veil-row { + margin: 0.5em 0; + padding: 0.5em; + border: 1px solid #ccc; + cursor: pointer; +} + +.veil-scroller { + max-width: 40em; + height: 12em; + overflow: auto; +} + +.veil-stage { + position: relative; + max-width: 40em; + height: 7em; + padding: 0 14em 0 1em; + border: 1px dashed #ccc; +} + +.veil-positioned { + position: absolute; + top: 1em; + right: 1em; + width: 11em; + background: #f7f7f7; +} + +.veil-sticky-scroller { + max-width: 40em; + height: 12em; + overflow: auto; + padding-top: 0; +} + +.veil-sticky-header { + position: sticky; + top: 0; + z-index: 100; + padding: 0.5em; + background: #336; + color: #fff; +} + +.veil-under-header { + margin-top: 4em; + background: #f7f7f7; +} + +.veil-filler { + height: 20em; +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.html index 0509383d71..d91f53c107 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.html +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.html @@ -45,4 +45,71 @@ </div> </div> +<h2>Delegated clicks</h2> + +<p> + One <code>AjaxEventBehavior</code> on the list handles the clicks on all of its rows through a + child selector, and every row has a local veil of its own. A click on a row takes a second and + veils only the row that was clicked, not the list or the page. +</p> +<ul wicket:id="rows" class="veil-rows"> + <li wicket:id="row" class="veil-row"> + Row <span wicket:id="rowNumber"></span>, clicked + <span wicket:id="rowCounter" class="row-counter"></span> times. Click anywhere on the row. + </li> +</ul> + +<h2>A markup id named after an object member</h2> + +<div wicket:id="constructor" class="veil-panel"> + This panel's markup id is <code>constructor</code>, the name of a member every JavaScript + object has. It is veiled like any other: + <a href="#" wicket:id="constructorSlow">Slow request</a>, clicked + <span wicket:id="constructorCounter" class="constructor-counter"></span> times. +</div> + +<h2>Scrolling host</h2> + +<p> + This box scrolls and has a local veil. Scroll down and click a request: the veil covers the + part of the box you see, with the spinner in its middle, and follows the box while you scroll + it. Requests sent: <span wicket:id="scrollerCounter" class="scroller-counter"></span>. +</p> +<div wicket:id="scroller" class="veil-panel veil-scroller"> + <p wicket:id="line"> + Line <span wicket:id="lineNumber"></span>: + <a href="#" wicket:id="lineSlow">slow request</a> + </p> +</div> + +<h2>Positioned host</h2> + +<div class="veil-stage"> + <p> + The card on the right is absolutely positioned and has a local veil. A request from it + takes two seconds, and the card stays where it is. + </p> + <div wicket:id="positioned" class="veil-panel veil-positioned"> + <a href="#" wicket:id="positionedSlow">Slow request</a>, clicked + <span wicket:id="positionedCounter" class="positioned-counter"></span> times + </div> +</div> + +<h2>Under a sticky header</h2> + +<p> + The panel in this box has a local veil. Click its request, which takes three seconds, then + scroll the box so the panel slides under the sticky header: the header stays in front of the + veil and its spinner. +</p> +<div class="veil-panel veil-sticky-scroller"> + <div class="veil-sticky-header">Sticky header</div> + <p>Scroll down to the panel.</p> + <div wicket:id="underHeader" class="veil-panel veil-under-header"> + <a href="#" wicket:id="underHeaderSlow">Slow request</a>, clicked + <span wicket:id="underHeaderCounter" class="under-header-counter"></span> times + </div> + <p class="veil-filler">Scroll on, so the panel can move under the header.</p> +</div> + </wicket:extend> diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.java index 1d1d279d71..7f46c88c82 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.java +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.java @@ -18,7 +18,9 @@ package org.apache.wicket.examples.ajax.builtin; import java.time.Duration; +import org.apache.wicket.AttributeModifier; import org.apache.wicket.MarkupContainer; +import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.markup.html.AjaxLink; @@ -28,18 +30,25 @@ import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.list.Loop; +import org.apache.wicket.markup.html.list.LoopItem; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.request.resource.CssResourceReference; /** * Demonstrates {@link PageVeilBehavior} and {@link LocalVeilBehavior}, including nested local - * veils with their own timings. + * veils with their own timings, and the cases a veil has to hold in: a delegated behavior, and + * hosts that scroll, are positioned or sit under a sticky header. */ public class VeilPage extends BasePage { private static final long serialVersionUID = 1L; + private static final int ROWS = 3; + + private final int[] rowClicks = new int[ROWS]; + /** * Constructor. */ @@ -68,6 +77,112 @@ public class VeilPage extends BasePage Label innerCounter = counter(inner, "innerCounter"); inner.add(new SleepingLink("innerSlow", Duration.ofSeconds(2), innerCounter, false)); inner.add(new SleepingLink("innerShort", Duration.ofMillis(200), innerCounter, false)); + + addDelegatedRows(); + addObjectMemberId(); + addScroller(); + addPositionedHost(); + addStickyHeader(); + } + + private void addDelegatedRows() + { + WebMarkupContainer rows = new WebMarkupContainer("rows"); + add(rows); + Loop rowLoop = new Loop("row", ROWS) + { + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(LoopItem item) + { + int index = item.getIndex(); + item.add(new LocalVeilBehavior()); + item.add(AttributeModifier.replace("data-row", index)); + item.add(new Label("rowNumber", index + 1)); + item.add(new Label("rowCounter", () -> rowClicks[index])); + } + }; + rows.add(rowLoop); + + rows.add(new AjaxEventBehavior("click") + { + private static final long serialVersionUID = 1L; + + @Override + protected void updateAjaxAttributes(AjaxRequestAttributes attributes) + { + super.updateAjaxAttributes(attributes); + attributes.setChildSelector("li"); + attributes.getDynamicExtraParameters() + .add("return {row: attrs.event.target.closest('li').dataset.row};"); + } + + @Override + protected void onEvent(AjaxRequestTarget target) + { + int index = getRequest().getRequestParameters() + .getParameterValue("row") + .toInt(-1); + if (index >= 0 && index < ROWS) + { + sleep(Duration.ofSeconds(1)); + rowClicks[index]++; + target.add(rowLoop.get(Integer.toString(index))); + } + } + }); + } + + private void addObjectMemberId() + { + WebMarkupContainer constructor = new WebMarkupContainer("constructor"); + constructor.setMarkupId("constructor"); + constructor.add(new LocalVeilBehavior()); + add(constructor); + + Label counter = counter(constructor, "constructorCounter"); + constructor.add(new SleepingLink("constructorSlow", Duration.ofSeconds(1), counter, false)); + } + + private void addScroller() + { + WebMarkupContainer scroller = new WebMarkupContainer("scroller"); + scroller.add(new LocalVeilBehavior()); + add(scroller); + + Label counter = counter(this, "scrollerCounter"); + scroller.add(new Loop("line", 30) + { + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(LoopItem item) + { + item.add(new Label("lineNumber", item.getIndex() + 1)); + item.add(new SleepingLink("lineSlow", Duration.ofSeconds(2), counter, false)); + } + }); + } + + private void addPositionedHost() + { + WebMarkupContainer card = new WebMarkupContainer("positioned"); + card.add(new LocalVeilBehavior()); + add(card); + + Label counter = counter(card, "positionedCounter"); + card.add(new SleepingLink("positionedSlow", Duration.ofSeconds(2), counter, false)); + } + + private void addStickyHeader() + { + WebMarkupContainer underHeader = new WebMarkupContainer("underHeader"); + underHeader.add(new LocalVeilBehavior()); + add(underHeader); + + Label counter = counter(underHeader, "underHeaderCounter"); + underHeader.add(new SleepingLink("underHeaderSlow", Duration.ofSeconds(3), counter, false)); } @Override diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.properties index 51fd11848e..a7f52f1149 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.properties +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/VeilPage.properties @@ -14,4 +14,4 @@ # limitations under the License. title=Veil Example description=blocks the page, or a part of it, while an Ajax request is running. -explanation=<p> PageVeilBehavior puts a transparent veil over the page as soon as an Ajax request starts, so nothing can be clicked until it is done. A spinner shows only if the request takes longer than 300 ms, and then stays for at least 500 ms. LocalVeilBehavior does the same for a single component, for the requests fired from inside it. Local veils can be nested, the innermost one taking the request, and each can have its own timings. A request opts out with PageVeilBehavior.noVeil(a [...] +explanation=<p> PageVeilBehavior puts a transparent veil over the page as soon as an Ajax request starts, so nothing can be clicked until it is done. A spinner shows only if the request takes longer than 300 ms, and then stays for at least 500 ms. LocalVeilBehavior does the same for a single component, for the requests fired from inside it. Local veils can be nested, the innermost one taking the request, and each can have its own timings. A request opts out with PageVeilBehavior.noVeil(a [...] diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.html index 77e6994e59..4d375c41bf 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.html +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.html @@ -12,6 +12,12 @@ Pushed counter: <span wicket:id="counter" class="push-counter"></span> </p> <p wicket:id="lastWork" class="push-last-work"></p> + <p> + <a href="#" wicket:id="strayUnveil">Slow request with a stray unveil</a>: an Ajax + request from this panel that takes two seconds. Half a second in, the server sends an + unveil message that no veil message preceded. The panel stays veiled until the request + is done. Requests sent: <span wicket:id="panelRequests" class="panel-requests"></span> + </p> </div> </wicket:extend> </body> diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.java index 3c9b001fc0..6273209bb1 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.java +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.java @@ -66,6 +66,8 @@ public class WebSocketVeilDemoPage extends WicketExamplePage private final Label lastWork; + private int panelRequests; + /** * Constructor. */ @@ -84,6 +86,20 @@ public class WebSocketVeilDemoPage extends WicketExamplePage counterPanel.add(counter); lastWork = new Label("lastWork", Model.of("No update pushed yet.")); counterPanel.add(lastWork); + counterPanel.add(new Label("panelRequests", () -> panelRequests)); + counterPanel.add(new AjaxLink<Void>("strayUnveil") + { + private static final long serialVersionUID = 1L; + + @Override + public void onClick(AjaxRequestTarget target) + { + sendStrayUnveil(); + sleep(Duration.ofSeconds(2)); + panelRequests++; + target.add(counterPanel); + } + }); add(new AjaxLink<Void>("start") { @@ -123,6 +139,49 @@ public class WebSocketVeilDemoPage extends WicketExamplePage } } + private void sendStrayUnveil() + { + String applicationName = getApplication().getName(); + String sessionId = getSession().getId(); + int pageId = getPageId(); + String unveilMessage = veil.getUnveilMessage(); + JSR356Application.get().getScheduledExecutorService().schedule(() -> { + IWebSocketConnection connection = connection(applicationName, sessionId, pageId); + if (connection != null && connection.isOpen()) + { + try + { + connection.sendMessage(unveilMessage); + } + catch (IOException e) + { + LOGGER.error("Sending the unveil message failed", e); + } + } + }, 500, TimeUnit.MILLISECONDS); + } + + private static IWebSocketConnection connection(String applicationName, String sessionId, + int pageId) + { + Application application = Application.get(applicationName); + return WebSocketSettings.Holder.get(application) + .getConnectionRegistry() + .getConnection(application, sessionId, new PageIdKey(pageId)); + } + + private static void sleep(Duration duration) + { + try + { + Thread.sleep(duration.toMillis()); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + } + private String taskKey() { return getSession().getId() + "#" + getPageId(); @@ -133,15 +192,23 @@ public class WebSocketVeilDemoPage extends WicketExamplePage { super.onEvent(event); - if (event.getPayload() instanceof WebSocketPushPayload payload && - payload.getMessage() instanceof CounterUpdate update) + if (event.getPayload() instanceof WebSocketPushPayload payload) { - counter.setDefaultModelObject(update.value); - lastWork.setDefaultModelObject(String.format( - "Update %d of %d took %d ms on the server.", update.value, ROUNDS, - update.work.toMillis())); - payload.getHandler().add(counterPanel); - veil.unveil(payload.getHandler()); + if (payload.getMessage() instanceof CounterUpdate update) + { + counter.setDefaultModelObject(update.value); + lastWork.setDefaultModelObject(String.format( + "Update %d of %d took %d ms on the server.", update.value, ROUNDS, + update.work.toMillis())); + payload.getHandler().add(counterPanel); + veil.unveil(payload.getHandler()); + } + else if (payload.getMessage() instanceof Progress progress) + { + lastWork.setDefaultModelObject(String.format( + "Working on update %d of %d, halfway through.", progress.round, ROUNDS)); + payload.getHandler().add(counterPanel); + } } } @@ -170,10 +237,24 @@ public class WebSocketVeilDemoPage extends WicketExamplePage } } + /** + * Reports halfway through the work, without lifting the veil. + */ + private static class Progress implements IWebSocketPushMessage + { + private final int round; + + Progress(int round) + { + this.round = round; + } + } + /** * Recomputes the counter a number of times, alternating long and short work. Both outlast the - * spinner delay; the short work ends within the spinner's minimum time, so the spinner stays - * on the redrawn panel for the rest of it. + * spinner delay; the long work reports its progress halfway through, which redraws the veiled + * panel; the short work ends within the spinner's minimum time, so the spinner stays on the + * redrawn panel for the rest of it. */ private static class PushTask implements Runnable { @@ -211,15 +292,26 @@ public class WebSocketVeilDemoPage extends WicketExamplePage { for (int round = 1; round <= ROUNDS && !canceled; round++) { - IWebSocketConnection connection = connection(); + IWebSocketConnection connection = connection(applicationName, sessionId, + pageId); if (connection == null || !connection.isOpen()) { return; } connection.sendMessage(veilMessage); - Duration work = Duration.ofMillis(round % 2 == 1 ? 1500 : 600); - TimeUnit.MILLISECONDS.sleep(work.toMillis()); + boolean longWork = round % 2 == 1; + Duration work = Duration.ofMillis(longWork ? 3000 : 600); + if (longWork) + { + TimeUnit.MILLISECONDS.sleep(work.toMillis() / 2); + connection.sendMessage(new Progress(round)); + TimeUnit.MILLISECONDS.sleep(work.toMillis() / 2); + } + else + { + TimeUnit.MILLISECONDS.sleep(work.toMillis()); + } connection.sendMessage(new CounterUpdate(round, work)); TimeUnit.SECONDS.sleep(1); @@ -238,13 +330,5 @@ public class WebSocketVeilDemoPage extends WicketExamplePage TASKS.remove(key); } } - - private IWebSocketConnection connection() - { - Application application = Application.get(applicationName); - return WebSocketSettings.Holder.get(application) - .getConnectionRegistry() - .getConnection(application, sessionId, new PageIdKey(pageId)); - } } } diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.properties index 9bf32492af..a97b76bb88 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.properties +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/websocket/WebSocketVeilDemoPage.properties @@ -14,4 +14,4 @@ # limitations under the License. title=Veil a component while the server recomputes it description=raises a local veil from the server and lifts it with the pushed update. -explanation=<p> "Start pushes" starts a background task bound to this page. Ten times, it tells the browser through the WebSocket connection that the counter panel is being recomputed, which raises the panel's LocalVeilBehavior, works for a while, and pushes the new counter. The update lifts the veil. Odd rounds take 1.5 s: the spinner shows after 300 ms and goes away with the update. Even rounds take 600 ms: the spinner shows after 300 ms, and since the update arrives within its minimum [...] +explanation=<p> "Start pushes" starts a background task bound to this page. Ten times, it tells the browser through the WebSocket connection that the counter panel is being recomputed, which raises the panel's LocalVeilBehavior, works for a while, and pushes the new counter. The update lifts the veil. Odd rounds take 3 s: the spinner shows after 300 ms, a progress update halfway through redraws the panel, which stays veiled, and the spinner goes away with the final update. Even rounds ta [...] diff --git a/wicket-examples/src/test/java/org/apache/wicket/examples/ajax/builtin/VeilPageSeleniumTest.java b/wicket-examples/src/test/java/org/apache/wicket/examples/ajax/builtin/VeilPageSeleniumTest.java index d114cb5b38..277a797d4e 100644 --- a/wicket-examples/src/test/java/org/apache/wicket/examples/ajax/builtin/VeilPageSeleniumTest.java +++ b/wicket-examples/src/test/java/org/apache/wicket/examples/ajax/builtin/VeilPageSeleniumTest.java @@ -57,6 +57,8 @@ class VeilPageSeleniumTest extends JettyTestCaseDecorator private static final String INNER_VEIL = ".veil-inner > .wicket-veil"; + private static final String ROW_VEIL = ".veil-row > .wicket-veil"; + /** * Logs when a veil is added, gets its spinner and is removed, with the time of each, into * {@code window.veilLog}. @@ -308,6 +310,151 @@ class VeilPageSeleniumTest extends JettyTestCaseDecorator "the spinner was shown for " + shown + " ms instead of the configured 1 s"); } + @ParameterizedTest + @EnumSource(Engine.class) + void aDelegatedClickVeilsTheRowThatWasClicked(Engine engine) + { + open(engine); + + List<WebElement> rows = driver.findElements(By.cssSelector(".veil-row")); + String rowId = rows.get(1).getDomAttribute("id"); + rows.get(1).click(); + + wait.until(ExpectedConditions.presenceOfElementLocated( + By.cssSelector("#" + rowId + " > .wicket-veil"))); + assertEquals(1, driver.findElements(By.cssSelector(ROW_VEIL)).size(), + "another row was veiled too"); + assertTrue(driver.findElements(By.cssSelector(PAGE_VEIL)).isEmpty(), + "the page was veiled"); + + wait.until(ExpectedConditions.textToBe( + By.cssSelector("[data-row='1'] .row-counter"), "1")); + awaitNoVeil(); + } + + @ParameterizedTest + @EnumSource(Engine.class) + void aHostWithTheIdOfAnObjectMemberIsVeiled(Engine engine) + { + open(engine); + + driver.findElement(By.cssSelector("#constructor a")).click(); + + wait.until(ExpectedConditions.presenceOfElementLocated( + By.cssSelector("#constructor > .wicket-veil"))); + assertTrue(driver.findElements(By.cssSelector(PAGE_VEIL)).isEmpty(), + "the page was veiled instead"); + wait.until(ExpectedConditions.textToBe(By.cssSelector(".constructor-counter"), "1")); + awaitNoVeil(); + } + + @ParameterizedTest + @EnumSource(Engine.class) + void theVeilOfAScrolledHostCoversItsVisiblePart(Engine engine) + { + open(engine); + + WebElement scroller = driver.findElement(By.cssSelector(".veil-scroller")); + js().executeScript("arguments[0].scrollIntoView({ block: 'center' });" + + "arguments[0].scrollTop = arguments[0].scrollHeight;", scroller); + List<WebElement> links = scroller.findElements(By.tagName("a")); + new Actions(driver).moveToElement(links.get(links.size() - 1)).click().perform(); + wait.until(ExpectedConditions.presenceOfElementLocated( + By.cssSelector(".veil-scroller > .wicket-veil.wicket-veil-busy"))); + + assertCoveredByItsVeil(scroller); + js().executeScript("arguments[0].scrollTop = 100;", scroller); + assertCoveredByItsVeil(scroller); + + wait.until(ExpectedConditions.textToBe(By.cssSelector(".scroller-counter"), "1")); + awaitNoVeil(); + } + + @ParameterizedTest + @EnumSource(Engine.class) + void aPositionedHostStaysWhereItIs(Engine engine) + { + open(engine); + + WebElement card = driver.findElement(By.cssSelector(".veil-positioned")); + Object before = offsetInStage(card); + card.findElement(By.tagName("a")).click(); + wait.until(ExpectedConditions.presenceOfElementLocated( + By.cssSelector(".veil-positioned > .wicket-veil"))); + + assertEquals(before, offsetInStage(card), "the host moved while it was veiled"); + assertEquals("absolute", card.getCssValue("position")); + + wait.until(ExpectedConditions.textToBe(By.cssSelector(".positioned-counter"), "1")); + awaitNoVeil(); + } + + @ParameterizedTest + @EnumSource(Engine.class) + void theVeilStaysBehindAStickyHeaderInFrontOfItsHost(Engine engine) + { + open(engine); + + WebElement panel = driver.findElement(By.cssSelector(".veil-under-header")); + js().executeScript("arguments[0].scrollIntoView({ block: 'center' });", panel); + panel.findElement(By.tagName("a")).click(); + wait.until(ExpectedConditions.presenceOfElementLocated( + By.cssSelector(".veil-under-header > .wicket-veil.wicket-veil-busy"))); + + Object hit = js().executeAsyncScript(""" + var callback = arguments[arguments.length - 1]; + var scroller = document.querySelector('.veil-sticky-scroller'); + var header = document.querySelector('.veil-sticky-header'); + var panel = document.querySelector('.veil-under-header'); + scroller.scrollTop += panel.getBoundingClientRect().top - + scroller.getBoundingClientRect().top; + requestAnimationFrame(function () { + var rect = header.getBoundingClientRect(); + var x = rect.left + rect.width / 2; + var y = rect.top + rect.height / 2; + var under = panel.getBoundingClientRect(); + callback({ + panelUnderHeader: under.top <= y && y <= under.bottom, + hit: document.elementFromPoint(x, y) === header + }); + }); + """); + assertEquals(Map.of("panelUnderHeader", true, "hit", true), hit, + "the veil paints over the sticky header"); + + wait.until(ExpectedConditions.textToBe(By.cssSelector(".under-header-counter"), "1")); + awaitNoVeil(); + } + + private void assertCoveredByItsVeil(WebElement host) + { + Object result = js().executeAsyncScript(""" + var host = arguments[0]; + var callback = arguments[arguments.length - 1]; + requestAnimationFrame(function () { + var veil = host.querySelector(':scope > .wicket-veil'); + var rect = host.getBoundingClientRect(); + var points = [[0.1, 0.1], [0.5, 0.5], [0.9, 0.9]].filter(function (point) { + var x = rect.left + host.clientWidth * point[0]; + var y = rect.top + host.clientHeight * point[1]; + return document.elementFromPoint(x, y) !== veil; + }); + callback(points.length === 0 ? 'covered' : + 'not covered at ' + JSON.stringify(points) + ', scrollTop ' + host.scrollTop); + }); + """, host); + assertEquals("covered", result); + } + + private Object offsetInStage(WebElement element) + { + return js().executeScript(""" + var r = arguments[0].getBoundingClientRect(); + var stage = arguments[0].parentNode.getBoundingClientRect(); + return [r.left - stage.left, r.top - stage.top, r.width, r.height].join(','); + """, element); + } + private WebElement outerPanel() { return driver.findElement(By.cssSelector(".veil-outer"));
