Skip to content
5 changes: 3 additions & 2 deletions deploy/devsandbox-dashboard/ui-e2e-tests/.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
SSO_USERNAME=${SSO_USERNAME}
SSO_PASSWORD=${SSO_PASSWORD}
BASE_URL=${BASE_URL}
ENVIRONMENT=ui-e2e-tests
BROWSER=${BROWSER}
ENVIRONMENT=${ENVIRONMENT}
BROWSER=${BROWSER}
KUBECONFIG=${KUBECONFIG}
33 changes: 31 additions & 2 deletions make/devsandbox-dashboard.mk
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ e2e-run-devsandbox-dashboard:
@echo "Running Developer Sandbox Dashboard setup e2e tests..."
DEVSANDBOX_DASHBOARD_NS=${DEVSANDBOX_DASHBOARD_NS} go test "./test/e2e/devsandbox-dashboard/setup" -v -timeout=10m -failfast

@echo "Running Developer Sandbox Dashboard e2e tests in firefox..."
@SSO_USERNAME=$(SSO_USERNAME_READ) SSO_PASSWORD=$(SSO_PASSWORD_READ) BASE_URL=${RHDH} BROWSER=firefox envsubst < deploy/devsandbox-dashboard/ui-e2e-tests/.env > testsupport/devsandbox-dashboard/.env
@echo "Running Developer Sandbox Dashboard e2e tests in Firefox..."
@SSO_USERNAME=$(SSO_USERNAME_READ) SSO_PASSWORD=$(SSO_PASSWORD_READ) BASE_URL=${RHDH} BROWSER=firefox ENVIRONMENT=${UI_ENVIRONMENT} envsubst < deploy/devsandbox-dashboard/ui-e2e-tests/.env > testsupport/devsandbox-dashboard/.env
@HOST_NS=$(HOST_NS) MEMBER_NS=$(MEMBER_NS) MEMBER_NS_2=$(MEMBER_NS_2) REGISTRATION_SERVICE_NS=$(HOST_NS) SECOND_MEMBER_MODE=$(SECOND_MEMBER) go test "./test/e2e/devsandbox-dashboard" -v -timeout=10m -failfast

@echo "The Developer Sandbox Dashboard e2e tests successfully finished"
Expand Down Expand Up @@ -99,3 +99,32 @@ endif
-e PUBLISH_UI=false \
-e RUNNING_IN_CONTAINER=true \
$(E2E_TEST_IMAGE_NAME) make test-devsandbox-dashboard-e2e

# Run Developer Sandbox Dashboard e2e tests against prod
.PHONY: test-devsandbox-dashboard-e2e-prod
test-devsandbox-dashboard-e2e-prod:
@echo "Installing Firefox browser for Playwright..."
go tool playwright install firefox

@echo "Running Developer Sandbox Dashboard e2e tests in Firefox..."
@SSO_USERNAME=${SSO_USERNAME_READ} SSO_PASSWORD=${SSO_PASSWORD_READ} BASE_URL=https://sandbox.redhat.com/ BROWSER=firefox ENVIRONMENT=prod KUBECONFIG=${KUBECONFIG} envsubst < deploy/devsandbox-dashboard/ui-e2e-tests/.env > testsupport/devsandbox-dashboard/.env
@go test "./test/e2e/devsandbox-dashboard" -v -timeout=10m -failfast

@echo "The Developer Sandbox Dashboard e2e tests successfully finished"


# Run Developer Sandbox Dashboard e2e tests against prod in container using podman
.PHONY: test-devsandbox-dashboard-in-container-prod
test-devsandbox-dashboard-in-container-prod: build-devsandbox-dashboard-e2e-tests
@rm -f build/_output/bin/ksctl
@echo "running the prod e2e tests in podman container..."
podman run --platform $(IMAGE_PLATFORM) --rm \
-v $(KUBECONFIG):/root/.kube/config \
-e KUBECONFIG=/root/.kube/config \
-v ${PWD}:/root/toolchain-e2e \
-e E2E_REPO_PATH=/root/toolchain-e2e \
-e SSO_USERNAME=$(SSO_USERNAME) \
-e SSO_PASSWORD=$(SSO_PASSWORD) \
-e RUNNING_IN_CONTAINER=true \
-e PATH=/app/build/_output/bin:$$PATH \
$(E2E_TEST_IMAGE_NAME) make test-devsandbox-dashboard-e2e-prod
9 changes: 9 additions & 0 deletions test/e2e/devsandbox-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ If you want to use your local devsandbox-dashboard, please:
1. Set the `QUAY_NAMESPACE` environment variable to your quay username: `export QUAY_NAMESPACE=<your-quay-username>`
2. Run `make test-devsandbox-dashboard-in-container SSO_USERNAME=<SSO_USERNAME> SSO_PASSWORD=<SSO_PASSWORD> UI_REPO_PATH=${PWD}/../devsandbox-dashboard`

