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
@@ -0,0 +1,95 @@
/**
* Copyright (C) 2000-2025 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See <https://vaadin.com/commercial-license-and-service-terms> for the full
* license.
*/
package com.vaadin.testbench.unit;

import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.Tag;

/**
* Example showing how to use ComponentTester.fireShortcut() method
* to test components with keyboard shortcuts.
*/
@ViewPackages(packages = "com.example")
@ExtendWith(TreeOnFailureExtension.class)
class ComponentTesterFireShortcutExampleTest extends UIUnitTest {

@Test
void example_testComponentWithKeyboardShortcut() {
// Create a component that responds to keyboard shortcuts
MyCustomComponent component = new MyCustomComponent();
getCurrentView().add(component);

// Get a tester for the component
ComponentTester<MyCustomComponent> tester = test(component);

// Test that the shortcut works
Assertions.assertFalse(component.isShortcutTriggered(), "Shortcut should not be triggered initially");

// Fire the shortcut using ComponentTester
tester.fireShortcut(Key.KEY_S, KeyModifier.CONTROL);

// Verify the shortcut was handled
Assertions.assertTrue(component.isShortcutTriggered(), "Shortcut should have been triggered");
}

@Test
void example_differenceFromUILevelShortcuts() {
MyCustomComponent component = new MyCustomComponent();
getCurrentView().add(component);
ComponentTester<MyCustomComponent> tester = test(component);

// This fires a shortcut on the specific component
// (for KeyNotifier listeners attached to the component)
tester.fireShortcut(Key.KEY_S, KeyModifier.CONTROL);
Assertions.assertTrue(component.isShortcutTriggered());

// Reset for next test
component.resetShortcutTriggered();

// This fires a shortcut at the UI level
// (for shortcuts created with Shortcuts.addShortcutListener on UI)
fireShortcut(Key.KEY_S, KeyModifier.CONTROL);

// The component-level listener won't be triggered by UI-level shortcut
// (depends on how the listeners are set up)
// This demonstrates the difference between the two approaches
}

@Tag("my-custom-component")
private static class MyCustomComponent extends Component {
private final AtomicBoolean shortcutTriggered = new AtomicBoolean(false);

public MyCustomComponent() {
// Simulate a component that listens for keyboard shortcuts
// In a real component this would be done with KeyNotifier interface
getElement().addEventListener("keydown", domEvent -> {
String key = domEvent.getEventData().getString("event.key");
if ("s".equals(key)) {
shortcutTriggered.set(true);
}
}).addEventData("event.key");
}

public boolean isShortcutTriggered() {
return shortcutTriggered.get();
}

public void resetShortcutTriggered() {
shortcutTriggered.set(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Copyright (C) 2000-2025 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See <https://vaadin.com/commercial-license-and-service-terms> for the full
* license.
*/
package com.vaadin.testbench.unit;

import java.util.concurrent.atomic.AtomicInteger;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.dom.DomListenerRegistration;

@ViewPackages(packages = "com.example")
@ExtendWith(TreeOnFailureExtension.class)
class ComponentTesterFireShortcutIntegrationTest extends UIUnitTest {

@Test
void fireShortcut_componentWithDomKeyDownListener_listenerInvoked() {
AtomicInteger eventsCounter = new AtomicInteger();
TestComponent component = new TestComponent();

// Add a DOM-level keydown listener to simulate KeyNotifier behavior
component.getElement().addEventListener("keydown", domEvent -> {
String key = domEvent.getEventData().getString("event.key");
if ("g".equals(key)) {
eventsCounter.incrementAndGet();
}
}).addEventData("event.key");

getCurrentView().add(component);
ComponentTester<TestComponent> tester = test(component);

// Fire shortcut - this should trigger the keydown listener
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
Assertions.assertEquals(1, eventsCounter.get());

// Fire different shortcut
tester.fireShortcut(Key.KEY_A, KeyModifier.CONTROL);
Assertions.assertEquals(1, eventsCounter.get());

// Fire the same shortcut again
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
Assertions.assertEquals(2, eventsCounter.get());
}

@Test
void fireShortcut_componentWithMultipleListeners_allListenersInvoked() {
AtomicInteger gKeyCounter = new AtomicInteger();
AtomicInteger aKeyCounter = new AtomicInteger();
TestComponent component = new TestComponent();

// Add listeners for different keys
component.getElement().addEventListener("keydown", domEvent -> {
String key = domEvent.getEventData().getString("event.key");
if ("g".equals(key)) {
gKeyCounter.incrementAndGet();
} else if ("a".equals(key)) {
aKeyCounter.incrementAndGet();
}
}).addEventData("event.key");

getCurrentView().add(component);
ComponentTester<TestComponent> tester = test(component);

// Fire G key shortcut
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
Assertions.assertEquals(1, gKeyCounter.get());
Assertions.assertEquals(0, aKeyCounter.get());

// Fire A key shortcut
tester.fireShortcut(Key.KEY_A, KeyModifier.ALT);
Assertions.assertEquals(1, gKeyCounter.get());
Assertions.assertEquals(1, aKeyCounter.get());
}

@Test
void fireShortcut_comparedToDirectDomEvent_behaviorIsConsistent() {
AtomicInteger shortcutCounter = new AtomicInteger();
AtomicInteger domEventCounter = new AtomicInteger();
TestComponent component = new TestComponent();

// Add keydown listener that counts both
component.getElement().addEventListener("keydown", domEvent -> {
String key = domEvent.getEventData().getString("event.key");
if ("g".equals(key)) {
shortcutCounter.incrementAndGet();
domEventCounter.incrementAndGet();
}
}).addEventData("event.key");

getCurrentView().add(component);
ComponentTester<TestComponent> tester = test(component);

// Use our fireShortcut method
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
int afterShortcut = shortcutCounter.get();

// Use direct DOM event (for comparison)
tester.fireDomEvent("keydown", elemental.json.Json.create("{\"event.key\": \"g\"}"));
int afterDomEvent = domEventCounter.get();

// Both should have triggered the listener
Assertions.assertTrue(afterShortcut > 0, "Shortcut should have triggered listener");
Assertions.assertTrue(afterDomEvent > afterShortcut, "DOM event should also trigger listener");
}

@Tag("test-component")
private static class TestComponent extends Component {
// Simple component for testing
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.vaadin.testbench.unit;

import java.util.concurrent.atomic.AtomicInteger;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.Tag;

@ViewPackages(packages = "com.example")
@ExtendWith(TreeOnFailureExtension.class)
class ComponentTesterFireShortcutTest extends UIUnitTest {

@Test
void fireShortcut_usableComponent_methodCallsInternalShortcutFunction() {
TestComponent component = new TestComponent();
getCurrentView().add(component);
ComponentTester<TestComponent> tester = test(component);

// The component is usable, so this should not throw
Assertions.assertDoesNotThrow(() -> {
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
});

// Test with no modifiers
Assertions.assertDoesNotThrow(() -> {
tester.fireShortcut(Key.ENTER);
});

// Test with multiple modifiers
Assertions.assertDoesNotThrow(() -> {
tester.fireShortcut(Key.KEY_S, KeyModifier.ALT, KeyModifier.SHIFT);
});
}

@Test
void fireShortcut_componentNotUsable_throwsException() {
TestComponent component = new TestComponent();
getCurrentView().add(component);
ComponentTester<TestComponent> tester = test(component);

// Make component not usable
component.setVisible(false);

// Should throw IllegalStateException because component is not usable
IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, () -> {
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
});

Assertions.assertTrue(exception.getMessage().contains("not usable"),
"Exception should mention that component is not usable");
}

@Test
void fireShortcut_componentNotAttached_throwsException() {
TestComponent component = new TestComponent();
// Note: component is not added to current view, so it's not attached
ComponentTester<TestComponent> tester = test(component);

// Should throw IllegalStateException because component is not attached
IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, () -> {
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
});

Assertions.assertTrue(exception.getMessage().contains("not usable"),
"Exception should mention that component is not usable");
}

@Test
void fireShortcut_componentDisabled_throwsException() {
TestComponent component = new TestComponent();
getCurrentView().add(component);
component.getElement().setEnabled(false);
ComponentTester<TestComponent> tester = test(component);

// Should throw IllegalStateException because component is disabled
IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, () -> {
tester.fireShortcut(Key.KEY_G, KeyModifier.CONTROL);
});

Assertions.assertTrue(exception.getMessage().contains("not usable"),
"Exception should mention that component is not usable");
}

@Tag("test-component")
private static class TestComponent extends Component {
// Simple component for testing
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.internal.AbstractFieldSupport;
import com.vaadin.flow.dom.DomEvent;
import com.vaadin.flow.internal.nodefeature.ElementListenerMap;
import com.vaadin.testbench.unit.internal.PrettyPrintTreeKt;
import com.vaadin.testbench.unit.internal.ShortcutsKt;

import elemental.json.Json;
import elemental.json.JsonObject;
Expand Down Expand Up @@ -239,6 +242,27 @@ protected void roundTrip() {
BaseUIUnitTest.roundTrip();
}

/**
* Simulates a keyboard shortcut performed on the wrapped component.
* <p>
* This method is designed to work with shortcuts attached to the component
* through the KeyNotifier interface methods, such as addKeyDownListener.
* For UI-level shortcuts created with the Shortcuts API, use
* {@link BaseUIUnitTest#fireShortcut(Key, KeyModifier...)} instead.
*
* @param key
* Primary key of the shortcut. This must not be a
* {@link KeyModifier}.
* @param modifiers
* Key modifiers. Can be empty.
* @throws IllegalStateException
* if the component is not usable
*/
public void fireShortcut(Key key, KeyModifier... modifiers) {
ensureComponentIsUsable();
ShortcutsKt._fireShortcut(getComponent(), key, modifiers);
}

/**
* Get field with given name in the wrapped component.
*
Expand Down