| // <copyright file="SessionUnitTests.cs" company="Selenium Committers"> | |
| // Licensed to the Software Freedom Conservancy (SFC) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The SFC licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| // </copyright> | |
| using System.Text.Json; | |
| using System.Text.Json.Nodes; | |
| using OpenQA.Selenium.BiDi; | |
| namespace OpenQA.Selenium.Tests.BiDi; | |
| [Parallelizable(ParallelScope.All)] | |
| [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] | |
| class SessionUnitTests | |
| { | |
| private IBiDi _bidi; | |
| private FakeTransport _transport; | |
| [SetUp] | |
| public async Task SetUp() | |
| { | |
| _transport = new FakeTransport(); | |
| _bidi = await Selenium.BiDi.BiDi.ConnectAsync(new Uri("ws://fake"), opts => | |
| opts.UseTransport(_ => (_, _) => Task.FromResult<ITransport>(_transport))); | |
| } | |
| [TearDown] | |
| public async Task TearDown() | |
| { | |
| await _bidi.DisposeAsync(); | |
| } | |
| [Test] | |
| public void ShouldRespectCancellationToken() | |
| { | |
| using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1)); | |
| Assert.That( | |
| () => _bidi.StatusAsync(cancellationToken: cts.Token), | |
| Throws.InstanceOf<TaskCanceledException>()); | |
| } | |
| [Test] | |
| public async Task ShouldHaveIdempotentDisposal() | |
| { | |
| await _bidi.DisposeAsync(); | |
| await _bidi.DisposeAsync(); | |
| } | |
| [Test] | |
| public async Task StatusCommandSendsCorrectMethod() | |
| { | |
| var status = await _bidi.StatusAsync() | |
| .WithResponse(_transport, """{"ready":true,"message":"running"}"""); | |
| Assert.That(status.Message, Is.EqualTo("running")); | |
| Assert.That(_transport.SentMessages, Has.Count.EqualTo(1)); | |
| using var doc = JsonDocument.Parse(_transport.SentMessages[0]); | |
| Assert.That(doc.RootElement.GetProperty("method").GetString(), Is.EqualTo("session.status")); | |
| } | |
| [Test] | |
| public async Task ErrorResponseThrowsBiDiException() | |
| { | |
| Assert.That( | |
| () => _bidi.StatusAsync() | |
| .WithErrorResponse(_transport, "invalid argument", "missing required field"), | |
| Throws.InstanceOf<BiDiException>() | |
| .With.Message.Contains("invalid argument")); | |
| } | |
| [Test] | |
| public async Task CommandIdsAreSequential() | |
| { | |
| await _bidi.StatusAsync() | |
| .WithResponse(_transport, """{"ready":true,"message":"first"}"""); | |
| await _bidi.StatusAsync() | |
| .WithResponse(_transport, """{"ready":true,"message":"second"}"""); | |
| using var doc1 = JsonDocument.Parse(_transport.SentMessages[0]); | |
| using var doc2 = JsonDocument.Parse(_transport.SentMessages[1]); | |
| var id1 = doc1.RootElement.GetProperty("id").GetInt64(); | |
| var id2 = doc2.RootElement.GetProperty("id").GetInt64(); | |
| Assert.That(id2, Is.EqualTo(id1 + 1)); | |
| } | |
| [Test] | |
| public async Task StatusResultExposesAdditionalData() | |
| { | |
| var status = await _bidi.StatusAsync() | |
| .WithResponse(_transport, """{"ready":true,"message":"running","foo":"value"}"""); | |
| Assert.That(status.AdditionalData["foo"].GetString(), Is.EqualTo("value")); | |
| } | |
| [Test] | |
| public async Task StatusResultExposesAdditionalMessageData() | |
| { | |
| var status = await _bidi.StatusAsync() | |
| .WithRawResponse(_transport, """{"foo":"topLevel","id":1,"type":"success","result":{"ready":true,"message":"running"}}"""); | |
| Assert.That(status.AdditionalMessageData["foo"].GetString(), Is.EqualTo("topLevel")); | |
| } | |
| [Test] | |
| public async Task CommandAdditionalDataIsSerializedIntoParams() | |
| { | |
| await _bidi.StatusAsync(new() | |
| { | |
| AdditionalData = new JsonObject { ["foo"] = "bar" } | |
| }).WithResponse(_transport, """{"ready":true,"message":"running"}"""); | |
| using var doc = JsonDocument.Parse(_transport.SentMessages[0]); | |
| Assert.That(doc.RootElement.GetProperty("params").GetProperty("foo").GetString(), Is.EqualTo("bar")); | |
| } | |
| [Test] | |
| public async Task CommandAdditionalMessageDataIsSerializedAsTopLevelFields() | |
| { | |
| await _bidi.StatusAsync(new() | |
| { | |
| AdditionalMessageData = """{"baz": "qux"}""" | |
| }).WithResponse(_transport, """{"ready":true,"message":"running"}"""); | |
| using var doc = JsonDocument.Parse(_transport.SentMessages[0]); | |
| Assert.That(doc.RootElement.GetProperty("baz").GetString(), Is.EqualTo("qux")); | |
| Assert.That(doc.RootElement.GetProperty("params").EnumerateObject().Any(p => p.Name == "baz"), Is.False, | |
| "AdditionalMessageData fields must be top-level, not inside params"); | |
| } | |
| [Test] | |
| public async Task ContextCommandPreservesAdditionalData() | |
| { | |
| var context = new Selenium.BiDi.BrowsingContext.BrowsingContext(_bidi, "ctx-1"); | |
| await context.GetTreeAsync(new() | |
| { | |
| AdditionalData = new JsonObject { ["foo"] = "bar" }, | |
| AdditionalMessageData = """{"trace":"enabled"}""" | |
| }).WithResponse(_transport, """{"contexts":[]}"""); | |
| using var doc = JsonDocument.Parse(_transport.SentMessages[0]); | |
| Assert.That(doc.RootElement.GetProperty("params").GetProperty("foo").GetString(), Is.EqualTo("bar")); | |
| Assert.That(doc.RootElement.GetProperty("trace").GetString(), Is.EqualTo("enabled")); | |
| } | |
| [Test] | |
| public void CommandAdditionalDataMustBeJsonObject() | |
| { | |
| Assert.That( | |
| () => new AdditionalData("""42"""), | |
| Throws.InstanceOf<ArgumentException>() | |
| .With.Message.Contains("Additional data must be a JSON object.")); | |
| } | |
| [Test] | |
| public async Task EventArgsExposesAdditionalData() | |
| { | |
| var stream = await _bidi.Script.RealmDestroyed.StreamAsync() | |
| .WithResponse(_transport, """{"subscription":"sub-1"}"""); | |
| _transport.EnqueueEvent("script.realmDestroyed", """{"realm":"r-1","foo":"extra"}"""); | |
| var received = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5)); | |
| Assert.That(received.AdditionalData["foo"].GetString(), Is.EqualTo("extra")); | |
| await stream.DisposeAsync().WithResponse(_transport); | |
| } | |
| [Test] | |
| public async Task EventArgsExposesAdditionalMessageData() | |
| { | |
| var stream = await _bidi.Script.RealmDestroyed.StreamAsync() | |
| .WithResponse(_transport, """{"subscription":"sub-1"}"""); | |
| _transport.Enqueue("""{"type":"event","method":"script.realmDestroyed","params":{"realm":"r-1"},"bar":"topLevel"}"""); | |
| var received = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5)); | |
| Assert.That(received.AdditionalMessageData["bar"].GetString(), Is.EqualTo("topLevel")); | |
| await stream.DisposeAsync().WithResponse(_transport); | |
| } | |
| [Test] | |
| public async Task StreamCanBeReadSequentially() | |
| { | |
| var stream = await _bidi.Script.RealmDestroyed.StreamAsync() | |
| .WithResponse(_transport, """{"subscription":"sub-1"}"""); | |
| _transport.EnqueueEvent("script.realmDestroyed", """{"realm":"r-1"}"""); | |
| _transport.EnqueueEvent("script.realmDestroyed", """{"realm":"r-2"}"""); | |
| var first = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5)); | |
| var second = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5)); | |
| Assert.That(first.Realm.Id, Is.EqualTo("r-1")); | |
| Assert.That(second.Realm.Id, Is.EqualTo("r-2")); | |
| await stream.DisposeAsync().WithResponse(_transport); | |
| } | |
| } |