Browse Source
* Use relative position of view for drag and drop * Add Drag And Drop Test * Add AutomationPropertiespull/20295/head
committed by
GitHub
5 changed files with 287 additions and 3 deletions
@ -0,0 +1,62 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
|
x:Class="IntegrationTestApp.Pages.DragDropPage"> |
||||
|
<DockPanel> |
||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="4"> |
||||
|
<Button Name="ResetDragDrop" Click="ResetDragDrop_Click" Margin="0,0,8,0">Reset</Button> |
||||
|
<TextBlock VerticalAlignment="Center">Drop Position: </TextBlock> |
||||
|
<TextBlock Name="DropPosition" VerticalAlignment="Center" Margin="4,0"/> |
||||
|
<TextBlock VerticalAlignment="Center" Margin="8,0,0,0">Status: </TextBlock> |
||||
|
<TextBlock Name="DragDropStatus" VerticalAlignment="Center" Margin="4,0"/> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<!-- Use a Grid with row definitions to create offset from window origin --> |
||||
|
<!-- The top spacer ensures the drag/drop controls are NOT at (0,0) --> |
||||
|
<Grid RowDefinitions="100,*"> |
||||
|
<!-- Spacer row to offset controls from window origin --> |
||||
|
<Border Grid.Row="0" Background="LightGray"> |
||||
|
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" |
||||
|
Text="Spacer (100px) - Controls below are offset from window origin"/> |
||||
|
</Border> |
||||
|
|
||||
|
<!-- Drag and Drop test area --> |
||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,*" Margin="20"> |
||||
|
<!-- Drag Source --> |
||||
|
<Border Name="DragSource" |
||||
|
Grid.Column="0" |
||||
|
Background="CornflowerBlue" |
||||
|
Margin="10" |
||||
|
CornerRadius="8" |
||||
|
AutomationProperties.AccessibilityView="Content" |
||||
|
PointerPressed="DragSource_PointerPressed"> |
||||
|
<TextBlock HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
Text="Drag Source" |
||||
|
Foreground="White" |
||||
|
FontWeight="Bold"/> |
||||
|
</Border> |
||||
|
|
||||
|
<!-- Drop Target --> |
||||
|
<Border Name="DropTarget" |
||||
|
Grid.Column="1" |
||||
|
Background="LightGreen" |
||||
|
Margin="10" |
||||
|
CornerRadius="8" |
||||
|
AutomationProperties.AccessibilityView="Content" |
||||
|
DragDrop.AllowDrop="True"> |
||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> |
||||
|
<TextBlock Text="Drop Target" |
||||
|
HorizontalAlignment="Center" |
||||
|
FontWeight="Bold"/> |
||||
|
<TextBlock Name="DropTargetText" |
||||
|
HorizontalAlignment="Center" |
||||
|
Text="Drop items here"/> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
</Grid> |
||||
|
</Grid> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,103 @@ |
|||||
|
using System; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Platform; |
||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace IntegrationTestApp.Pages; |
||||
|
|
||||
|
public partial class DragDropPage : UserControl |
||||
|
{ |
||||
|
public DragDropPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
|
||||
|
// Set up drag-drop event handlers
|
||||
|
AddHandler(DragDrop.DragOverEvent, DropTarget_DragOver); |
||||
|
AddHandler(DragDrop.DropEvent, DropTarget_Drop); |
||||
|
} |
||||
|
|
||||
|
private async void DragSource_PointerPressed(object? sender, PointerPressedEventArgs e) |
||||
|
{ |
||||
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) |
||||
|
{ |
||||
|
var dragData = new DataTransfer(); |
||||
|
dragData.Add(DataTransferItem.CreateText("TestDragData")); |
||||
|
|
||||
|
DragDropStatus.Text = "Dragging..."; |
||||
|
|
||||
|
var result = await DragDrop.DoDragDropAsync(e, dragData, DragDropEffects.Copy | DragDropEffects.Move); |
||||
|
|
||||
|
DragDropStatus.Text = result switch |
||||
|
{ |
||||
|
DragDropEffects.Copy => "Copied", |
||||
|
DragDropEffects.Move => "Moved", |
||||
|
DragDropEffects.None => "Cancelled", |
||||
|
_ => $"Result: {result}" |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void DropTarget_DragOver(object? sender, DragEventArgs e) |
||||
|
{ |
||||
|
// Only handle events for the drop target
|
||||
|
if (e.Source != DropTarget && !IsChildOf(e.Source as Visual, DropTarget)) |
||||
|
return; |
||||
|
|
||||
|
e.DragEffects = DragDropEffects.Copy; |
||||
|
|
||||
|
// Get the position relative to the drop target
|
||||
|
var position = e.GetPosition(DropTarget); |
||||
|
DropPosition.Text = $"DragOver: ({position.X:F0}, {position.Y:F0})"; |
||||
|
} |
||||
|
|
||||
|
private void DropTarget_Drop(object? sender, DragEventArgs e) |
||||
|
{ |
||||
|
// Only handle events for the drop target
|
||||
|
if (e.Source != DropTarget && !IsChildOf(e.Source as Visual, DropTarget)) |
||||
|
return; |
||||
|
|
||||
|
// Get the position relative to the drop target
|
||||
|
var position = e.GetPosition(DropTarget); |
||||
|
DropPosition.Text = $"Drop: ({position.X:F0}, {position.Y:F0})"; |
||||
|
|
||||
|
// Check if the position is within reasonable bounds of the drop target
|
||||
|
var bounds = DropTarget.Bounds; |
||||
|
var isWithinBounds = position.X >= 0 && position.X <= bounds.Width && |
||||
|
position.Y >= 0 && position.Y <= bounds.Height; |
||||
|
|
||||
|
var text = e.DataTransfer.TryGetText(); |
||||
|
if (text != null) |
||||
|
{ |
||||
|
DropTargetText.Text = isWithinBounds |
||||
|
? $"Dropped: {text} at ({position.X:F0}, {position.Y:F0})" |
||||
|
: $"ERROR: Position out of bounds! ({position.X:F0}, {position.Y:F0})"; |
||||
|
DragDropStatus.Text = isWithinBounds ? "Drop OK" : "Drop position ERROR"; |
||||
|
} |
||||
|
|
||||
|
e.DragEffects = DragDropEffects.Copy; |
||||
|
} |
||||
|
|
||||
|
private static bool IsChildOf(Visual? child, Visual? parent) |
||||
|
{ |
||||
|
if (child == null || parent == null) |
||||
|
return false; |
||||
|
|
||||
|
var current = child.Parent as Visual; |
||||
|
while (current != null) |
||||
|
{ |
||||
|
if (current == parent) |
||||
|
return true; |
||||
|
current = current.Parent as Visual; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private void ResetDragDrop_Click(object? sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
DropPosition.Text = string.Empty; |
||||
|
DragDropStatus.Text = string.Empty; |
||||
|
DropTargetText.Text = "Drop items here"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using OpenQA.Selenium.Appium; |
||||
|
using OpenQA.Selenium.Interactions; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.IntegrationTests.Appium; |
||||
|
|
||||
|
[Collection("Default")] |
||||
|
public class DragDropTests : TestBase |
||||
|
{ |
||||
|
public DragDropTests(DefaultAppFixture fixture) |
||||
|
: base(fixture, "DragDrop") |
||||
|
{ |
||||
|
var reset = Session.FindElementByAccessibilityId("ResetDragDrop"); |
||||
|
reset.Click(); |
||||
|
} |
||||
|
|
||||
|
[PlatformFact(TestPlatforms.MacOS)] |
||||
|
public void DragDrop_Coordinates_Correct_When_Controls_Offset_From_Origin() |
||||
|
{ |
||||
|
// This test verifies the fix for drag-drop coordinate calculation when
|
||||
|
// controls are positioned away from the window origin.
|
||||
|
// Issue: In embedded views or when controls have margin/offset from origin,
|
||||
|
// the drag-drop coordinates were incorrectly calculated relative to the
|
||||
|
// window rather than the view.
|
||||
|
|
||||
|
var dragSource = Session.FindElementByAccessibilityId("DragSource"); |
||||
|
var dropTarget = Session.FindElementByAccessibilityId("DropTarget"); |
||||
|
var dropPosition = Session.FindElementByAccessibilityId("DropPosition"); |
||||
|
var dragDropStatus = Session.FindElementByAccessibilityId("DragDropStatus"); |
||||
|
|
||||
|
// Perform drag from source to target
|
||||
|
new Actions(Session) |
||||
|
.MoveToElement(dragSource) |
||||
|
.ClickAndHold() |
||||
|
.MoveToElement(dropTarget) |
||||
|
.Release() |
||||
|
.Perform(); |
||||
|
|
||||
|
Thread.Sleep(500); // Allow UI to update
|
||||
|
|
||||
|
// Verify the drop was successful
|
||||
|
var status = dragDropStatus.Text; |
||||
|
Assert.True(status == "Drop OK" || status == "Copied", |
||||
|
$"Expected drop to succeed, but status was: {status}"); |
||||
|
|
||||
|
// Verify the drop position is within the target bounds
|
||||
|
// If the coordinate calculation bug exists, the position would be
|
||||
|
// offset by the spacer/margin and would show negative coordinates
|
||||
|
// or coordinates outside the target bounds
|
||||
|
var positionText = dropPosition.Text; |
||||
|
Assert.StartsWith("Drop:", positionText, StringComparison.Ordinal); |
||||
|
|
||||
|
// The DropTargetText should not contain "ERROR"
|
||||
|
var dropTargetText = Session.FindElementByAccessibilityId("DropTargetText"); |
||||
|
Assert.DoesNotContain("ERROR", dropTargetText.Text); |
||||
|
} |
||||
|
|
||||
|
[PlatformFact(TestPlatforms.MacOS)] |
||||
|
public void DragDrop_Position_Updates_During_DragOver() |
||||
|
{ |
||||
|
// Verifies that position is correctly reported during drag-over events
|
||||
|
var dragSource = Session.FindElementByAccessibilityId("DragSource"); |
||||
|
var dropTarget = Session.FindElementByAccessibilityId("DropTarget"); |
||||
|
var dropPosition = Session.FindElementByAccessibilityId("DropPosition"); |
||||
|
|
||||
|
// Start drag and move over target, then release
|
||||
|
var device = new PointerInputDevice(PointerKind.Mouse); |
||||
|
var builder = new ActionBuilder(); |
||||
|
|
||||
|
// Move to drag source and start drag
|
||||
|
builder.AddAction(device.CreatePointerMove(dragSource, 0, 0, TimeSpan.FromMilliseconds(100))); |
||||
|
builder.AddAction(device.CreatePointerDown(MouseButton.Left)); |
||||
|
|
||||
|
// Move to drop target (this triggers DragOver events)
|
||||
|
builder.AddAction(device.CreatePointerMove(dropTarget, 0, 0, TimeSpan.FromMilliseconds(200))); |
||||
|
|
||||
|
// Pause to allow DragOver events to be processed
|
||||
|
builder.AddAction(device.CreatePause(TimeSpan.FromMilliseconds(200))); |
||||
|
|
||||
|
// Release at current position (completes the drag)
|
||||
|
builder.AddAction(device.CreatePointerUp(MouseButton.Left)); |
||||
|
|
||||
|
Session.PerformActions(builder.ToActionSequenceList()); |
||||
|
|
||||
|
Thread.Sleep(200); |
||||
|
|
||||
|
// Check that position was recorded (either DragOver or Drop)
|
||||
|
var positionText = dropPosition.Text; |
||||
|
|
||||
|
// Position should have been updated during drag-over or drop
|
||||
|
Assert.True(positionText.Contains("DragOver:") || positionText.Contains("Drop:"), |
||||
|
$"Expected position to be updated during drag, but got: {positionText}"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void DragDrop_Can_Be_Cancelled() |
||||
|
{ |
||||
|
var dragSource = Session.FindElementByAccessibilityId("DragSource"); |
||||
|
var dragDropStatus = Session.FindElementByAccessibilityId("DragDropStatus"); |
||||
|
|
||||
|
// Start drag but release outside any drop target
|
||||
|
new Actions(Session) |
||||
|
.MoveToElement(dragSource) |
||||
|
.ClickAndHold() |
||||
|
.MoveByOffset(-200, 0) // Move away from drop target
|
||||
|
.Release() |
||||
|
.Perform(); |
||||
|
|
||||
|
Thread.Sleep(500); |
||||
|
|
||||
|
// Status should indicate cancelled
|
||||
|
var status = dragDropStatus.Text; |
||||
|
Assert.Equal("Cancelled", status); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue