diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx
index c113e4ce..72d70a3c 100644
--- a/ZB.MOM.WW.OtOpcUa.slnx
+++ b/ZB.MOM.WW.OtOpcUa.slnx
@@ -99,6 +99,7 @@
+
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientBrowseSessionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientBrowseSessionTests.cs
new file mode 100644
index 00000000..adadfb1e
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientBrowseSessionTests.cs
@@ -0,0 +1,76 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
+using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests;
+
+///
+/// Live-server tests against the opc-plc Docker fixture. Bring up with
+/// `lmxopcua-fix up opcuaclient` from PowerShell before running. These tests
+/// drive the internal OpcUaClientBrowseSession through its public factory
+/// so no InternalsVisibleTo is needed.
+/// Filter out with --filter "Category!=RequiresOpcPlc" when the fixture
+/// is unreachable.
+///
+[Trait("Category", "RequiresOpcPlc")]
+public sealed class OpcUaClientBrowseSessionTests
+{
+ private static string Endpoint =>
+ Environment.GetEnvironmentVariable("OPCUA_SIM_ENDPOINT") ?? "opc.tcp://10.100.0.35:50000";
+
+ // Enum values use their numeric ordinal because the browser uses default
+ // System.Text.Json with no JsonStringEnumConverter. SecurityPolicy.None,
+ // SecurityMode.None, OpcUaAuthType.Anonymous are all index 0, so omitting them
+ // also works — keeping them explicit for documentation value.
+ private static string ConfigJson => $$"""
+ {
+ "EndpointUrl":"{{Endpoint}}",
+ "SecurityPolicy":0,
+ "SecurityMode":0,
+ "AuthType":0,
+ "SessionTimeout":"00:01:00",
+ "Timeout":"00:00:10",
+ "PerEndpointConnectTimeout":"00:00:10"
+ }
+ """;
+
+ /// RootAsync should surface at least one node under ObjectsFolder
+ /// (opc-plc exposes a non-empty top level).
+ [Fact]
+ public async Task RootAsync_returns_at_least_one_node()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var browser = new OpcUaClientDriverBrowser();
+ await using var session = await browser.OpenAsync(ConfigJson, ct);
+ var roots = await session.RootAsync(ct);
+ roots.Count.ShouldBeGreaterThan(0);
+ }
+
+ /// ExpandAsync should accept a stable NodeId returned by RootAsync
+ /// and round-trip through the live namespace table.
+ [Fact]
+ public async Task ExpandAsync_round_trips_stable_NodeId()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var browser = new OpcUaClientDriverBrowser();
+ await using var session = await browser.OpenAsync(ConfigJson, ct);
+ var roots = await session.RootAsync(ct);
+ var folder = roots.FirstOrDefault(n => n.Kind == BrowseNodeKind.Folder);
+ folder.ShouldNotBeNull("expected at least one Folder under ObjectsFolder");
+ var children = await session.ExpandAsync(folder!.NodeId, ct);
+ children.ShouldNotBeNull();
+ }
+
+ /// The OPC UA picker treats variables as terminal leaves, so
+ /// AttributesAsync must always be empty for this driver.
+ [Fact]
+ public async Task AttributesAsync_is_empty_for_opcuaclient()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var browser = new OpcUaClientDriverBrowser();
+ await using var session = await browser.OpenAsync(ConfigJson, ct);
+ var attrs = await session.AttributesAsync("nsu=http://opcfoundation.org/UA/;i=85", ct);
+ attrs.ShouldBeEmpty();
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientDriverBrowserTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientDriverBrowserTests.cs
new file mode 100644
index 00000000..c61cbd47
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientDriverBrowserTests.cs
@@ -0,0 +1,52 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests;
+
+///
+/// Unit-only coverage of 's pre-connect
+/// validation. These tests do not require a live OPC UA endpoint and are safe to
+/// run without the opc-plc Docker fixture.
+///
+[Trait("Category", "Unit")]
+public sealed class OpcUaClientDriverBrowserTests
+{
+ private readonly OpcUaClientDriverBrowser _sut = new();
+
+ /// The DriverType key must match the AdminUI's persisted value.
+ [Fact]
+ public void DriverType_is_OpcUaClient() => _sut.DriverType.ShouldBe("OpcUaClient");
+
+ /// An empty endpoint must fail fast with a clear EndpointUrl-mentioning message.
+ [Fact]
+ public async Task OpenAsync_with_empty_endpoint_throws()
+ {
+ var json = """{"EndpointUrl":"","EndpointUrls":[]}""";
+ var ex = await Should.ThrowAsync(
+ () => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
+ ex.Message.ShouldContain("EndpointUrl");
+ }
+
+ /// A JSON literal that deserializes to null must fail fast.
+ [Fact]
+ public async Task OpenAsync_with_null_json_throws()
+ {
+ var ex = await Should.ThrowAsync(
+ () => _sut.OpenAsync("null", TestContext.Current.CancellationToken));
+ ex.Message.ShouldContain("null");
+ }
+
+ /// Certificate auth is not supported by the browser; the failure message
+ /// must say so explicitly rather than surfacing a downstream COM/SDK error.
+ /// OpcUaAuthType.Certificate serializes as the numeric value 2 under the
+ /// browser's default System.Text.Json options (no string-enum converter).
+ [Fact]
+ public async Task OpenAsync_with_certificate_auth_throws_clear_message()
+ {
+ var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":2}""";
+ var ex = await Should.ThrowAsync(
+ () => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
+ ex.Message.ShouldContain("Certificate");
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj
new file mode 100644
index 00000000..8caa7d5b
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ true
+ ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+