Skip to content

Commit 049e410

Browse files
authored
Merge pull request #3 from g4-api/development
Development
2 parents 791d53d + 77d2ba2 commit 049e410

39 files changed

Lines changed: 1520 additions & 297 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<!-- Assembly -->
5+
<TargetFramework>net10.0</TargetFramework>
6+
<AssemblyVersion>10.0.0.0</AssemblyVersion>
7+
<FileVersion>10.0.0.0</FileVersion>
8+
<IsPackable>false</IsPackable>
9+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
10+
<SatelliteResourceLanguages>en-US</SatelliteResourceLanguages>
11+
<LangVersion>latest</LangVersion>
12+
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
13+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
14+
15+
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib1040-1049 -->
16+
<NoWarn>$(NoWarn);SYSLIB1045;IDE0130;SYSLIB1054;CA2101;S4200;MSB3305</NoWarn>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\Common.Domain\Common.Domain.csproj" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Content Include="..\ChromiumPeek.Extension\**\*.*">
25+
<Link>ChromiumExtension\%(RecursiveDir)%(FileName)%(Extension)</Link>
26+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
27+
</Content>
28+
</ItemGroup>
29+
30+
</Project>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using ChromiumPeek.Domain.Models;
2+
3+
namespace UiaPeek.Domain
4+
{
5+
/// <summary>
6+
/// Represents a repository for accessing UI Automation elements and their ancestor chains.
7+
/// </summary>
8+
public class ChromiumPeekRepository : IChromiumPeekRepository
9+
{
10+
public ChromiumChainModel Peek()
11+
{
12+
throw new System.NotImplementedException();
13+
}
14+
15+
public ChromiumChainModel Peek(int x, int y)
16+
{
17+
throw new System.NotImplementedException();
18+
}
19+
}
20+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using Microsoft.AspNetCore.SignalR;
2+
3+
using System;
4+
using System.Collections.Concurrent;
5+
using System.Threading.Tasks;
6+
7+
using Common.Domain.Models;
8+
using UiaPeek.Domain;
9+
using ChromiumPeek.Domain.Models;
10+
11+
namespace ChromiumPeek.Domain.Hubs
12+
{
13+
/// <summary>
14+
/// SignalR hub for handling UI Automation (UIA) peek operations.
15+
/// Provides real-time communication for heartbeat checks and
16+
/// ancestor chain inspection at specific screen coordinates.
17+
/// </summary>
18+
public class ChromiumPeekHub(IChromiumPeekRepository repository) : Hub
19+
{
20+
// Collection of active recording sessions keyed by a unique session id.
21+
private readonly static ConcurrentDictionary<string, ConcurrentBag<ChromiumChainModel>> s_sessions = new();
22+
23+
// Repository used for querying UIA elements at coordinates.
24+
private readonly IChromiumPeekRepository _repository = repository;
25+
26+
// Sends a heartbeat message to the caller.
27+
// This can be used by clients to verify the connection is alive.
28+
[HubMethodName(name: nameof(SendHeartbeat))]
29+
public Task SendHeartbeat()
30+
{
31+
// Notify the calling client with a heartbeat message.
32+
return Clients.Caller.SendAsync(
33+
method: "ReceiveHeartbeat",
34+
arg1: new HubResponseModel("Heartbeat received - connection is alive"));
35+
}
36+
37+
// Resolves the UIA element at the given screen coordinates and
38+
// returns its ancestor chain back to the caller.
39+
[HubMethodName(name: $"{nameof(SendPeek)}At")]
40+
public Task SendPeek(RecorderPointModel point)
41+
{
42+
// Query the repository to get the UIA ancestor chain at the given coordinates.
43+
var peekResponse = _repository.Peek(x: point.XPos, y: point.YPos);
44+
45+
// Send the result back to the calling client.
46+
return Clients.Caller.SendAsync(
47+
method: "ReceivePeek",
48+
arg1: new HubResponseModel(peekResponse));
49+
}
50+
51+
// Resolves the UIA element at the given screen coordinates and
52+
// returns its ancestor chain back to the caller.
53+
[HubMethodName(name: $"{nameof(SendPeek)}Focused")]
54+
public Task SendPeek()
55+
{
56+
// Query the repository to get the UIA ancestor chain from the currently focused element.
57+
var peekResponse = _repository.Peek();
58+
59+
// Send the result back to the calling client.
60+
return Clients.Caller.SendAsync(
61+
method: "ReceivePeek",
62+
arg1: new HubResponseModel(peekResponse));
63+
}
64+
65+
// Starts a new recording session for the current SignalR caller.
66+
[HubMethodName(name: $"{nameof(StartRecordingSession)}")]
67+
public Task StartRecordingSession()
68+
{
69+
// Generate a unique identifier for this caller's recording session.
70+
var session = Guid.NewGuid().ToString();
71+
72+
// Initialize storage for this session's recorded events/actions.
73+
// Assumes `_sessions` is a (thread-safe) dictionary keyed by session id.
74+
s_sessions[session] = [];
75+
76+
// Notify ONLY the invoking client that the session has started and
77+
// return the session id as the payload. The client should listen to
78+
// "RecordingSessionStarted" and extract the `Value` field.
79+
return Clients.Caller.SendAsync(
80+
method: "RecordingSessionStarted",
81+
arg1: new HubResponseModel(session));
82+
}
83+
84+
// Stops an existing recording session for the current SignalR caller.
85+
[HubMethodName(name: $"{nameof(StopRecordingSession)}")]
86+
public Task StopRecordingSession(string session)
87+
{
88+
// Remove the session from the active sessions collection.
89+
s_sessions.TryRemove(session, out var chains);
90+
91+
// Notify ONLY the invoking client that the session has stopped.
92+
return Clients.Caller.SendAsync(
93+
method: "RecordingSessionStopped",
94+
arg1: new HubResponseModel(chains));
95+
}
96+
97+
/// <summary>
98+
/// Lightweight envelope for hub-to-client messages that carry a single value.
99+
/// </summary>
100+
/// <param name="value">The payload to send to the client.</param>
101+
private sealed class HubResponseModel(object value)
102+
{
103+
/// <summary>
104+
/// The payload carried by this response. Using <see cref="object"/> allows
105+
/// any serializable value (string, number, DTO, etc.).
106+
/// </summary>
107+
public object Value { get; init; } = value;
108+
}
109+
}
110+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using ChromiumPeek.Domain.Models;
2+
3+
namespace UiaPeek.Domain
4+
{
5+
/// <summary>
6+
/// Represents a repository for accessing UI Automation elements and their ancestor chains.
7+
/// </summary>
8+
public interface IChromiumPeekRepository
9+
{
10+
/// <summary>
11+
/// Retrieves the ancestor chain of the UI Automation element located at the given screen coordinates.
12+
/// </summary>
13+
/// <param name="x">The X-coordinate on the screen.</param>
14+
/// <param name="y">The Y-coordinate on the screen.</param>
15+
/// <returns>A <see cref="ChromiumChainModel"/> representing the ancestor chain of the element at the specified point, or <c>null</c> if no element is found.</returns>
16+
ChromiumChainModel Peek();
17+
18+
/// <summary>
19+
/// Retrieves the currently focused UI Automation element and constructs
20+
/// its ancestor chain representation, including an absolute XPath locator.
21+
/// </summary>
22+
/// <returns>A <see cref="ChromiumChainModel"/> representing the focused element and its ancestors,or an empty model if no element is currently focused.</returns>
23+
ChromiumChainModel Peek(int x, int y);
24+
}
25+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Common.Domain.Models;
2+
3+
namespace ChromiumPeek.Domain.Models
4+
{
5+
/// <summary>
6+
/// Represents a chain of UIA (UI Automation) nodes recorded by UiaPeek.
7+
/// Uses <see cref="ChromiumNodeModel"/> as the node type.
8+
///
9+
/// This class acts as a strongly-typed alias for <see cref="ChainModel{TNode}"/>,
10+
/// providing clearer intent within the UIA domain. Extend this class when
11+
/// UIA-specific chain metadata or behavior is required.
12+
/// </summary>
13+
public class ChromiumChainModel : ChainModel<ChromiumNodeModel>
14+
{
15+
// Intentionally empty — provides a domain-specific type for UIA chains.
16+
// Add members here if UIA chains require additional metadata or logic.
17+
}
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Common.Domain.Models;
2+
3+
namespace ChromiumPeek.Domain.Models
4+
{
5+
/// <summary>
6+
/// Represents a UI Automation (UIA) event captured by the UiaPeek tool.
7+
/// Inherits from <see cref="RecorderEventModel{TChain}"/> using <see cref="ChromiumChainModel"/>
8+
/// as the event chain type.
9+
///
10+
/// This model serves as the strongly-typed event payload used throughout the
11+
/// recorder pipeline. It provides structure, metadata, and a chain of actions
12+
/// that describe the recorded UI interaction sequence.
13+
///
14+
/// The class does not add new members — all functionality is inherited —
15+
/// but it allows the domain layer to work with UIA-specific event chains
16+
/// without needing to reference the generic base everywhere.
17+
/// </summary>
18+
public class ChromiumEventModel : RecorderEventModel<ChromiumChainModel>
19+
{
20+
// Intentionally empty — this class acts as a domain-specific alias
21+
// so consumers can work with a UiaEventModel rather than a generic type.
22+
// Extend here in the future if UIA events require custom fields or behavior.
23+
}
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Common.Domain.Models;
2+
3+
namespace ChromiumPeek.Domain.Models
4+
{
5+
/// <summary>
6+
/// Represents a single UI Automation (UIA) node within a recorded chain.
7+
/// Wraps an <see cref="IUIAutomationElement"/> as the underlying UI element.
8+
///
9+
/// This class is a strongly-typed alias for <see cref="RecorderNodeModel{TElement}"/>,
10+
/// allowing the recorder pipeline to work specifically with UIA elements.
11+
/// Extend this class when UIA nodes require additional metadata, properties,
12+
/// or domain-specific behavior.
13+
/// </remarks>
14+
public class ChromiumNodeModel : RecorderNodeModel<object>
15+
{
16+
// Intentionally empty — provides a domain-specific node type for UIA recordings.
17+
// Add UIA-specific fields or logic here if needed in future.
18+
}
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// background.js (MV3 service worker)
2+
3+
// Simple startup log so we know the extension is alive.
4+
chrome.runtime.onInstalled.addListener(() => {
5+
console.log("[ChromiumPeek] Extension installed.");
6+
});
7+
8+
// ONLY if you really want to handle clicks directly (and NOT just use the popup)
9+
if (chrome.action && chrome.action.onClicked && chrome.action.onClicked.addListener) {
10+
chrome.action.onClicked.addListener((tab) => {
11+
console.log("[ChromiumPeek] Action icon clicked on tab:", tab.id);
12+
});
13+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// content-script.js
2+
3+
console.log("[ChromiumPeek] Hello from content script on:", window.location.href);
4+
5+
// As a visible smoke test, mark the page once:
6+
if (!window.__chromiumPeekHelloInjected) {
7+
window.__chromiumPeekHelloInjected = true;
8+
9+
const banner = document.createElement("div");
10+
banner.textContent = "ChromiumPeek: Hello World!";
11+
banner.style.position = "fixed";
12+
banner.style.zIndex = 999999;
13+
banner.style.bottom = "10px";
14+
banner.style.right = "10px";
15+
banner.style.padding = "6px 10px";
16+
banner.style.borderRadius = "4px";
17+
banner.style.fontFamily = "system-ui, sans-serif";
18+
banner.style.fontSize = "12px";
19+
banner.style.background = "rgba(0, 0, 0, 0.8)";
20+
banner.style.color = "#fff";
21+
banner.style.pointerEvents = "none";
22+
23+
document.body.appendChild(banner);
24+
25+
// Auto-remove after a few seconds
26+
setTimeout(() => banner.remove(), 3000);
27+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// popup.js
2+
3+
let connection = null;
4+
5+
document.addEventListener("DOMContentLoaded", () => {
6+
const button = document.getElementById("hello-btn");
7+
const status = document.getElementById("status");
8+
9+
button.addEventListener("click", async () => {
10+
// Already connected? Don't reconnect.
11+
if (connection && connection.state === "Connected") {
12+
status.textContent = "Already connected to Peek hub.";
13+
return;
14+
}
15+
16+
status.textContent = "Connecting to Peek hub...";
17+
18+
try {
19+
// Create the connection if we don't have one yet
20+
if (!connection) {
21+
connection = new signalR.HubConnectionBuilder()
22+
.withUrl("http://localhost:9956/hub/v4/g4/peek")
23+
.withAutomaticReconnect()
24+
.build();
25+
26+
// Optional: basic logging
27+
connection.onreconnecting((error) => {
28+
console.warn("[ChromiumPeek] Reconnecting to hub...", error);
29+
status.textContent = "Reconnecting to Peek hub...";
30+
});
31+
32+
connection.onreconnected((connectionId) => {
33+
console.log("[ChromiumPeek] Reconnected to hub:", connectionId);
34+
status.textContent = "Reconnected to Peek hub.";
35+
});
36+
37+
connection.onclose((error) => {
38+
console.warn("[ChromiumPeek] Connection closed:", error);
39+
status.textContent = "Connection closed.";
40+
});
41+
42+
// Later you'll add handlers like:
43+
// connection.on("SomeServerEvent", data => { ... });
44+
}
45+
46+
// Start the connection
47+
await connection.start();
48+
console.log("[ChromiumPeek] Connected to hub.");
49+
status.textContent = "Connected to Peek hub.";
50+
51+
} catch (err) {
52+
console.error("[ChromiumPeek] Failed to connect to hub:", err);
53+
status.textContent = "Failed to connect – see console.";
54+
}
55+
});
56+
});

0 commit comments

Comments
 (0)