Disclosure Statement: This site contains affiliate links, which means that I may receive a commission if you make a purchase using these links. As an eBay Partner, I earn from qualifying purchases.

What's the best approach for browser automation

Post Reply
shobits1
Posts: 25
Joined: Wed Mar 04, 2020 9:16 pm

What's the best approach for browser automation

Post by shobits1 »

first, thanks for your hard work.

I'm new to CEF in general so I'm lost... and many information I found using google where using the old CEF not the current one and many functions got deprecated, anyway my question will be straight one. I'm trying to automate filling information and checking it in on our site this will require executing and waiting for each function to finish before starting other like this:

goto login page
fill username and password value
click login button
wait until page loads / or return error message if login failed
navigate to some pages using navigation menu (click item)
wait until page load

*process data
fill fields on form (fields count changes)
click save button
waiting for ajax queries to finish
process next data

logout

so what's the best way to do this,, I tried using RTTIExtention combining "ExecuteJavaScript" and Callbacks from the injected JS function on RttiExtention but it seems too complicated, especially how to make each waits until last one finishes (like processing data steps) ... also how to check if clicking login button actually worked (it will load new page which will reset the JS object and I can't callback RttiExtention since I don't know what function has loaded new page)
shobits1
Posts: 25
Joined: Wed Mar 04, 2020 9:16 pm

Re: What's the best approach for browser automation

Post by shobits1 »

Btw, this what I'm doing right now:

Code: Select all

// do login
sCode := 'PAS.doLogin("' + aUsername + '","' + aPassword + '");';
JSExecuter.Execute( ... sCode, 30000, True);

procedure TJSExecuter.Execute(const aChrome : TChromium; const sCode : ustring; const msTimeout: Int64;
  const WaitUntilDone: Boolean);
const
  scriptUrl = '';
  startLine = 0;
var
  LTimeoutTime : Int64;
  frame: ICefFrame;

begin
  frame := aChrome.Browser.MainFrame;

  if (frame = nil) or not(frame.IsValid)  then
    Exit;

  frame.ExecuteJavaScript(sCode, scriptUrl, startLine);

  LTimeoutTime := MilliSecondsBetween(Now, 0) + msTimeout;

  FisRunning := True;

  if WaitUntilDone then
    while FisRunning = True do  // This loop will force wait until js call finish executing
    begin
      if aChrome.IsLoading then
        LTimeoutTime := MilliSecondsBetween(Now, 0) + msTimeout;

      if (MilliSecondsBetween(Now, 0) > LTimeoutTime) then
      begin
        TimeOut;
        break;
      end;

      Sleep(10);                               // sleep so to not use cpu all time
      Application.ProcessMessages;  // don't hung the UI
    end;
end;

procedure TJSExecuter.CallbackFromJS(id: Integer; const data: Variant);
begin
  FisRunning := False;
end;

procedure TfrmMain.ChromeProcessMessageReceived(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame;
  sourceProcess: TCefProcessId; const message: ICefProcessMessage; out Result: Boolean);
begin
  .....
 JSExecuter.CallbackFromJS(id, data);
end;

// RttiExtention
class procedure TPAS_Callback.doLogin(const data: Variant);
begin
  SendMessageToBrowser(PID_BROWSER, Message);
end;

Code: Select all

var PAS;
if (!PAS)
  PAS = {};
(function() {
  PAS.DocLoaded = function(){    
    window.addEventListener('load', function(){
      switch( PAS.GetCookie() ){
        case '':
          console.log('Page loaded with no last click');
          break;
          
        case 'doLogin':
          PAS_Callback.doLogin(true);
          break;         
     }
     PAS.DelCookie();
     });
  };
  

 PAS.doLogin = function(sUsername, sPassword) {
    console.log('PAS.doLogin -----------------');
    var result  = false;
    try{
      $('#loginFrm\\:j_username').val(sUsername);
      $('#loginFrm\\:j_password').val(sPassword);
      
      result  = true;
      PAS.SetCookie('doLogin');
      $('#loginFrm\\:loginBtn').click();      
    }catch (e){	
      result  = false;
      console.log(' >>> PAS.doLogin got an exception');
    }
    
    if (result == false)
      PAS_Callback.doLogin(false);
  };
  
 
})();

