Skip to content

Commit dc2a16a

Browse files
committed
feat: Use source generator for event dispatch extensions
1 parent 715c554 commit dc2a16a

14 files changed

+574
-2605
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
using System.Text;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace Bunit.Blazor;
5+
6+
[Generator]
7+
public class EventDispatcherExtensionGenerator : IIncrementalGenerator
8+
{
9+
public void Initialize(IncrementalGeneratorInitializationContext context)
10+
{
11+
context.RegisterPostInitializationOutput(GenerateEventDispatchExtensions);
12+
}
13+
14+
private static void GenerateEventDispatchExtensions(IncrementalGeneratorPostInitializationContext context)
15+
{
16+
var sourceBuilder = new StringBuilder(4000);
17+
sourceBuilder.AppendLine("#nullable enable");
18+
sourceBuilder.AppendLine("using AngleSharp.Dom;");
19+
sourceBuilder.AppendLine("using Microsoft.AspNetCore.Components.Web;");
20+
sourceBuilder.AppendLine("using System.Threading.Tasks;");
21+
sourceBuilder.AppendLine();
22+
sourceBuilder.AppendLine("namespace Bunit;");
23+
sourceBuilder.AppendLine();
24+
25+
GenerateExtensionsForEventArgsType(sourceBuilder, "ChangeEventArgs");
26+
GenerateExtensionsForEventArgsType(sourceBuilder, "ClipboardEventArgs");
27+
GenerateExtensionsForEventArgsType(sourceBuilder, "DragEventArgs");
28+
GenerateExtensionsForEventArgsType(sourceBuilder, "FocusEventArgs");
29+
GenerateExtensionsForEventArgsType(sourceBuilder, "KeyboardEventArgs");
30+
GenerateExtensionsForEventArgsType(sourceBuilder, "MouseEventArgs");
31+
GenerateExtensionsForEventArgsType(sourceBuilder, "PointerEventArgs");
32+
GenerateExtensionsForEventArgsType(sourceBuilder, "ProgressEventArgs");
33+
GenerateExtensionsForEventArgsType(sourceBuilder, "TouchEventArgs");
34+
GenerateExtensionsForEventArgsType(sourceBuilder, "WheelEventArgs");
35+
36+
context.AddSource("EventDispatchExtensions.g.cs", sourceBuilder.ToString());
37+
}
38+
39+
private static void GenerateExtensionsForEventArgsType(StringBuilder sourceBuilder, string eventArgsType)
40+
{
41+
var className = GetClassNameForEventArgsType(eventArgsType);
42+
sourceBuilder.AppendLine("/// <summary>");
43+
sourceBuilder.AppendLine($"/// Event dispatch helper extension methods for {eventArgsType}.");
44+
sourceBuilder.AppendLine("/// </summary>");
45+
sourceBuilder.AppendLine($"public static partial class {className}");
46+
sourceBuilder.AppendLine("{");
47+
48+
if (eventArgsType == "MouseEventArgs")
49+
{
50+
GenerateMouseEventExtensions(sourceBuilder);
51+
}
52+
else if (eventArgsType == "KeyboardEventArgs")
53+
{
54+
GenerateKeyboardEventExtensions(sourceBuilder);
55+
}
56+
else if (eventArgsType == "ChangeEventArgs")
57+
{
58+
GenerateInputEventExtensions(sourceBuilder);
59+
}
60+
else
61+
{
62+
GenerateGenericEventExtension(sourceBuilder, eventArgsType);
63+
}
64+
65+
sourceBuilder.AppendLine("}");
66+
sourceBuilder.AppendLine();
67+
}
68+
69+
private static void GenerateMouseEventExtensions(StringBuilder sourceBuilder)
70+
{
71+
var mouseEvents = new[]
72+
{
73+
("MouseOver", "onmouseover"),
74+
("MouseOut", "onmouseout"),
75+
("MouseMove", "onmousemove"),
76+
("MouseDown", "onmousedown"),
77+
("MouseUp", "onmouseup"),
78+
("Click", "onclick"),
79+
("DoubleClick", "ondblclick"),
80+
("ContextMenu", "oncontextmenu")
81+
};
82+
83+
foreach (var (methodName, eventName) in mouseEvents)
84+
{
85+
// Special treatment for Click and DoubleClick to set Detail property by default
86+
if (methodName == "Click")
87+
{
88+
GenerateAsyncEventMethodWithDefaultArgs(sourceBuilder, methodName, eventName, "MouseEventArgs", "new MouseEventArgs() { Detail = 1 }");
89+
}
90+
else if (methodName == "DoubleClick")
91+
{
92+
GenerateAsyncEventMethodWithDefaultArgs(sourceBuilder, methodName, eventName, "MouseEventArgs", "new MouseEventArgs() { Detail = 2 }");
93+
}
94+
else
95+
{
96+
GenerateAsyncEventMethod(sourceBuilder, methodName, eventName, "MouseEventArgs");
97+
}
98+
}
99+
}
100+
101+
private static void GenerateKeyboardEventExtensions(StringBuilder sourceBuilder)
102+
{
103+
var keyboardEvents = new[]
104+
{
105+
("KeyDown", "onkeydown"),
106+
("KeyUp", "onkeyup"),
107+
("KeyPress", "onkeypress")
108+
};
109+
110+
foreach (var (methodName, eventName) in keyboardEvents)
111+
{
112+
GenerateAsyncEventMethod(sourceBuilder, methodName, eventName, "KeyboardEventArgs");
113+
}
114+
}
115+
116+
private static void GenerateGenericEventExtension(StringBuilder sourceBuilder, string eventArgsType)
117+
{
118+
var methodName = eventArgsType.Replace("EventArgs", "");
119+
var eventName = $"on{methodName.ToLowerInvariant()}";
120+
GenerateAsyncEventMethod(sourceBuilder, methodName, eventName, eventArgsType);
121+
}
122+
123+
private static void GenerateAsyncEventMethod(StringBuilder sourceBuilder, string methodName, string eventName, string eventArgsType)
124+
{
125+
GenerateAsyncEventMethodWithDefaultArgs(sourceBuilder, methodName, eventName, eventArgsType, $"new {eventArgsType}()");
126+
}
127+
128+
private static void GenerateAsyncEventMethodWithDefaultArgs(StringBuilder sourceBuilder, string methodName, string eventName, string eventArgsType, string defaultArgs)
129+
{
130+
sourceBuilder.AppendLine("\t/// <summary>");
131+
sourceBuilder.AppendLine($"\t/// Raises the <c>@{eventName}</c> event on <paramref name=\"element\"/>, passing the provided <paramref name=\"eventArgs\"/>");
132+
sourceBuilder.AppendLine($"\t/// to the event handler. If <paramref name=\"eventArgs\"/> is null, a new instance of {eventArgsType} will be created.");
133+
sourceBuilder.AppendLine("\t/// </summary>");
134+
sourceBuilder.AppendLine("\t/// <param name=\"element\">The element to raise the event on.</param>");
135+
sourceBuilder.AppendLine("\t/// <param name=\"eventArgs\">The event arguments to pass to the event handler.</param>");
136+
sourceBuilder.AppendLine($"\tpublic static void {methodName}(this IElement element, {eventArgsType}? eventArgs = null) => _ = {methodName}Async(element, eventArgs);");
137+
sourceBuilder.AppendLine();
138+
139+
sourceBuilder.AppendLine("\t/// <summary>");
140+
sourceBuilder.AppendLine($"\t/// Raises the <c>@{eventName}</c> event on <paramref name=\"element\"/>, passing the provided <paramref name=\"eventArgs\"/>");
141+
sourceBuilder.AppendLine($"\t/// to the event handler. If <paramref name=\"eventArgs\"/> is null, a new instance of {eventArgsType} will be created.");
142+
sourceBuilder.AppendLine("\t/// </summary>");
143+
sourceBuilder.AppendLine("\t/// <param name=\"element\">The element to raise the event on.</param>");
144+
sourceBuilder.AppendLine("\t/// <param name=\"eventArgs\">The event arguments to pass to the event handler.</param>");
145+
sourceBuilder.AppendLine("\t/// <returns>A task that completes when the event handler is done.</returns>");
146+
sourceBuilder.AppendLine($"\tpublic static Task {methodName}Async(this IElement element, {eventArgsType}? eventArgs = null) => element.TriggerEventAsync(\"{eventName}\", eventArgs ?? {defaultArgs});");
147+
sourceBuilder.AppendLine();
148+
149+
sourceBuilder.AppendLine("\t/// <summary>");
150+
sourceBuilder.AppendLine($"\t/// Raises the <c>@{eventName}</c> event on the element returned by <paramref name=\"elementTask\"/>, passing the provided <paramref name=\"eventArgs\"/>");
151+
sourceBuilder.AppendLine($"\t/// to the event handler. If <paramref name=\"eventArgs\"/> is null, a new instance of {eventArgsType} will be created.");
152+
sourceBuilder.AppendLine("\t/// </summary>");
153+
sourceBuilder.AppendLine("\t/// <param name=\"elementTask\">A task that returns the element to raise the element on.</param>");
154+
sourceBuilder.AppendLine("\t/// <param name=\"eventArgs\">The event arguments to pass to the event handler.</param>");
155+
sourceBuilder.AppendLine("\t/// <returns>A task that completes when the event handler is done.</returns>");
156+
sourceBuilder.AppendLine($"\tpublic static async Task {methodName}Async(this Task<IElement> elementTask, {eventArgsType}? eventArgs = null)");
157+
sourceBuilder.AppendLine("\t{");
158+
sourceBuilder.AppendLine("\t\tvar element = await elementTask;");
159+
sourceBuilder.AppendLine($"\t\tawait {methodName}Async(element, eventArgs);");
160+
sourceBuilder.AppendLine("\t}");
161+
sourceBuilder.AppendLine();
162+
}
163+
164+
private static string GetClassNameForEventArgsType(string eventArgsType)
165+
{
166+
return eventArgsType switch
167+
{
168+
"MouseEventArgs" => "MouseEventDispatchExtensions",
169+
"KeyboardEventArgs" => "KeyboardEventDispatchExtensions",
170+
"ChangeEventArgs" => "InputEventDispatchExtensions",
171+
"WheelEventArgs" => "WheelEventDispatchExtensions",
172+
"FocusEventArgs" => "FocusEventDispatchExtensions",
173+
"DragEventArgs" => "DragEventDispatchExtensions",
174+
"ClipboardEventArgs" => "ClipboardEventDispatchExtensions",
175+
"TouchEventArgs" => "TouchEventDispatchExtensions",
176+
"PointerEventArgs" => "PointerEventDispatchExtensions",
177+
"ProgressEventArgs" => "ProgressEventDispatchExtensions",
178+
_ => $"{eventArgsType.Replace("EventArgs", "")}EventDispatchExtensions"
179+
};
180+
}
181+
182+
private static void GenerateInputEventExtensions(StringBuilder sourceBuilder)
183+
{
184+
var inputEvents = new[]
185+
{
186+
("Change", "onchange"),
187+
("Input", "oninput")
188+
};
189+
190+
foreach (var (methodName, eventName) in inputEvents)
191+
{
192+
GenerateAsyncEventMethod(sourceBuilder, methodName, eventName, "ChangeEventArgs");
193+
}
194+
}
195+
}

src/bunit.generators.internal/bunit.generators.internal.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@
2121
<PackageReference Include="Meziantou.Polyfill" PrivateAssets="all" IncludeAssets="Runtime;Build;Native;contentFiles;Analyzers" />
2222
</ItemGroup>
2323

24+
<ItemGroup>
25+
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers\dotnet\cs\$(AssemblyName).dll" Visible="false" />
26+
</ItemGroup>
27+
2428
</Project>

src/bunit/EventDispatchExtensions/ClipboardEventDispatchExtensions.cs

Lines changed: 0 additions & 129 deletions
This file was deleted.

src/bunit/EventDispatchExtensions/DialogEventDispatchExtensions.cs

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)