cze
11
2012
.NET // WatiN

WatiN i okna dialogowe

Kontyunuując wątek testów automatycznych (rozpoczęty 2 posty temu) muszę stwierdzić, że największym problemem jaki napotkałem podczas implementacji testów była obsługa wyskakujących okienek dialogowych. Oczywiście wzorzec polegający na wrzucaniu na strony mnóstwa Pop-Upów jest już delikatnie mówiąc przestarzały ale wciąż może się zdarzyć. Przykładem takiego zdarzenia jest nasz system, którego logika oparta jest w uproszczeniu na stronie na której znajduję się gromada kontrolek (jedna pod drugą). Obsługa takiej kontrolki polega na kliknięciu przycisku, który wyświetla okienko dialogowe na którym osadzona jest kolejna strona z formularzem. Po jego wypełnieniu okienko jest zamykane a uzupełnione parametry są przekazywane do strony wywołującej. Wymagane jest więc jak widać odpowiednie obsłużenie wyskakującej strony poprzez "dobranie się" do iframe na którym jest osadzona. Poza tym innym często spotykanym rodzajem okienek są Confirm. Są to okienka potwierdzające wykonanie danej czynności ("Czy na pewno chcesz usunąć ten plik?") z możliwymi akcjami "tak/nie". Jest to trochę inny obiekt Javascriptowy niż "normalne" okno i wymaga całkiem innej obsługi. Z innych rodzajów okien można jeszcze wymienić np. Alert ale powiedzmy że jego użycie jest na tyle rzadkie że nie będziemy się nim zajmować.

Podsumowując, mamy problem z 2 typami okien: zwykłe okienka z osadzoną treścią oraz okienka typu Confirm. WatiN radzi sobie zarówno z jednymi jak i drugimi.

  • Okienka z osadzoną treścią

Aby obsłużyć tego typu okienka przede wszystkim musimy sobie podpiąć Handlera typu ReturnDialogHandler. Ja podpinam sobie go w klasie bazowej dla testów przy pobieraniu obiektu przeglądarki:

protected IE GetBrowser()
        {
            var browser = TestObjectBuilder.Browser;
            browser.AddDialogHandler(ReturnDialogHandler.CreateInstance());
            return browser;
        }

Następnym krokiem jest stworzenie klasy odpowiedzialnej za dany Pop-Up. Najpierw przygotowujemy klasę bazową dla tego typu okien:

public abstract class BasePopUp : IDisposable
    {
        protected HtmlDialog _dialog;

        public BasePopUp(HtmlDialog dialog)
        {
            _dialog = dialog;
        }

        public void WaitUntilClosed()
        {
            _dialog.WaitUntilClosed();
        }

        public void Dispose()
        {
            _dialog.Dispose();
        }
    }

Jak widać, jest w niej tylko jeden member odpowiadający za przechowywanie referencji do okna i zapewniający dispose tegoż kiedy nie jest już potrzebne. Przykładem konkretnej implementacji okna jest np.

public class PopCharges : BasePopUp
    {
        public PopCharges(HtmlDialog d)
            : base(d)
        {
        }

        public Button AcceptButton { get { return _dialog.Frames[0].Button(Find.ByValue("Akceptuj")); } }
        public Button CloseButton { get { return _dialog.Frames[0].Button(Find.ByValue("Zamknij")); } }
    }

Implementacja jest prosta, zapewnia dostęp do dwóch przycisków na oknie ("Akceptuj" i "Zamknij"). Warto zwrócić uwagę na to, że dostajemy się do tych obiektów poprzez naszego membera z klasy bazowej oraz ramkę która jeszcze jest po drodze. 

Samo użycie takiej klasy wygląda następująco:

public void CheckAndAccept(IE browser)
        {
            using (PopCharges pop = SetCharges(browser))
            {
                pop.AcceptButton.ClickNoWait();
                pop.WaitUntilClosed();
            }
        }

public PopCharges SetCharges(IE browser)
        {
            ChargesButton.ClickNoWait();
            HtmlDialog d = browser.HtmlDialogs[0];
            return new PopCharges(d);
        }

