diff --git a/Gopkg.lock b/Gopkg.lock index 53c079c..ce9c7dd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -80,6 +80,14 @@ revision = "4b2b341e8d7715fae06375aa633dbb6e91b3fb46" version = "v1.0.0" +[[projects]] + digest = "1:dafc9bfdb2d9816587ca63844a722b36525183e14423b1f356fd9a8a805b6230" + name = "github.com/blang/semver" + packages = ["."] + pruneopts = "NT" + revision = "ba2c2ddd89069b46a7011d4106f6868f17ee1705" + version = "v3.6.1" + [[projects]] digest = "1:13af3d205c774ec0bbda45fee1a4089f3cd9a55c5367b527698b34550ec15e71" name = "github.com/census-instrumentation/opencensus-proto" @@ -757,6 +765,20 @@ revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" version = "v1.3.0" +[[projects]] + branch = "master" + digest = "1:6cca2701b85cc807c552444eca3273868f7f1251f4c2ed0e8ac4312944a8d807" + name = "github.com/tebeka/selenium" + packages = [ + ".", + "chrome", + "firefox", + "internal/zip", + "log", + ] + pruneopts = "NT" + revision = "edf31bb7fd715ad505d9190f8d65d13f39a7c825" + [[projects]] digest = "1:00dde67969c6b5ac2d7bfe189a394095d7be3adff2bb915909235cb697e05d13" name = "github.com/toqueteos/trie" @@ -1527,6 +1549,7 @@ "github.com/redhat-developer/devconsole-git/pkg/controller/gitsourceanalysis", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", + "github.com/tebeka/selenium", "k8s.io/api/core/v1", "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/apis/meta/v1", diff --git a/Gopkg.toml b/Gopkg.toml index 48b1a0f..25fa69b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -10,6 +10,7 @@ required = [ "k8s.io/gengo/args", "sigs.k8s.io/controller-tools/pkg/crd/generator", "github.com/golang/protobuf/proto", + "github.com/tebeka/selenium", ] [[override]] @@ -107,3 +108,7 @@ required = [ "pkg/apis", "pkg/apis/devconsole/v1alpha1", ] + +[[constraint]] + name = "github.com/tebeka/selenium" + branch = "master" diff --git a/make/test.mk b/make/test.mk index 305561f..1bb26cb 100644 --- a/make/test.mk +++ b/make/test.mk @@ -14,7 +14,7 @@ export DEPLOYED_NAMESPACE:= .PHONY: test ## Runs Go package tests and stops when the first one fails test: ./vendor - $(Q)go test -vet off ${V_FLAG} $(shell go list ./... | grep -v -E '(/test/e2e|/test/operatorsource)') -failfast + $(Q)go test -vet off ${V_FLAG} $(shell go list ./... | grep -v -E '(/test/e2e|/test/operatorsource|/test/ui)') -failfast .PHONY: test-coverage ## Runs Go package tests and produces coverage information @@ -26,7 +26,7 @@ test-coverage-html: ./vendor ./out/cover.out $(Q)go tool cover -html=./out/cover.out ./out/cover.out: ./vendor - $(Q)go test ${V_FLAG} -race $(shell go list ./... | grep -v -E '(/test/e2e|/test/operatorsource)') -failfast -coverprofile=cover.out -covermode=atomic -outputdir=./out + $(Q)go test ${V_FLAG} -race $(shell go list ./... | grep -v -E '(/test/e2e|/test/operatorsource|/test/ui)') -failfast -coverprofile=cover.out -covermode=atomic -outputdir=./out .PHONY: get-test-namespace get-test-namespace: ./out/test-namespace @@ -49,7 +49,6 @@ ifeq ($(OPENSHIFT_VERSION),3) endif $(Q)operator-sdk test local ./test/e2e --namespace $(TEST_NAMESPACE) --up-local --go-test-flags "-v -timeout=15m" - .PHONY: e2e-setup e2e-setup: e2e-cleanup $(Q)oc new-project $(TEST_NAMESPACE) @@ -112,6 +111,31 @@ test-operator-source: push-operator-app-registry DEVCONSOLE_OPERATOR_VERSION=$(DEVCONSOLE_OPERATOR_VERSION) \ go test -vet off ${V_FLAG} $(shell go list ./... | grep $(OPSRC_DIR)) -failfast +.PHONY: test-ui-devperspective-admin +test-ui-devperspective-admin: + $(Q)oc login -u system:admin + $(Q)sh ./hack/install_devconsole/consoledeveloper.sh + $(eval export DEVCONSOLE_USERNAME := $(OC_LOGIN_USERNAME)) + $(eval export DEVCONSOLE_PASSWORD := $(OC_LOGIN_PASSWORD)) + $(eval export CHROMEDRIVER_BINARY := /usr/bin/chromedriver) + $(eval CONSOLE_TARGET_PORT := $(shell oc get routes console -n openshift-console -o jsonpath='{.spec.port.targetPort}')) + $(eval CONSOLE_HOST := $(shell oc get routes console -n openshift-console -o jsonpath='{.spec.host}')) + $(eval export OS_CONSOLE_URL := $(CONSOLE_TARGET_PORT)://$(CONSOLE_HOST)) + $(Q)go test -vet off ${V_FLAG} $(shell go list ./... | grep test/ui/devperspective) -failfast + +.PHONY: test-ui-devperspective-nonadmin +test-ui-devperspective-nonadmin: + $(Q)oc login -u system:admin + $(Q)sh ./hack/install_devconsole/consoledeveloper.sh + $(eval export DEVCONSOLE_USERNAME := consoledeveloper) + $(eval export DEVCONSOLE_PASSWORD := developer) + $(eval export CHROMEDRIVER_BINARY := /usr/bin/chromedriver) + $(eval CONSOLE_TARGET_PORT := $(shell oc get routes console -n openshift-console -o jsonpath='{.spec.port.targetPort}')) + $(eval CONSOLE_HOST := $(shell oc get routes console -n openshift-console -o jsonpath='{.spec.host}')) + $(eval export OS_CONSOLE_URL := $(CONSOLE_TARGET_PORT)://$(CONSOLE_HOST)) + $(eval export USER_IS_ADMIN := false) + $(Q)go test -vet off ${V_FLAG} $(shell go list ./... | grep test/ui/devperspective) -failfast + .PHONY: olm-integration-cleanup olm-integration-cleanup: get-test-namespace ifeq ($(OPENSHIFT_VERSION),3) diff --git a/test/support/support.go b/test/support/support.go new file mode 100644 index 0000000..ec99ddb --- /dev/null +++ b/test/support/support.go @@ -0,0 +1,20 @@ +package support + +import ( + "os" + "testing" +) + +// Getenv returns a value of environment variable, if it exists. +// Returns the default value otherwise. +func Getenv(t *testing.T, key string, defaultValue string) string { + value, found := os.LookupEnv(key) + var retVal string + if found { + retVal = value + } else { + retVal = defaultValue + } + t.Logf("Using env variable: %s=%s", key, retVal) + return retVal +} diff --git a/test/ui/devperspective/devperspective_test.go b/test/ui/devperspective/devperspective_test.go new file mode 100644 index 0000000..f2b6846 --- /dev/null +++ b/test/ui/devperspective/devperspective_test.go @@ -0,0 +1,102 @@ +package devperspective + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/redhat-developer/devconsole-operator/test/support" + "github.com/redhat-developer/devconsole-operator/test/ui" + "github.com/stretchr/testify/require" + + "github.com/tebeka/selenium" +) + +var tag string + +func TestDevPerspective(t *testing.T) { + tag = support.Getenv(t, "TAG", fmt.Sprintf("%d", time.Now().Unix())) + userIsAdmin := support.Getenv(t, "USER_IS_ADMIN", "true") + chBin := support.Getenv(t, "CHROMEDRIVER_BINARY", "/usr/bin/chromedriver") + chPort, err := strconv.Atoi(support.Getenv(t, "CHROMEDRIVER_PORT", "9515")) + require.NoError(t, err, "Chromedriver port") + + devconsoleUsername := support.Getenv(t, "DEVCONSOLE_USERNAME", "consoledeveloper") + devconsolePassword := support.Getenv(t, "DEVCONSOLE_PASSWORD", "developer") + openshiftConsoleURL := support.Getenv(t, "OS_CONSOLE_URL", "http://localhost") + + wd, svc := ui.InitSelenium( + t, + chBin, + chPort, + ) + + defer tearDown(t, wd, svc) + + defaultWait := 10 * time.Second + + t.Logf("Open URL: %s", openshiftConsoleURL) + err = wd.Get(openshiftConsoleURL) + require.NoErrorf(t, err, "Open URL: %s", openshiftConsoleURL) + consoleIsUp := false + for attempt := 0; attempt < 10; attempt++ { + err = wd.Refresh() + require.NoErrorf(t, err, "Refresh URL: %s", openshiftConsoleURL) + el, _ := wd.FindElement(selenium.ByXPATH, "//*[contains(text(),'Application is not available')]") + if el != nil { + t.Logf("Openshift Console is not available, try again after 2s.") + time.Sleep(2 * time.Second) + } else { + t.Logf("Openshift Console is up.") + consoleIsUp = true + break + } + } + if !consoleIsUp { + require.FailNow(t, "Openshift Console is not available.") + } + + require.NoError(t, err, fmt.Sprintf("Open console starting URL: %s", openshiftConsoleURL)) + ui.WaitForURLToContain(t, wd, "oauth", defaultWait) + + var elem selenium.WebElement + + if userIsAdmin == "true" { + elem = ui.FindElementBy(t, wd, selenium.ByLinkText, "kube:admin") + } else { + elem = ui.FindElementBy(t, wd, selenium.ByLinkText, devconsoleUsername) + } + + ui.WaitForElementToBeDisplayed(t, wd, elem, defaultWait) + ui.ClickToElement(t, elem) + + elem = ui.FindElementBy(t, wd, selenium.ByID, "inputUsername") + ui.WaitForElementToBeDisplayed(t, wd, elem, defaultWait) + ui.SendKeysToElement(t, elem, devconsoleUsername) + + elem = ui.FindElementBy(t, wd, selenium.ByID, "inputPassword") + ui.WaitForElementToBeDisplayed(t, wd, elem, defaultWait) + ui.SendKeysToElement(t, elem, devconsolePassword) + + elem = ui.FindElementBy(t, wd, selenium.ByXPATH, "//*/button[contains(text(),'Log In')]") + ui.ClickToElement(t, elem) + + elem = ui.FindElementBy(t, wd, selenium.ByID, "nav-toggle") + ui.WaitForElementToBeDisplayed(t, wd, elem, defaultWait) + ui.ClickToElement(t, elem) + + elem = ui.FindElementBy(t, wd, selenium.ByXPATH, "//*/a[@target][contains(text(),'Developer')]") + ui.WaitForElementToBeDisplayed(t, wd, elem, defaultWait) +} + +func tearDown(t *testing.T, wd selenium.WebDriver, svc *selenium.Service) { + err := wd.Quit() + if err != nil { + t.Log(err) + } + err = svc.Stop() + if err != nil { + t.Log(err) + } +} diff --git a/test/ui/selenium.go b/test/ui/selenium.go new file mode 100644 index 0000000..c63d859 --- /dev/null +++ b/test/ui/selenium.go @@ -0,0 +1,131 @@ +package ui + +import ( + "bytes" + "fmt" + "image" + "image/png" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tebeka/selenium" +) + +//FindElementBy look for a web element by a given selector and returs it back when found. +func FindElementBy(t *testing.T, wd selenium.WebDriver, by string, selector string) selenium.WebElement { + t.Logf("Find element by %s=%s", by, selector) + maxAttempts := 10 + attemptInterval := 100 * time.Millisecond + + // To avoid a problem where the element is yet not present + counter := 0 + for { + elems, err := wd.FindElements(by, selector) + if err != nil || len(elems) == 0 { + if counter <= maxAttempts { + t.Logf("Element for %s=%s not found, trying again...", by, selector) + time.Sleep(attemptInterval) + counter++ + } else { + require.NoError(t, fmt.Errorf("element for %s=%s not found", by, selector)) + } + } else { + return elems[0] + } + } +} + +//WaitForElementToBeDisplayed for a given web element to be displayed/visible for a given time duration. +func WaitForElementToBeDisplayed(t *testing.T, wd selenium.WebDriver, element selenium.WebElement, duration time.Duration) { + t.Logf("Wait for element to be displayed") + err := wd.WaitWithTimeout(func(wd selenium.WebDriver) (bool, error) { + return element.IsDisplayed() + }, duration) + require.NoError(t, err, "Wait for element to be displayed") +} + +//WaitForURLToContain waits for a current URL to contain the given text. It waits for a given time duration. +func WaitForURLToContain(t *testing.T, wd selenium.WebDriver, text string, duration time.Duration) { + t.Logf("Wait for URL to contain test '%s'...", text) + counter := 1 + err := wd.WaitWithTimeout(func(wd selenium.WebDriver) (bool, error) { + currentURL, err2 := wd.CurrentURL() + counter++ + return strings.Contains(currentURL, text), err2 + }, duration) + currentURL, err2 := wd.CurrentURL() + require.NoError(t, err2, fmt.Sprintf("Get current URL")) + require.NoError(t, err, fmt.Sprintf("Wait for URL to contain '%s'. The current URL is '%s'.", text, currentURL)) +} + +//SendKeysToElement sends keys to a given web element +func SendKeysToElement(t *testing.T, element selenium.WebElement, keys string) { + t.Log("Send keys to element") + err := element.SendKeys(keys) + require.NoError(t, err, "Send keys to element") +} + +//ClickToElement performs a click on a given web element. +func ClickToElement(t *testing.T, element selenium.WebElement) { + t.Log("Click to element") + err := element.Click() + require.NoError(t, err, "Click to element") +} + +//InitSelenium creates and initializes a new ChromeDriver service. +func InitSelenium(t *testing.T, chromedriverPath string, chromedriverPort int) (selenium.WebDriver, *selenium.Service) { + + service, err := selenium.NewChromeDriverService(chromedriverPath, chromedriverPort) + require.NoError(t, err) + + chromeOptions := map[string]interface{}{ + "args": []string{ + "--verbose", + "--no-cache", + "--no-sandbox", + "--headless", + "--window-size=1920,1080", + "--window-position=0,0", + "--enable-features=NetworkService", // to ignore invalid HTTPS certificates + //"--whitelisted-ips=''", // to support running in a container + }, + } + + caps := selenium.Capabilities{ + "browserName": "chrome", + "chromeOptions": chromeOptions, + } + + wd, err := selenium.NewRemote(caps, fmt.Sprintf("http://localhost:%d/wd/hub", chromedriverPort)) + require.NoError(t, err) + return wd, service +} + +//SaveScreenShotToPNG saves current screen to a given PNG file. +func SaveScreenShotToPNG(t *testing.T, wd selenium.WebDriver, filename string) { + t.Logf("Save screenshot to '%s'", filename) + err := os.MkdirAll(path.Dir(filename), 0775) + require.NoError(t, err, "Create screenshot directory") + // convert []byte to image for saving to file + imgByte, err := wd.Screenshot() + require.NoError(t, err, "Take screenshot") + img, _, _ := image.Decode(bytes.NewReader(imgByte)) + + //save the imgByte to file + out, err := os.Create(filename) + defer close(t, out) + + require.NoError(t, err, "Create a file for the screenshot") + + err = png.Encode(out, img) + require.NoError(t, err, "Write screanshot to PNG file") +} + +func close(t *testing.T, f *os.File) { + err := f.Close() + require.NoError(t, err, "Close output file") +}