1

I am creating a browser automation library that is capable of using Puppeteer(automates chromium) and Selenium(automates all major browsers), but the goal is to also be able to add more libraries in the future.

I have an Enum that defines a couple of possible browsers to use

public enum ExternalBrowserType
{
    ChromeSelenium,
    FirefoxSelenium,
    ChromiumPuppeteer
}

And an ExternalBrowser class that can be instantiated and will let the user control the browser he wants.

public class ExternalBrowser
{
    public ExternalBrowserType Type { get; private set; }
    public ExternalBrowserMouse Mouse { get; set; }

    public ExternalBrowser(ExternalBrowserType externalBrowserType)
    {
        Type = externalBrowserType;
    }
}

Here comes the problem, obviously these libraries have different APIs, for instance, to move the mouse inside the browsers:

  • Selenium : new Actions(IWebDriver).MoveByOffset(x, y);
  • Puppeteer: await Page.Mouse.MoveAsync(x, y);

Ok that can be solved by creating an Abstract Class called ExternalBrowserMouse:

public abstract class ExternalBrowserMouse : ExternalBrowserModule
{
    public Point CurrentPosition;

    public ExternalBrowserMouse(ExternalBrowser externalBrowser) : base(externalBrowser)
    {
    }

    public abstract void MoveByOffset(int x, int y);
}

ExternalBrowserModule:

public class ExternalBrowserModule
{
    protected ExternalBrowser ExternalBrowser;
    protected object BrowserController;

    public ExternalBrowserModule(ExternalBrowser externalBrowser)
    {
        ExternalBrowser = externalBrowser;
    }
}

And then deriving from this base class to construct the implementers for Puppeteer and Selenium.

Then based on the enum value I should instantiate the right classes that match the browser version chosen by the user, that works.

But, lets say I want to define new behaviours for the ExternalBrowserMouse, by inheriting it, for example, I want it to move in a more human way with a defined path and not instantly like it does by default, how would I do that?

1 Answer 1

0

I see the command pattern coming into play here. Using this pattern, you could separate the actions that each browser would need to implement and have the flexibility to implement new APIs down the road. This also allows you to implement variations in your actions, e.g. you can have a command for MoveMouse and MoveMouseLikeHuman.

The Point class is referenced via using System.Drawing;

First, we define our interfaces and base classes:

// The receiver interface defines the contract for receivers implemented
// by each external browser. Each one will need to implement this
// interface. Additional actions are added here and in turn implemented 
// by external browser receiver implementations
public interface IBrowserReceiver
{
    void MoveMouse(Point position);
    void ClickButton(Point position);
    void Delay(int delaySeconds);

    // additional external browser actions defined here
    // ...
}

// the command base is implemented by each command action
public abstract class BrowserCommandBase
{
    protected readonly IBrowserReceiver Receiver;
    protected BrowserCommandBase(IBrowserReceiver receiver)
    {
        this.Receiver = receiver ?? throw new ArgumentNullException(nameof(receiver));
    }

    public abstract void Execute();
}

// this class is implemented by each external browser implementation and is used
// to queue multiple commands and invoke them sequentially 
public abstract class ExternalBrowserBase
{
    private readonly Queue<BrowserCommandBase> commandQueue;
    public ExternalBrowserType Type { get; }
    protected ExternalBrowserBase(ExternalBrowserType type)
    {
        this.Type = type;
        commandQueue = new Queue<BrowserCommandBase>();
    }
    public void QueueCommand(BrowserCommandBase command)
    {
        commandQueue.Enqueue(command);
    }
    public void ExecuteCommands()
    {
        foreach (BrowserCommandBase command in commandQueue)
        {
            command.Execute();
        }
    }
    public void ClearCommands()
    {
        commandQueue.Clear();
    }
}

… and your enum:

public enum ExternalBrowserType
{
    ChromeSelenium,
    FirefoxSelenium,
    ChromiumPuppeteer
}

Next, we define our receiver implementations. These classes implement the receiver interface for the particular browser, allowing each browser to implement the actions as required for their specific API; i.e. MoveMouse will be implemented differently for the Selenium and the Puppeteer APIs

public class ChromeSeleniumReceiver : IBrowserReceiver
{
    public void MoveMouse(Point position)
    {
        Console.WriteLine($"{nameof(ChromeSeleniumReceiver)} : {nameof(MoveMouse)}; {position.ToString()}");
    }
    public void ClickButton(Point position)
    {
        Console.WriteLine($"{nameof(ChromeSeleniumReceiver)} : {nameof(ClickButton)}; {position.ToString()}");
    }
    public void Delay(int delaySeconds)
    {
        Console.WriteLine($"{nameof(ChromeSeleniumReceiver)} : {nameof(Delay)}; delaying for {delaySeconds.ToString()} second(s)");
    }
}

