diff --git a/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutExampleTest.java b/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutExampleTest.java new file mode 100644 index 000000000..81e45f359 --- /dev/null +++ b/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutExampleTest.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2000-2025 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See 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 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 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); + } + } +} \ No newline at end of file diff --git a/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutIntegrationTest.java b/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutIntegrationTest.java new file mode 100644 index 000000000..5c4f35dc8 --- /dev/null +++ b/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutIntegrationTest.java @@ -0,0 +1,121 @@ +/** + * Copyright (C) 2000-2025 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See 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 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 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 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 + } +} \ No newline at end of file diff --git a/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutTest.java b/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutTest.java new file mode 100644 index 000000000..9730c6ab0 --- /dev/null +++ b/vaadin-testbench-unit-junit5/src/test/java/com/vaadin/testbench/unit/ComponentTesterFireShortcutTest.java @@ -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 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 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 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 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 + } +} \ No newline at end of file diff --git a/vaadin-testbench-unit-shared/src/main/java/com/vaadin/testbench/unit/ComponentTester.java b/vaadin-testbench-unit-shared/src/main/java/com/vaadin/testbench/unit/ComponentTester.java index 84ce11d46..04c24239b 100644 --- a/vaadin-testbench-unit-shared/src/main/java/com/vaadin/testbench/unit/ComponentTester.java +++ b/vaadin-testbench-unit-shared/src/main/java/com/vaadin/testbench/unit/ComponentTester.java @@ -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; @@ -239,6 +242,27 @@ protected void roundTrip() { BaseUIUnitTest.roundTrip(); } + /** + * Simulates a keyboard shortcut performed on the wrapped component. + *

+ * 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. *