Up to: Mika Raento's Symbian Programming pages

Symbian Programming - automated UI tests

Automated UI testing is annoying in general and Symbian doesn't really provide anything to make it easier. There are many aspects to testing UIs, but one is testing the actual drawing and layout. A necessary conditions for such tests is the ability to exercise drawing code and capture the drawn output in a test. The code below shows how to capture the output from a CCoeControl-derived control in a googletest test.

    // Copyright 2009 Karhea Oy.
    // Author: mikie@karhea.com (Mika Raento)
    // This code put in the public domain. Use it as you wish.

    // Example UI drawing test for Symbian: how to capture the look of a CCoeControl
    // into a file. This works on the 3.0 MR emulator, leaks on the PDK and
    // has not been tested on S60 emulators > 3.0 or on-device.

    #include <aknapp.h>
    #include <aknappui.h>
    #include <akndoc.h>
    #include <basched.h>
    #include <coecntrl.h>
    #include <eikenv.h>
    #include <fbs.h>
    #include <imageconversion.h>

    // Some internal utils - not included in the example.
    #include "base/public/scoped_ptr.h"
    #include "purity/symbian/symbian_test.h"
    #include "testing/public/ktest.h"
    #include "third_party/googletest/include/gtest/gtest.h"

    namespace ksymbian {

    class StoppingActive : CActive {
     public:
      StoppingActive() : CActive(CActive::EPriorityStandard) {
        CActiveScheduler::Add(this);
      }
      TRequestStatus* mutable_status() {
        return &iStatus;
      }
      void Activate() {
        SetActive();
      }
      virtual ~StoppingActive() {
        Cancel();
      }
     private:
      virtual void RunL() {
        CActiveScheduler::Stop();
      }
      virtual void DoCancel() {
      }
    };

    void SaveBitmap(RFs* fs, CFbsBitmap* bitmap, const TDesC& filename) {
      // CImageEncoder uses active objects and needs an active scheduler present.
      scoped_ptr<CBaActiveScheduler> sched;
      // We may already have an active scheduler (if running with CEikonEnv).
      if (!CActiveScheduler::Current()) {
        sched.reset(new (ELeave) CBaActiveScheduler);
        CActiveScheduler::Install(sched.get());
      }
      fs->Delete(filename);
      scoped_ptr<CImageEncoder> encoder(
          CImageEncoder::FileNewL(*fs, filename, _L8("image/png")));
      scoped_ptr<StoppingActive> s(new (ELeave) StoppingActive);
      encoder->Convert(s->mutable_status(), *bitmap);
      s->Activate();
      CActiveScheduler::Start();
      s.reset(NULL);
      if (sched.get()) {
        CActiveScheduler::Install(NULL);
      }
    }

    }  // namespace ksymbian

    namespace {

    class EikonTest : public KTest {};

    class CDummyAppUi : public CAknAppUi {
     public:
      void ConstructL() {
        // If you don't have/want an application resource file you can pass in
        // ENoAppResourceFile here.
        BaseConstructL();
      }
    };

    class CDummyDocument : public CAknDocument {
     public:
      CAknAppUi* CreateAppUiL() {
        return new (ELeave) CDummyAppUi;
      }
      CDummyDocument(CEikApplication& aApp) : CAknDocument( aApp ) {}
    };

    class CDummyApplication : public CAknApplication {
     public:
      virtual CApaDocument* CreateDocumentL() {
        return new (ELeave) CDummyDocument(*this);
      }
      virtual TUid AppDllUid() const {
        return TUid::Uid(0x20028529);
      }
      // Need to override ResourceFileName and BitmapStoreName as the inherited
      // implementations will try to access objects that are only created as
      // part of the framework-provided 'normal' application startup code.
      // We can't create all of those objects as they are in the not public
      // headers.
      TFileName ResourceFileName() const {
        TFileName n = _L("e:\\resource\\apps\\model_test.rsc");
        return n;
      }
      TFileName BitmapStoreName() const {
        return TFileName();
      }
    };

    // Green: a simple CCoeControl that just draws a full-screen green
    // rectangle and allows access to the backing bitmap.
    class Green : public CCoeControl {
     public:
      void ConstructL() {
        // We want a backed up window so that we can easily get access to the
        // bitmap. Regular windows need grabbing a screenshot which gets much
        // more complicated as it needs to be synchronized with redraws etc.
        CreateBackedUpWindowL(iEikonEnv->RootWin());
        SetExtentToWholeScreen();
        ActivateL();
      }
      TInt Bitmap() {
        BackedUpWindow().UpdateBackupBitmap();
        return BackedUpWindow().BitmapHandle();
      }

     private:
      virtual void Draw(const TRect& /*aRect*/) const {
        CWindowGc& gc = SystemGc();
        TRect drawRect(Rect());
        gc.SetBrushColor(TRgb(0, 150, 50));
        gc.SetBrushStyle(CGraphicsContext::ESolidBrush);
        gc.DrawRect(drawRect);
      }
    };

    // Create a Green and save to a file.
    void SaveGreen() {
      scoped_ptr<Green> g(new (ELeave) Green);
      g->ConstructL();
      g->DrawNow();

      scoped_ptr<CFbsBitmap> bitmap(new (ELeave) CFbsBitmap);
      bitmap->Duplicate(g->Bitmap());
      ksymbian::SaveBitmap(&CEikonEnv::Static()->FsSession(),
                           bitmap.get(), _L("e:\\green.png"));
    }

    TEST_F(EikonTest, Create) {
      // Wait for emulator to start up enough for eikon construction to succeed.
      // Waiting for menu.exe (SecureId().iUid == 0x101f4cd2) to appear works
      // on 3.0 MR. If we don't wait the construction of CEikonEnv will leave
      // with KErrNotFound (-1).
      ksymbian::WaitForMenuExe();

      CEikonEnv* e = new CEikonEnv;
      // Creating a CEikonEnv creates a cleanup stack. So even if we are
      // running in a TRAP harness here, we start afresh and need to TRAP() any
      // calls that will use the cleanup stack as there's no TRAP item on the
      // stack otherwise and you geta E32User-CBase 66 panic.
      TInt err = KErrNone;
      TRAP(err, e->ConstructL(EFalse));
      EXPECT_EQ(KErrNone, err);

      // Use of the application resource file needs an application, as it not only
      // loads the file but also provides access to its handle.
      CDummyApplication* app = new CDummyApplication;
      TRAP(err, app->PreDocConstructL());
      EXPECT_EQ(KErrNone, err);

      CDummyDocument* doc = new (ELeave) CDummyDocument(*app);

      // CEikonEnv will unconditionally access an appui on some Symbian versions.
      // You should always have an AppUi if you create an Eikon env
      // to avoid crashes (KERN-EXEC 3).
      CDummyAppUi* app_ui = new CDummyAppUi;
      app_ui->SetDocument(doc);
      TRAP(err, app_ui->ConstructL());
      EXPECT_EQ(KErrNone, err);

      TRAP(err, SaveGreen());
      EXPECT_EQ(KErrNone, err);

      // The AppUi is deleted by CEikonEnv but the doc and app are not.
      delete doc;
      delete app;
      e->DestroyEnvironment();
    }

    }  // namespace

Mika Raento, mikie(at)iki.fi