meetup app osaka@6 に登壇します。
meetupapp.connpass.com
推しの技術を発表するという企画。僕の押しの技術はSeleniumとFriendlyなんですが、最近は仕事でBlazorもやってます。でこの三つなんですがじゃあ全部入りでやってみようと作ってみました。コードはここに置いてます。
github.com
使い方
結果から。使い方としてこんな感じでできるようになりました。
[Test] publicvoid Counter() { var driver = new ChromeDriver(); driver!.Url = "https://localhost:7128/counter"; //ロードされるまで待つwhile (driver.Title != "Counter") Thread.Sleep(100); //Blazor操作用オブジェクト作成 var app = new BlazorAppFriend(driver); //コンポーネント検索 var counter = app.FindComponentByType("BlazorApp.Pages.Counter"); //APIを直接操作 counter.currentCount = 1000; counter.StateHasChanged(); }
オブジェクトの生成とか配列の書き換えもOK
[Test] publicvoid FetchData() { var driver = new ChromeDriver(); driver!.Url = "https://localhost:7128/fetchdata"; //ロードされるまで待つwhile (driver.Title != "Weather forecast") Thread.Sleep(100); var app = new BlazorAppFriend(driver); //コンポーネント取得 var fetchData = app.FindComponentByType("BlazorApp.Pages.FetchData"); //お天気データをBlazorアプリ内に生成する var forecasts = app.Type("BlazorApp.Pages.FetchData+WeatherForecast[]")(1); forecasts[0] = app.Type("BlazorApp.Pages.FetchData+WeatherForecast")(); forecasts[0].Date = DateTime.Now; forecasts[0].TemperatureC = 3; forecasts[0].Summary = "Friendly!"; //セット fetchData.forecasts = forecasts; fetchData.StateHasChanged(); }
残念なことにWindowsアプリのように何の仕掛けもなくってわけにはいきませんでした。Blazorアプリ側でもライブラリの参照と一行だけ呼び出しが必要です。App.razorのOnInitializeでこれ呼びます。
protectedoverridevoid OnInitialized() => Selenium.Friendly.Blazor.BlazorController.Initialize(this);
実装
Friendlyは外部から対象アプリのAPIを呼び出すものです。簡単に言うとリフレクション情報を対象アプリに渡して実行させています。Windowsアプリではdllインジェクションとか駆使しつつ、最終的にはWindowメッセージでプロセス間通信をしています。
ではSelenium→Blazorではどうすればいいのでしょうか?
調べてみるとBlazorではJavaScriptから.Netのメソッドを呼び出すことができるようです。そしてSeleniumはJavaScriptを呼び出せる。この二つを組み合わせたらできそうです。
JSInvokable
docs.microsoft.com
同期、非同期の呼び出しをそれぞれサポートしています。これを使います。やりたいことはAPI実行情報を渡して結果を戻してもらいたいです。データはJsonで受け渡しするのでstring型の引数と戻り値を付けます。文字列で受け取ったものをオブジェクトにして実行して戻り値をさらにJsonのテキストにして返す感じです。DotNetFriendlyControlはWindowsアプリ用のをほとんどそのままコピってきました。
using Selenium.Friendly.Blazor.DotNetExecutor; using Microsoft.JSInterop; namespace Selenium.Friendly.Blazor { publicstaticclass JSInterface { static DotNetFriendlyControl _ctrl = new DotNetFriendlyControl(); internalstaticbool FriendlyAccessEnabled { get; set; } [JSInvokable] publicstaticstring ExecuteFriendly(string args) { if (!FriendlyAccessEnabled) thrownew NotSupportedException(); return _ctrl.Execute(args); } } }
これを書いておくとJavaScriptからこんな感じで呼ぶことができます。
DotNet.invokeMethod("Selenium.Friendly.Blazor", "ExecuteFriendly", args);
ExecuteScript
次はテストコードでの呼び出しです。Seleniumと依存関係つけるの面倒だったので一旦object渡しでもらうようにしてます。
internalstatic ReturnInfo SendAndRecieve(object wedbDriver, ProtocolInfo data) { var arg = JsonConvert.SerializeObject(data); var src = ((dynamic)wedbDriver).ExecuteScript(@"var arg = arguments[0];return DotNet.invokeMethod(""Selenium.Friendly.Blazor"", ""ExecuteFriendly"", arg);", arg); var ret = JsonConvert.DeserializeObject<ReturnInfo>((string)src); ret.SetReturnValueFromJson(); return ret; }
これでSeleniumからBlazorアプリまでの通信経路ができました。
残りの実装
この経路ができたら残りの実装はWindows用のFriendlyの実装をコピってWindows用のところを削除したり↑の実装に差し替えたりって感じでした。実はFriendly作るときに色々考えてインターフェイスはそのままに通信経路だけ変えて様々なものを操作できるようにって目論んでたんですよ。その結果無駄に複雑になって今にして思えばやっちまったって感じではあります。でもそのおかげでコード流用はやりやすくて割とサクッとできました。
Blazor用の機能
Componentを探す機能をつけてみました。
var fetchData = app.FindComponentByType("BlazorApp.Pages.FetchData");
どうやってるかと言うと、アウトなことやってます。(まあこれは勉強会用のネタライブラリなんで)
このコードを見てみると今は ComponetBase→RenderHandle→Renderer→ComponetのDictionary って感じで持っていたので(private)それをリフレクションで手繰りました。
github.com
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.RenderTree; using System.Reflection; namespace Selenium.Friendly.Blazor { publicclass BlazorController { static ComponentBase _app; publicstaticvoid Initialize(ComponentBase app) { _app = app; JSInterface.FriendlyAccessEnabled = true; } publicstatic ComponentBase FindComponentByType(string typeFullName) { var list = new List<ComponentBase>(); GetDescendants(_app, list); return list.Where(x => x.GetType().FullName == typeFullName).FirstOrDefault(); } publicstatic List<ComponentBase> GetDescendants(ComponentBase parent, List<ComponentBase> list) { list.Add(parent); //今はこれで下位のコンポーネントを取ってこれるみたい/* foreach (var e in parent._renderHandle._renderer._componentStateById) { var child = e.ValueComponent; } */ var flgs = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; var _renderHandleField = typeof(ComponentBase).GetField("_renderHandle", flgs); var _rendererFiled = typeof(RenderHandle).GetField("_renderer", flgs); var _componentStateByIdField = typeof(Renderer).GetField("_componentStateById", flgs); var _renderHandle = _renderHandleField.GetValue(parent); var _renderer = _rendererFiled.GetValue(_renderHandle); dynamic _componentStateById = _componentStateByIdField.GetValue(_renderer); foreach (object e in _componentStateById) { var valueField = e.GetType().GetProperty("Value", flgs); var obj = valueField.GetValue(e); if (obj == null) continue; var prop = obj.GetType().GetProperty("Component", flgs); var val = prop.GetValue(obj); var child = val as ComponentBase; if (child == null) continue; if (list.Contains(child)) continue; GetDescendants(child, list); } return list; } } }
結論
これ使わなくてもSeleniumだけでテストしたらいいと思いましたw