PAS.DocLoaded();
User avatar
salvadordf
Posts: 4016
Joined: Thu Feb 02, 2017 12:24 pm
Location: Spain
Contact:

Re: What's the best approach for browser automation

Post by salvadordf »

shobits1 wrote: Wed Mar 04, 2020 9:51 pm so what's the best way to do this,, I tried using RTTIExtention combining "ExecuteJavaScript" and Callbacks from the injected JS function on RttiExtention but it seems too complicated, especially how to make each waits until last one finishes (like processing data steps) ... also how to check if clicking login button actually worked (it will load new page which will reset the JS object and I can't callback RttiExtention since I don't know what function has loaded new page)
Please, read the code comments in the JSExtension demo.

The process you describe has many steps and the resulting code will always be complicated.

I would try to create a clear state diagram with all possible steps and states. Then I would add some synchronization objects to store the "state" for each step. After that you can use JavaScript code with custom JavaScript extensions to proceed with all the steps.
Use process messages to send information between the render process and the browser process.
shobits1
Posts: 25
Joined: Wed Mar 04, 2020 9:16 pm

Re: What's the best approach for browser automation

Post by shobits1 »

Please, read the code comments in the JSExtension demo.

Code: Select all

// If you put all you know so far together you can get any result or information in Delphi from
// JavaScript following these steps :
// 1. Use TChromium.ExecuteJavaScript to execute your custom JavaScript code.
// 2. That custom JavaScript code is executed in the RENDERER PROCESS and it can call functions in your
//    custom JavaScript extension, which executes Delphi code. This Delphi code is also executed in
//    the RENDERER PROCESS.
// 3. The Delphi code in the JavaScript extension can use ICefFrame.SendProcessMessage to send
//    information back to the BROWSER PROCESS.
// 4. The BROWSER PROCESS receives the information in the TChromium.OnProcessMessageReceived event.
it's like the way I'm doing it right now, only using RttiExtention to simplify things (or maybe I'm complicating it !!!) - did you see the pseudo code in my previous post-

so,, I gather, there is no way around using Extension + JS + Sending messages between processes (in my use case).


I'm now interested on (since it looks like will make my code cleaner and easier to maintain) :
Then I would add some synchronization objects to store the "state" for each step
unfortunately, I don't know how to do it... if you care please, post a small demo (example) or if you have some reading on other sites(posts/blogs/tutorials...etc) that can help me, I'll be grateful.

thank you again, for helping me.
User avatar
salvadordf
Posts: 4016
Joined: Thu Feb 02, 2017 12:24 pm
Location: Spain
Contact:

Re: What's the best approach for browser automation

Post by salvadordf »

shobits1 wrote: Thu Mar 05, 2020 8:51 pm
Then I would add some synchronization objects to store the "state" for each step
unfortunately, I don't know how to do it... if you care please, post a small demo (example) or if you have some reading on other sites(posts/blogs/tutorials...etc) that can help me, I'll be grateful.
thank you again, for helping me.
It depends on what you need to do with your application. If you need to use the render processes to read some small information from the DOM then you can send it as a parameter in the process messages.

However, if you need to read large amounts of information you may need to store it in a database protected by a named mutex :
http://docwiki.embarcadero.com/Librarie ... bjs.TMutex
http://docwiki.embarcadero.com/CodeExam ... e_(Delphi)
https://stackoverflow.com/questions/247 ... ng-mutexes

That database can be used from all processes as long as they acquire and release the mutex when they use it.

Most TChromium events are executed in a CEF thread and you should use a TCriticalSection to protect the information that you will use in the application main thread :
http://docwiki.embarcadero.com/Librarie ... calSection
http://docwiki.embarcadero.com/CodeExam ... t_(Delphi)

Some CEF4Delphi demos and source code uses TCriticalSection and TMutex too :
https://github.com/salvadordf/CEF4Delph ... er.pas#L97
https://github.com/salvadordf/CEF4Delph ... el.pas#L73

Search "Delphi TMutex" and "Delphi TCriticalSection" in google for more information.
Post Reply