public class ChromiumPuppeteerReceiver : IBrowserReceiver
{
    public void MoveMouse(Point position)
    {
        Console.WriteLine($"{nameof(ChromiumPuppeteerReceiver)} : {nameof(MoveMouse)}; {position.ToString()}");
    }
    public void ClickButton(Point position)
    {
        Console.WriteLine($"{nameof(ChromiumPuppeteerReceiver)} : {nameof(ClickButton)}; {position.ToString()}");
    }
    public void Delay(int delaySeconds)
    {
        Console.WriteLine($"{nameof(ChromiumPuppeteerReceiver)} : {nameof(Delay)}; delaying for {delaySeconds.ToString()} second(s)");
    }
}

public class FirefoxSeleniumReceiver : IBrowserReceiver
{
    public void MoveMouse(Point position)
    {
        Console.WriteLine($"{nameof(FirefoxSeleniumReceiver)} : {nameof(MoveMouse)}; {position.ToString()}");
    }
    public void ClickButton(Point position)
    {
        Console.WriteLine($"{nameof(FirefoxSeleniumReceiver)} : {nameof(ClickButton)}; {position.ToString()}");
    }
    public void Delay(int delaySeconds)
    {
        Console.WriteLine($"{nameof(FirefoxSeleniumReceiver)} : {nameof(Delay)}; delaying for {delaySeconds.ToString()} second(s)");
    }
}

Command implementations:

The commands call into the Receiver implemented by each external browser.

public class MoveMouseCommand : BrowserCommandBase
{
    private readonly Point position;
    public MoveMouseCommand(IBrowserReceiver receiver, Point position)
        : base(receiver)
    {
        this.position = position;
    }
    public override void Execute()
    {
        this.Receiver.MoveMouse(position);
    }
}

// moves the mouse gradually from the start point to the end point by
// repeatedly calling the move mouse command until the end point is reached
public class MoveMouseLikeHumanCommand : BrowserCommandBase
{
    private readonly Point fromPosition;
    private readonly Point toPosition;
    public MoveMouseLikeHumanCommand(IBrowserReceiver receiver, 
        Point fromPosition, Point toPosition)
        : base(receiver)
    {
        this.fromPosition = fromPosition;
        this.toPosition = toPosition;
    }
    public override void Execute()
    {
        Point startPosition = fromPosition;
        Point lastPosition = toPosition;
        Point currentPosition = startPosition;
        while (currentPosition != lastPosition)
        {
            this.Receiver.MoveMouse(currentPosition); // call move mouse command
            currentPosition = GetNextMovePoint(startPosition, lastPosition);
            startPosition = currentPosition;
        }
        this.Receiver.MoveMouse(lastPosition);
    }

    private Point GetNextMovePoint(Point start, Point end)
    {
        int newX = GetNextMoveValue(start.X, end.X);
        int newY = GetNextMoveValue(start.Y, end.Y);
        return new Point(newX, newY);
    }

    private static int GetNextMoveValue(int start, int end)
    {
        if (start < end)
        {
            return start + 1;
        }
        if (start > end)
        {
            return start - 1;
        }
        return start;
    }
}

public class ClickButtonCommand : BrowserCommandBase
{
    private readonly Point position;
    public ClickButtonCommand(IBrowserReceiver receiver, Point position) 
        : base(receiver)
    {
        this.position = position;
    }
    public override void Execute()
    {
        this.Receiver.ClickButton(this.position);
    }
}

public class DelayCommand : BrowserCommandBase
{
    private readonly int delaySeconds;
    public DelayCommand(IBrowserReceiver receiver, int delaySeconds) : base(receiver)
    {
        this.delaySeconds = delaySeconds;
    }

    public override void Execute()
    {
        base.Receiver.Delay(this.delaySeconds);
    }
}

Last, we implement our external browsers that inherit from the ExternalBrowserBase class.

public class ChromeSeleniumExternalBrowser : ExternalBrowserBase
{
    public ChromeSeleniumExternalBrowser() : base(ExternalBrowserType.ChromeSelenium)
    {
    }
}

public class ChromiumPuppeteerExternalBrowser : ExternalBrowserBase
{
    public ChromiumPuppeteerExternalBrowser() : base(ExternalBrowserType.ChromiumPuppeteer)
    {
    }
}

