Search code examples
delphichromium-embeddeddunitcef4delphi

CEF4Delphi and DUnit


I am testing a few processes I have created with CEF4Delphi in my application via DUnit.

The following is a MCVE to reproduce the issue:

unit MyUnit;

interface

{$I cef.inc}

uses
  Winapi.Windows,
  Winapi.Messages,
  System.SysUtils,
  System.Variants,
  System.Classes,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  uCEFWindowParent,
  uCEFChromiumWindow,
  uCEFChromium,
  Vcl.ExtCtrls,
  Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    ChromiumWindow1: TChromiumWindow;
    Timer1: TTimer;
    procedure Timer1Timer(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure ChromiumWindow1AfterCreated(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    FChromiumCreated: Boolean;
    procedure WMMove(var aMessage: TWMMove); message WM_MOVE;
    procedure WMMoving(var aMessage: TMessage); message WM_MOVING;
  public
    { Public declarations }
    function IsChromiumCreated: Boolean;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ChromiumWindow1AfterCreated(Sender: TObject);
begin
  ChromiumWindow1.LoadURL('https://www.google.com');
  FChromiumCreated := True;

end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FChromiumCreated := False;
end;

procedure TForm1.FormShow(Sender: TObject);
begin
  if not (ChromiumWindow1.CreateBrowser) then
    Timer1.Enabled := True;
end;

function TForm1.IsChromiumCreated: Boolean;
begin
  Result := FChromiumCreated;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  if not (ChromiumWindow1.CreateBrowser) and not (ChromiumWindow1.Initialized) then
    Timer1.Enabled := True
end;

procedure TForm1.WMMove(var aMessage: TWMMove);
begin
  inherited;
  if (ChromiumWindow1 <> nil) then
    ChromiumWindow1.NotifyMoveOrResizeStarted;
end;

procedure TForm1.WMMoving(var aMessage: TMessage);
begin
  inherited;
  if (ChromiumWindow1 <> nil) then
    ChromiumWindow1.NotifyMoveOrResizeStarted;
end;

end.

and the following is the Test case:

unit TestMyTest;
{

  Delphi DUnit Test Case
  ----------------------
  This unit contains a skeleton test case class generated by the Test Case Wizard.
  Modify the generated code to correctly setup and call the methods from the unit
  being tested.

}

interface

uses
  TestFramework,
  Vcl.Forms,
  MyUnit,
  System.Classes;

type
  // Test methods for class TForm1

  TestTForm1 = class(TTestCase)
  strict private
    FFormHolder: TForm;
    FForm1: TForm1;
  public
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestFormActivate;
  end;

implementation

procedure TestTForm1.SetUp;
begin
  Application.Initialize;
  FForm1 := TForm1.Create(nil);
  Application.Run;
end;

procedure TestTForm1.TearDown;
begin
  FForm1.Free;
  FForm1 := nil;
end;

procedure TestTForm1.TestFormActivate;
begin
  FForm1.Show;
  CheckTrue(FForm1.IsChromiumCreated);
end;

initialization
  // Register any test cases with the test runner
  RegisterTest(TestTForm1.Suite);

end.

If I use .Show, the instruction FChromiumCreated := True; is not executed, TChromium does not load the page and the test returns false. I am not sure but this may be because TChromium is initialised asynchronously and when the test is performed TChromium is not initialised completely yet.

How can perform my test in this case?

Edit I have read this answer . In my case .Show does allow to progress to the next line of the test but it seems TChromium hasn't initialised completely at that stage. I also tried the suggestion from tomazy but that does not work either.


Solution

  • There's no chance of your test passing in its current form. Chromium's loading is delayed, and will only be loaded at some point in the future. Yet your test immediately checks if it is loaded. Testing asynchronous code is possible, but it really makes a mess of your tests. I'd caution you to be careful about what you're testing. You might want to use another tool like Selenium for your page behaviour tests, and focus your Delphi tests on whether you're loading the right pages in required situations.


    A cursory look at CEF4 demo code reveals the reason creation may be delayed.

    GlobalCEFApp.GlobalContextInitialized has to be TRUE before creating any browser. If it's not initialized yet, we use a simple timer to create the browser later.

    WARNING: Global state can wreak havoc with unit testing. You'll need to investigate further to determine how best to ensure your tests are not negatively affected by this state.

    One approach that may work is to ensure GlobalCEFApp.GlobalContextInitialized is initialised before you start running any tests. But I suspect that will be a rather limited solution because although I'm not familiar with the TChromiumWindow component, I suspect many of it's interactions are asynchronous. You can trigger something, but then you'll have to wait for an event callback to determine the final outcome.

    This is where your test code will become messy. For example, suppose your form is intended to automatically load a particular page as soon as the Chromium window is fully initialised. Your test would have to do something as follows:

    procedure TestTForm1.TestBrowserLoad;
    begin
      FForm1.InitialPage := 'https://google.com';
      FForm1.Show;
      WaitForChromiumCreated(Form1.ChromiumWindow1); { <-- This is the tricky bit }
      CheckTrue(FForm1.IsChromiumCreated);
    end;
    

    Essentially WaitForChromiumCreated must allow the message loop of your main form to continue pumping messages. But also block processing in your test method. It also needs to reliably know when the component fully initialised. This is a situation where ProcessMessages() is justified, because you're not in a position to re-architect CEF4.

    Something along the following lines should do the trick.

    procedure WaitForChromiumCreated(AChromiumWindow: TChromiumWindow);
    begin
      while True do
      begin
        if (AChromiumWindow.Initialized) then Break;
        { You'll also need a way to break out of this loop
          if something goes wrong and the component cannot 
          initialise, or if the tests are aborted. }
        Application.ProcessMessages();
      end;
    end;
    

    TIP: I also strongly recommend adding a timeout parameter to all Wait... methods and make your tests fail immediately if a wait times out unexpectedly.