Najpierw musimy wyświetlić samo okienko dialogowe i pobrać jego referencje. Zajmuje się tym metoda SetCharges. Odnajduje ona przycisk ChargesButton i wykonuje akcję ClickNoWait. Pytanie dlaczego nie samo Click ? Otóż w przypadku kiedy spodziewamy się uzyskać okno dialogowe użycie Click spowodowało by blokadę wątku (nowe okno jest już w innym wątku i nie ma do niego dostępu z głównego). Stąd specjalna metoda ClickNoWait przygotowana właśnie pod tego typu sytuacje. Po kliknięciu potrzebujemy pobrać referencje okna czyli obiekt HtmlDialog. Ponieważ nie spodziewamy się większej ilości okien pobieramy pierwsze z brzegu (browser.HtmlDialogs[0]). Następnie przekazujemy otrzymany obiekt do konstruktora klasy PopCharges. Samo PopCharges jest w usingu aby zapewnic dispose otrzymanego HtmlDialoga. Sama logika użycia tego pop-upa jest prosta - klikamy w przycisk Accept (znów używamy metody ClickNoWait) i oczekujemy na zamknięcie okna: pop.WaitUntilClosed(). W tym przypadku logika odpowiadająca za zamknięcie okna jest oprogramowana w przycisku i nie musimy tego robić sami, natomiast jeśli musielibyśmy to zrobić to należałoby użyć metody pop.Close().

Używając tego wzorca zaprogramowałem obsługę większości potrzebnych okien. Oczywiście zawsze znajdzie się "przypadek szczególny" w którym trzeba dodatkowo pokombinować. Przypadek polegał na tym, że potrzebowałem dostać się do okna które było otwierane przez Pop-Up. Czyli sytuacja: strona->pop-up->kolejna strona. W takim przypadku standardowe podejście nie działało i trzeba to było zrobić tak:

using (PopPrintDocumentForCheque wnd = page205.PrintDocuments(browser))
            {
                wnd.W02110CheckBox.Click();
                wnd.Print.Click();
                IE popik = IE.AttachTo<IE>(Find.ByTitle((p) => p.StartsWith("Wydruk dokumentów")), 10);
                popik.Close();
                popik.WaitForComplete();
                wnd.WaitUntilClosed();
            }

Najpierw otwieramy pop-upa (w standardowy sposób). Na pop-upie zaznaczamy checkboxa a następnie klikamy na przycisk Click. Otwiera nam się kolejna okno (nazwałem je popik), do którego dostajemy się dość karkołomnie (podpinamy się do instacji IE zawierającej okno o tytule zaczynającym się od "Wydruk dokumentów"). A następnie brutalnie je zamykamy. Jestem świadomy, że sposób nie jest ładny ale nie udało mi się nic lepszego wymyślić.

  • Okienko Confirm
W celu obsługi tego typu okienka musimy użyć innego handlera: ConfirmDialogHandler. Sposób jego użycia znalazłem na Sieci i nie zdecydowałem się coś w nim zmieniać:
 
public void Confirm(IE browser)
        {
            var wnd = new ConfirmDialogHandler();
            using (new UseDialogOnce(browser.DialogWatcher, wnd))
            {
                ConfirmButton.ClickNoWait();
                wnd.WaitUntilExists();
                wnd.OKButton.Click();
            }
        }

Ta metoda oczekuje pojawienia się okienka typu Confirm a jak się pojawi to klika w przycisk OK i czeka na jego zamknięcie. Czyli najpierw musimy kliknąć jakiś przycisk wyświetlający confirm-a a następnie skorzystać z tej metody. No i to ogólnie działa, ale czasem mogą pojawić się problemy. Objawiają się one pozostaniem okna Confirma na ekranie mimo tego, że powinno się zamknąc. Problemy te powoduje samo IE, które czasem z niewiadomych powodów jakoś dziwnie blokuje okienko Confirm tak że WatiN nie może się do niego dostać i wtedy okienko takie "zostaje" na ekranie i test się wywala. W takim przypadku powtórzenie testu zazwyczaj pomaga ale należy pamiętać o tym, że niestety nie ma 100 % pewności powodzenia nawet jeśli test jest napisany dobrze. Nie udało mi się znaleźć nigdzie rozwiązania na tego typu "zawiechy" okienka.