public class FirefoxSeleniumExternalBrowser : ExternalBrowserBase
{
    public FirefoxSeleniumExternalBrowser() : base(ExternalBrowserType.FirefoxSelenium)
    {
    }
}

And we use it like so (sorry, my variable names are terrible):

ChromeSeleniumExternalBrowser:

ChromeSeleniumExternalBrowser chromeSelenium = new ChromeSeleniumExternalBrowser();
ChromeSeleniumReceiver chromeSeleniumReceiver = new ChromeSeleniumReceiver();
MoveMouseCommand chromeSeleniumMoveMouseCommand = 
    new MoveMouseCommand(chromeSeleniumReceiver, new Point(100, 110));
chromeSelenium.QueueCommand(chromeSeleniumMoveMouseCommand);
DelayCommand chromeSeleniumDelayCommand = new DelayCommand(chromeSeleniumReceiver, 5);
chromeSelenium.QueueCommand(chromeSeleniumDelayCommand);
ClickButtonCommand chromeSeleniumClickButtonCommand = 
    new ClickButtonCommand(chromeSeleniumReceiver, new Point(100, 100));
chromeSelenium.QueueCommand(chromeSeleniumClickButtonCommand);
MoveMouseLikeHumanCommand chromeSeleniumLerpMoveMouseCommand = 
    new MoveMouseLikeHumanCommand(chromeSeleniumReceiver, 
    new Point(100, 110), new Point(115, 120));
chromeSelenium.QueueCommand(chromeSeleniumLerpMoveMouseCommand);
chromeSelenium.ExecuteCommands();

// ChromeSeleniumExternalBrowser output:
// ChromeSeleniumReceiver : MoveMouse; {X=100,Y=110}
// ChromeSeleniumReceiver : Delay; delaying for 5 second(s)
// ChromeSeleniumReceiver : ClickButton; {X=100,Y=100}
// ChromeSeleniumReceiver : MoveMouse; {X=100,Y=110}
// ChromeSeleniumReceiver : MoveMouse; {X=101,Y=111}
// ChromeSeleniumReceiver : MoveMouse; {X=102,Y=112}
// ChromeSeleniumReceiver : MoveMouse; {X=103,Y=113}
// ChromeSeleniumReceiver : MoveMouse; {X=104,Y=114}
// ChromeSeleniumReceiver : MoveMouse; {X=105,Y=115}
// ChromeSeleniumReceiver : MoveMouse; {X=106,Y=116}
// ChromeSeleniumReceiver : MoveMouse; {X=107,Y=117}
// ChromeSeleniumReceiver : MoveMouse; {X=108,Y=118}
// ChromeSeleniumReceiver : MoveMouse; {X=109,Y=119}
// ChromeSeleniumReceiver : MoveMouse; {X=110,Y=120}
// ChromeSeleniumReceiver : MoveMouse; {X=111,Y=120}
// ChromeSeleniumReceiver : MoveMouse; {X=112,Y=120}
// ChromeSeleniumReceiver : MoveMouse; {X=113,Y=120}
// ChromeSeleniumReceiver : MoveMouse; {X=114,Y=120}
// ChromeSeleniumReceiver : MoveMouse; {X=115,Y=120}

FirefoxSeleniumExternalBrowser:

FirefoxSeleniumExternalBrowser firefoxSeleniumPilot = new FirefoxSeleniumExternalBrowser();
FirefoxSeleniumReceiver firefoxSeleniumReceiver = new FirefoxSeleniumReceiver();
MoveMouseCommand firefoxSeleniumMoveMouseCommand = 
    new MoveMouseCommand(firefoxSeleniumReceiver, new Point(50, 55));
firefoxSeleniumPilot.QueueCommand(firefoxSeleniumMoveMouseCommand);
ClickButtonCommand firefoxSeleniumClickButtonCommand = 
    new ClickButtonCommand(firefoxSeleniumReceiver, new Point(50, 55));
firefoxSeleniumPilot.QueueCommand(firefoxSeleniumClickButtonCommand);
MoveMouseLikeHumanCommand firefoxSeleniumLerpMoveMouseCommand = 
    new MoveMouseLikeHumanCommand(firefoxSeleniumReceiver, 
    new Point(50, 65), new Point(59, 69));
firefoxSeleniumPilot.QueueCommand(firefoxSeleniumLerpMoveMouseCommand);
firefoxSeleniumPilot.ExecuteCommands();

