Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
import com.vaadin.browserless.internal.Routes;
import com.vaadin.browserless.locator.Locators;
import com.vaadin.browserless.mocks.MockedUI;
import com.vaadin.browserless.trigger.TriggerSimulation;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasElement;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.shared.Registration;

/**
* Abstract base for browserless JUnit 5 extensions. Holds all shared state and
Expand All @@ -50,6 +53,7 @@ abstract class AbstractBrowserlessExtension

// Runtime state
private TestSignalEnvironment signalsTestEnvironment;
private Registration triggerArmingRegistration;
private Runnable cleanupAction;

// --- Protected builder helpers ---
Expand Down Expand Up @@ -100,6 +104,10 @@ private void standaloneCleanup() {
signalsTestEnvironment.unregister();
signalsTestEnvironment = null;
}
if (triggerArmingRegistration != null) {
triggerArmingRegistration.remove();
triggerArmingRegistration = null;
}
MockVaadin.tearDown();
}

Expand Down Expand Up @@ -132,6 +140,8 @@ private void standaloneInit(Class<?> testClass) {
Routes routes = RouteDiscovery.discover(packages);
MockVaadin.setup(routes, MockedUI::new, services);
signalsTestEnvironment = TestSignalEnvironment.register();
triggerArmingRegistration = TriggerSimulation
.install(VaadinService.getCurrent());
}

// --- Testing DSL ---
Expand Down
46 changes: 46 additions & 0 deletions quarkus/src/test/java/com/example/base/ClipboardSmokeView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2000-2026 Vaadin Ltd.
*
* Licensed 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 com.example.base;

import com.vaadin.flow.component.clipboard.Clipboard;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.router.Route;

/**
* Test fixture wiring both a copy ({@code writeText}) and a read
* ({@code readText}) clipboard binding to buttons, so a browserless smoke test
* can verify trigger/action simulation end to end.
*/
@Route("clipboard-smoke")
public class ClipboardSmokeView extends Div {

public static final String COPY_TEXT = "smoke-copied-value";

public final NativeButton copy = new NativeButton("Copy");
public final NativeButton paste = new NativeButton("Paste");

public String copied;
public String pasted;

public ClipboardSmokeView() {
Clipboard.onClick(copy).writeText(COPY_TEXT, c -> copied = c, e -> {
});
Clipboard.onClick(paste).readText(t -> pasted = t, e -> {
});
add(copy, paste);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2000-2026 Vaadin Ltd.
*
* Licensed 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 com.vaadin.browserless.quarkus;

import com.example.base.ClipboardSmokeView;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import com.vaadin.browserless.ViewPackages;
import com.vaadin.flow.component.clipboard.ClipboardSimulator;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Smoke test that Flow trigger/action simulation works under
* {@link QuarkusBrowserlessTest}: clicking a clipboard-bound button fires the
* client-side trigger, so the {@link ClipboardSimulator} and the application's
* callbacks behave as in a browser.
*/
@QuarkusTest
@ViewPackages(packages = "com.example")
class ClipboardSmokeTest extends QuarkusBrowserlessTest {

@Test
void clickingCopy_firesWriteTrigger() {
ClipboardSmokeView view = navigate(ClipboardSmokeView.class);

test(view.copy).click();

assertEquals(ClipboardSmokeView.COPY_TEXT,
ClipboardSimulator.current().text());
assertEquals(ClipboardSmokeView.COPY_TEXT, view.copied);
}

@Test
void clickingPaste_firesReadTrigger() {
ClipboardSmokeView view = navigate(ClipboardSmokeView.class);
ClipboardSimulator.current().setText("pasted-value");

test(view.paste).click();

assertEquals("pasted-value", view.pasted);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
import com.vaadin.browserless.internal.MockVaadin;
import com.vaadin.browserless.internal.Routes;
import com.vaadin.browserless.mocks.MockedUI;
import com.vaadin.browserless.trigger.TriggerSimulation;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasElement;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.shared.Registration;

/**
* Base class for browserless tests.
Expand All @@ -54,6 +57,8 @@ public abstract class BaseBrowserlessTest {

private TestSignalEnvironment signalsTestEnvironment;

private Registration triggerArmingRegistration;

protected synchronized Routes discoverRoutes() {
return discoverRoutes(scanPackages());
}
Expand Down Expand Up @@ -81,6 +86,14 @@ protected void initVaadinEnvironment() {

protected void initSignalsSupport() {
signalsTestEnvironment = TestSignalEnvironment.register();
// Observe trigger arming for this environment, so client-side triggers
// (e.g. Clipboard bindings) armed during navigation are recorded for
// simulation. Removed in cleanVaadinEnvironment(). Runs here — the
// point
// every init path (plain, Spring, Quarkus) reaches — before the test
// navigates.
triggerArmingRegistration = TriggerSimulation
.install(VaadinService.getCurrent());
}

/**
Expand Down Expand Up @@ -123,6 +136,10 @@ protected void cleanVaadinEnvironment() {
signalsTestEnvironment.unregister();
signalsTestEnvironment = null;
}
if (triggerArmingRegistration != null) {
triggerArmingRegistration.remove();
triggerArmingRegistration = null;
}
MockVaadin.tearDown();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@
import com.vaadin.browserless.internal.UIFactory;
import com.vaadin.browserless.mocks.MockVaadinServlet;
import com.vaadin.browserless.mocks.MockedUI;
import com.vaadin.browserless.trigger.TriggerSimulation;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinServlet;
import com.vaadin.flow.server.VaadinServletService;
import com.vaadin.flow.shared.Registration;

/**
* Application-level context for multi-user browserless testing.
Expand Down Expand Up @@ -83,6 +85,7 @@ public class BrowserlessApplicationContext implements AutoCloseable {
private final List<Runnable> closeHooks;
private final List<BrowserlessUserContext> users = new ArrayList<>();
private TestSignalEnvironment signalsTestEnvironment;
private Registration triggerArmingRegistration;
private RequestContextHandler requestContextHandler;
private boolean closed;

Expand All @@ -95,6 +98,11 @@ public class BrowserlessApplicationContext implements AutoCloseable {
// here so it covers session-init listeners fired by
// BrowserlessUserContext.
this.signalsTestEnvironment = TestSignalEnvironment.register();
// Observe trigger arming for this environment before any window/UI is
// created, so client-side triggers armed during navigation are recorded
// for simulation. Bound to this environment's service so it ignores
// armings from other (concurrent) environments. Removed in close().
this.triggerArmingRegistration = TriggerSimulation.install(service);
}

/**
Expand Down Expand Up @@ -268,6 +276,10 @@ public void close() {
signalsTestEnvironment.unregister();
signalsTestEnvironment = null;
}
if (triggerArmingRegistration != null) {
triggerArmingRegistration.remove();
triggerArmingRegistration = null;
}
MockVaadin.fireServiceDestroy(service);
VaadinService.setCurrent(null);
List<RuntimeException> hookFailures = new ArrayList<>();
Expand Down
5 changes: 5 additions & 0 deletions shared/src/main/java/com/vaadin/browserless/Clickable.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.vaadin.browserless;

import com.vaadin.browserless.trigger.TriggerSimulation;
import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentUtil;
Expand Down Expand Up @@ -111,5 +112,9 @@ default void click(int button, MetaKeys metaKeys) {
new ClickEvent<>(component, true, 0, 0, 0, 0, 0, button,
metaKeys.isCtrl(), metaKeys.isShift(), metaKeys.isAlt(),
metaKeys.isMeta()));
// A real browser also runs any client-side click triggers on this
// component (e.g. Clipboard.onClick bindings) during the gesture;
// reproduce their server-observable effect here.
TriggerSimulation.fireClick(component, button, metaKeys);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2000-2026 Vaadin Ltd.
*
* Licensed 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 com.vaadin.browserless.trigger;

import com.vaadin.flow.component.trigger.internal.Action;
import com.vaadin.flow.component.trigger.internal.Trigger;

/**
* Reproduces, on the server, the server-observable effect of an {@link Action}
* when its trigger fires — without a browser. Registered per action type with
* {@link TriggerSimulation} and invoked by
* {@link TriggerSimulation#fire(com.vaadin.flow.component.Component, String, tools.jackson.databind.node.ObjectNode)}.
*
* @param <A>
* the action type this simulator handles
* @since 1.1
*/
@FunctionalInterface
public interface ActionSimulator<A extends Action> {

/**
* Simulates the given action firing on the given trigger.
*
* @param action
* the action to simulate, not {@code null}
* @param trigger
* the trigger the action was armed on, not {@code null}
* @param context
* the simulation context (event payload, UI, input evaluation),
* not {@code null}
*/
void simulate(A action, Trigger trigger, SimulationContext context);
}
Loading
Loading