## Deploy Developer Sandbox Dashboard in Prod

`make test-devsandbox-dashboard-e2e-prod SSO_USERNAME=${SSO_USERNAME} SSO_PASSWORD=${SSO_PASSWORD} KUBECONFIG=${KUBECONFIG}`

`make test-devsandbox-dashboard-in-container-prod SSO_USERNAME=${SSO_USERNAME} SSO_PASSWORD=${SSO_PASSWORD} KUBECONFIG=${KUBECONFIG}`

The Developer Sandbox Dashboard E2E tests will run against the production environment at `https://sandbox.redhat.com/`.


## Deploy Developer Sandbox Dashboard in E2E mode

Please note that OCP cluster does not have a valid CA, so when accessing the Developer Sandbox Dashboard, you need to:
Expand Down
108 changes: 80 additions & 28 deletions test/e2e/devsandbox-dashboard/fresh_signup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import (
"context"
"errors"
"regexp"
"strings"
"testing"

toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/toolchain-e2e/testsupport"
"github.com/codeready-toolchain/toolchain-e2e/testsupport/cleanup"
sandboxui "github.com/codeready-toolchain/toolchain-e2e/testsupport/devsandbox-dashboard"
"github.com/playwright-community/playwright-go"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// TestFreshSignup tests the complete fresh signup flow:
Expand All @@ -27,7 +31,7 @@ func TestFreshSignup(t *testing.T) {
// Ensure the user signup is not present in the system
env := viper.GetString("ENVIRONMENT")
username := viper.GetString("SSO_USERNAME")
ensureNoUserSignup(t, env, username)
ensureNoUserSignup(t, page, env, username)

// Step 2: Verify homepage layout on first access
verifyHomepage(t, page)
Expand All @@ -39,8 +43,9 @@ func TestFreshSignup(t *testing.T) {
verifyDevSandboxAccess(t, page, env, testName)
}

func ensureNoUserSignup(t *testing.T, env, username string) {
if env == sandboxui.TestEnv {
func ensureNoUserSignup(t *testing.T, page playwright.Page, env, username string) {
switch env {
case sandboxui.TestEnv:
awaitilities := testsupport.WaitForDeployments(t)
hostAwait := awaitilities.Host()
userSignup, err := sandboxui.WaitForUserSignup(t, hostAwait, username)
Expand All @@ -51,6 +56,18 @@ func ensureNoUserSignup(t *testing.T, env, username string) {
// delete user signup
err := sandboxui.DeleteUserSignup(t, hostAwait, userSignup)
require.NoError(t, err)
reloadPage(t, page)
}
case sandboxui.ProdEnv:
client, err := newClient(t, viper.GetString("KUBECONFIG"))
require.NoError(t, err)

userSignup := sandboxui.GetUserSignupWithClient(t, client, username)

if userSignup != nil {
// delete user signup
sandboxui.DeleteUserSignupWithClient(t, client, userSignup)
reloadPage(t, page)
}
}
}
Expand Down Expand Up @@ -126,17 +143,15 @@ func performSignup(t *testing.T, page playwright.Page, env, username string) {
err = tryItButton.Click()
require.NoError(t, err)

if env == sandboxui.TestEnv {
// add signup to cleanup
awaitilities := testsupport.WaitForDeployments(t)
hostAwait := awaitilities.Host()
userSignup, err := sandboxui.WaitForUserSignup(t, hostAwait, username)
require.NoError(t, err)
cleanup.AddCleanTasks(t, hostAwait.Client, userSignup)
}
t.Cleanup(func() {
ensureNoUserSignup(t, nil, env, username)
})

// wait for loading icon to disappear
err = loadingIcon.WaitFor(playwright.LocatorWaitForOptions{State: playwright.WaitForSelectorStateHidden})
err = loadingIcon.WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(60000), // 1 minute timeout
})
require.NoError(t, err)

// wait for network to be idle (ensures all updates are complete)
Expand Down Expand Up @@ -188,23 +203,60 @@ func verifyDevSandboxAccess(t *testing.T, page playwright.Page, env, testName st
devSandboxPage, err := sandboxui.ClickAndWaitForPopup(t, page, tryItBtn, testName)
require.NoError(t, err)

img := devSandboxPage.GetByRole("img", playwright.PageGetByRoleOptions{
Name: imgName,
})
err = img.WaitFor(playwright.LocatorWaitForOptions{
Timeout: playwright.Float(30000),
})
require.NoError(t, err)
if env == sandboxui.ProdEnv {
// Wait for auth redirect to complete
if strings.Contains(devSandboxPage.URL(), "/oauth/authorize") {
err := devSandboxPage.WaitForURL("**/k8s/cluster/projects/**", playwright.PageWaitForURLOptions{
Timeout: playwright.Float(180000), // 3 minutes
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
require.NoError(t, err)
}

h := devSandboxPage.GetByRole("heading", playwright.PageGetByRoleOptions{})
hText, err := h.TextContent()
require.NoError(t, err)
require.Contains(t, hText, logMessage)
// Find welcome text and wait for it to be visible
welcomeText := devSandboxPage.GetByText("Welcome to the new OpenShift experience!")
err = welcomeText.WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(180000), // 3 minutes
})
require.NoError(t, err)
} else {
img := devSandboxPage.GetByRole("img", playwright.PageGetByRoleOptions{
Name: imgName,
})
err = img.WaitFor(playwright.LocatorWaitForOptions{
Timeout: playwright.Float(30000),
})
require.NoError(t, err)

list := devSandboxPage.GetByRole("list", playwright.PageGetByRoleOptions{})
listText, err := list.TextContent()
require.NoError(t, err)
require.Contains(t, listText, "DevSandbox")
h := devSandboxPage.GetByRole("heading", playwright.PageGetByRoleOptions{})
hText, err := h.TextContent()
require.NoError(t, err)
require.Contains(t, hText, logMessage)

list := devSandboxPage.GetByRole("list", playwright.PageGetByRoleOptions{})
listText, err := list.TextContent()
require.NoError(t, err)
require.Contains(t, listText, "DevSandbox")
}

require.NoError(t, devSandboxPage.Close())
}

func newClient(t *testing.T, kubeconfigPath string) (client.Client, error) {
require.NotEmpty(t, kubeconfigPath)

cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
require.NoError(t, err)

s := runtime.NewScheme()
require.NoError(t, toolchainv1alpha1.AddToScheme(s))

return client.New(cfg, client.Options{Scheme: s})
}

func reloadPage(t *testing.T, page playwright.Page) {
if page != nil {
_, err := page.Reload()
require.NoError(t, err)
}
}
14 changes: 6 additions & 8 deletions testsupport/devsandbox-dashboard/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func NewLoginPage(page playwright.Page, environment string) *LoginPage {
}

switch environment {
case DevEnv:
case DevEnv, ProdEnv:
lp.LoginUsernameLoc = page.GetByRole("textbox", playwright.PageGetByRoleOptions{
Name: "Red Hat login",
})
Expand Down Expand Up @@ -59,19 +59,17 @@ func NewLoginPage(page playwright.Page, environment string) *LoginPage {
}

func (lp *LoginPage) Navigate(t *testing.T, url string) {
_, err := lp.Page.Goto(url)
_, err := lp.Page.Goto(url, playwright.PageGotoOptions{
Timeout: playwright.Float(60000), // 1 minute timeout
})
require.NoError(t, err)
}

func (lp *LoginPage) Login(t *testing.T, loginUsername, loginPw string) {
// Mask username field
_, err := lp.LoginUsernameLoc.Evaluate(`element => element.style.webkitTextSecurity = 'disc'`, nil)
err := lp.LoginUsernameLoc.Fill(loginUsername)
require.NoError(t, err)

err = lp.LoginUsernameLoc.Fill(loginUsername)
require.NoError(t, err)

if lp.Env == DevEnv {
if lp.Env == DevEnv || lp.Env == ProdEnv {
err := lp.NextBtn.Click()
require.NoError(t, err)
}
Expand Down
13 changes: 9 additions & 4 deletions testsupport/devsandbox-dashboard/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sandboxui

import (
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
Expand All @@ -15,6 +16,7 @@ import (
const (
TestEnv = "ui-e2e-tests"
DevEnv = "dev"
ProdEnv = "prod"
)

var (
Expand Down Expand Up @@ -62,8 +64,11 @@ func Setup(t *testing.T, testName string) playwright.Page {
context, err := browser.NewContext(opts)
require.NoError(t, err)

// save trace
trace(t, context, testName)
// save trace only if not running in CI
// we do not want to expose sensitive information in CI
if os.Getenv("ARTIFACT_DIR") == "" { // not CI environment
trace(t, context, testName)
}
Comment on lines +67 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't disable tracing for every CI run.

ARTIFACT_DIR is already how getTraceDirectory() routes trace artifacts into CI storage. Gating trace() on that variable removes Playwright traces from all CI jobs, including the existing ui-e2e-tests path, so failures lose the most useful debugging artifact. If the concern is prod secrecy, gate on env == ProdEnv or a dedicated opt-out flag instead of CI detection.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 69-69: Remove this unnecessary variable declaration and use the expression directly in the condition.

See more on https://sonarcloud.io/project/issues?id=codeready-toolchain_toolchain-e2e&issues=AZ0mCgEWDAKfaaDBdd63&open=AZ0mCgEWDAKfaaDBdd63&pullRequest=1271

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@testsupport/devsandbox-dashboard/setup.go` around lines 67 - 71, The current
gate using ARTIFACT_DIR prevents trace(t, context, testName) from running in CI
and removes Playwright traces; instead remove the ARTIFACT_DIR check and always
call trace(...) (so traces continue to be routed by getTraceDirectory()), or if
you need to disable tracing only for production, gate on env == ProdEnv or a
dedicated opt-out flag (e.g. DISABLE_TRACING) rather than ARTIFACT_DIR; update
testsupport/devsandbox-dashboard/setup.go to call trace(t, context, testName)
unconditionally or replace the ARTIFACT_DIR condition with the appropriate
ProdEnv/opt-out check.


page, err := context.NewPage()
require.NoError(t, err)
Expand All @@ -73,9 +78,9 @@ func Setup(t *testing.T, testName string) playwright.Page {
login := NewLoginPage(page, env)
login.Navigate(t, baseURL)

if env == DevEnv {
if env == ProdEnv {
// handle cookie consent
// on dev environment, the cookie consent appears after the login page is loaded
// on prod environment, the cookie consent appears after the login page is loaded
handleCookiesConsent(t, page)
}

Expand Down
25 changes: 14 additions & 11 deletions testsupport/devsandbox-dashboard/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,8 @@ func handleRecordedVideo(t *testing.T, page playwright.Page, targetVideoPath str

// Handle failed test - rename video
if t.Failed() {
// For popup videos, rename with UUID to avoid conflicts when multiple popups exist in the same test
if strings.Contains(targetVideoPath, "popup") {
uuid := filepath.Base(videoPath)
uuid = strings.TrimSuffix(uuid, ".webm")
if len(uuid) > 8 {
uuid = uuid[:8] // Truncate to first 8 chars
}
targetVideoPath = strings.Replace(targetVideoPath, "popup", fmt.Sprintf("popup-%s", uuid), 1)
}

if err := os.Rename(videoPath, targetVideoPath); err != nil {
finalPath := buildPopupVideoPath(targetVideoPath, videoPath)
if err := os.Rename(videoPath, finalPath); err != nil {
t.Logf("failed to rename video %s: %v", videoPath, err)
}
return
Expand All @@ -90,3 +81,15 @@ func handleRecordedVideo(t *testing.T, page playwright.Page, targetVideoPath str
}
})
}

func buildPopupVideoPath(targetPath, videoPath string) string {
if !strings.Contains(targetPath, "popup") {
return targetPath
}
uuid := filepath.Base(videoPath)
uuid = strings.TrimSuffix(uuid, ".webm")
if len(uuid) > 8 {
uuid = uuid[:8]
}
return strings.Replace(targetPath, "popup", fmt.Sprintf("popup-%s", uuid), 1)
}
Loading
Loading