// output: 
// FirefoxSeleniumReceiver : MoveMouse; {X=50,Y=55}
// FirefoxSeleniumReceiver : ClickButton; {X=50,Y=55}
// FirefoxSeleniumReceiver : MoveMouse; {X=50,Y=65}
// FirefoxSeleniumReceiver : MoveMouse; {X=51,Y=66}
// FirefoxSeleniumReceiver : MoveMouse; {X=52,Y=67}
// FirefoxSeleniumReceiver : MoveMouse; {X=53,Y=68}
// FirefoxSeleniumReceiver : MoveMouse; {X=54,Y=69}
// FirefoxSeleniumReceiver : MoveMouse; {X=55,Y=69}
// FirefoxSeleniumReceiver : MoveMouse; {X=56,Y=69}
// FirefoxSeleniumReceiver : MoveMouse; {X=57,Y=69}
// FirefoxSeleniumReceiver : MoveMouse; {X=58,Y=69}
// FirefoxSeleniumReceiver : MoveMouse; {X=59,Y=69}

ChromiumPuppeteerExternalBrowser:

ChromiumPuppeteerExternalBrowser chromiumPuppeteerPilot = 
    new ChromiumPuppeteerExternalBrowser();
ChromiumPuppeteerReceiver chromiumPuppeteerReceiver = new ChromiumPuppeteerReceiver();
MoveMouseCommand chromiumPuppeteerMoveMouseCommand = 
    new MoveMouseCommand(chromiumPuppeteerReceiver, new Point(30, 40));
chromiumPuppeteerPilot.QueueCommand(chromiumPuppeteerMoveMouseCommand);
ClickButtonCommand chromiumPuppeteerClickButtonCommand = 
    new ClickButtonCommand(chromiumPuppeteerReceiver, new Point(30, 40));
chromiumPuppeteerPilot.QueueCommand(chromiumPuppeteerClickButtonCommand);
MoveMouseLikeHumanCommand chromiumPuppeteerLerpMoveMouseCommand = 
    new MoveMouseLikeHumanCommand(chromiumPuppeteerReceiver, 
    new Point(30, 40), new Point(39, 49));
chromiumPuppeteerPilot.QueueCommand(chromiumPuppeteerLerpMoveMouseCommand);
chromiumPuppeteerPilot.ExecuteCommands();

// output
// ChromiumPuppeteerReceiver : MoveMouse; {X=30,Y=40}
// ChromiumPuppeteerReceiver : ClickButton; {X=30,Y=40}
// ChromiumPuppeteerReceiver : MoveMouse; {X=30,Y=40}
// ChromiumPuppeteerReceiver : MoveMouse; {X=31,Y=41}
// ChromiumPuppeteerReceiver : MoveMouse; {X=32,Y=42}
// ChromiumPuppeteerReceiver : MoveMouse; {X=33,Y=43}
// ChromiumPuppeteerReceiver : MoveMouse; {X=34,Y=44}
// ChromiumPuppeteerReceiver : MoveMouse; {X=35,Y=45}
// ChromiumPuppeteerReceiver : MoveMouse; {X=36,Y=46}
// ChromiumPuppeteerReceiver : MoveMouse; {X=37,Y=47}
// ChromiumPuppeteerReceiver : MoveMouse; {X=38,Y=48}
// ChromiumPuppeteerReceiver : MoveMouse; {X=39,Y=49}
3
  • I was interested in having an ExternalBrowser class and inside it modules like public ExternalBrowserMouse Mouse; So i can access it by ExternalBrowser.Mouse.MoveByOffset(1, 2); Do you think the composite pattern would work since the client(ExternalBrowser) has access to Composite (ExternalBrowserMouse) which has access to the leaf (ExternalBrowserPuppeteerChromiumSpecificMouse)
    – Joao Vitor
    Jan 10, 2020 at 21:14
  • I don't think the composite pattern would be good here. With the composite you have the need to treat the composite similar to the leafs (refactoring.guru/design-patterns/composite). To me the leafs seem to be different than the composite and it gets more complicated when you what to then implement various external browser types. It seems you want to compose your objects such that you can easily access them through the dot notation. You may be able to use some form of the code I posted along with the facade pattern to get what you want. refactoring.guru/design-patterns/facade
    – quaabaam
    Jan 10, 2020 at 22:54
  • But there is a tree like structure in what im trying to achieve, meaning that ExternalBrowser has an ExternalBrowserMouse which has a leaf of ExternalBrowserChromiumPuppeteerSpecificMouse. Or even an ExternalBrowserHumanMouse -> ExternalBrowserMouse -> ExternalBrowserChromiumPuppeteerSpecificMouse... Then you have all the other modules inside the ExternalBrowser
    – Joao Vitor
    Jan 10, 2020 at 23:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.