From c11002914d1202a7085b55d6b0065124bd17d878 Mon Sep 17 00:00:00 2001
From: Yetkin Timocin <ytimocin@microsoft.com>
Date: Tue, 16 Apr 2024 09:11:06 -0700
Subject: [PATCH] Adding test AKS workflow to v0.32 (#1052)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Run functional tests every 2 hours and add another workflow that runs… (#1020)

* Run functional tests every 2 hours and add another workflow that runs the tests on AKS instead of k3d

Signed-off-by: ytimocin <ytimocin@microsoft.com>

* Triggering workflow

Signed-off-by: ytimocin <ytimocin@microsoft.com>

---------

Signed-off-by: ytimocin <ytimocin@microsoft.com>

* Removing the run of the AKS workflow on PRs (#1051)

Signed-off-by: ytimocin <ytimocin@microsoft.com>

* Update the timeout (30s) for Playwright

Signed-off-by: ytimocin <ytimocin@microsoft.com>

---------

Signed-off-by: ytimocin <ytimocin@microsoft.com>
---
 .github/scripts/cleanup-cluster.sh       |  53 ++++
 .github/workflows/test-aks.yaml          | 374 +++++++++++++++++++++++
 .github/workflows/test.yaml              |  25 +-
 playwright/package-lock.json             |  85 ++++--
 playwright/package.json                  |  10 +-
 playwright/playwright.config.ts          |  25 +-
 playwright/tests/demo/demo.app.spec.ts   | 120 +++++---
 playwright/tests/eshop/eshop.app.spec.ts | 115 ++++---
 samples/eshop/eshop.bicep                |   7 +-
 9 files changed, 645 insertions(+), 169 deletions(-)
 create mode 100755 .github/scripts/cleanup-cluster.sh
 create mode 100644 .github/workflows/test-aks.yaml

diff --git a/.github/scripts/cleanup-cluster.sh b/.github/scripts/cleanup-cluster.sh
new file mode 100755
index 00000000..fcecaa56
--- /dev/null
+++ b/.github/scripts/cleanup-cluster.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+
+# ------------------------------------------------------------
+# Copyright 2023 The Radius Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ------------------------------------------------------------
+
+set -e
+
+echo "Cleaning up cluster"
+
+# Delete all test resources in queuemessages.
+if kubectl get crd queuemessages.ucp.dev >/dev/null 2>&1; then
+    echo "delete all resources in queuemessages.ucp.dev"
+    kubectl delete queuemessages.ucp.dev -n radius-system --all
+fi
+
+# Testing deletion of deployment.apps.
+
+# Delete all test resources in resources without proxy resource.
+if kubectl get crd resources.ucp.dev >/dev/null 2>&1; then
+    echo "delete all resources in resources.ucp.dev"
+    resources=$(kubectl get resources.ucp.dev -n radius-system --no-headers -o custom-columns=":metadata.name")
+    for r in $resources; do
+        echo "delete resource: $r"
+        kubectl delete resources.ucp.dev $r -n radius-system --ignore-not-found=true
+    done
+fi
+
+# Delete all test namespaces.
+# Any namespace that is not in the list below will be deleted.
+echo "Delete all test namespaces"
+namespaces=$(kubectl get namespace |
+    grep -vE '(radius-system|kube-system|kube-public|kube-node-lease|gatekeeper-system|default|dapr-system|cert-manager)' |
+    awk '{print $1}')
+for ns in $namespaces; do
+    if [ -z "$ns" ]; then
+        break
+    fi
+    echo "deleting namespaces: $ns"
+    kubectl delete namespace $ns --ignore-not-found=true
+done
diff --git a/.github/workflows/test-aks.yaml b/.github/workflows/test-aks.yaml
new file mode 100644
index 00000000..390a6d4d
--- /dev/null
+++ b/.github/workflows/test-aks.yaml
@@ -0,0 +1,374 @@
+name: Test Samples (AKS and EKS)
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "Radius version number to use (e.g. 0.1.0, 0.1.0-rc1, edge). Defaults to edge."
+        required: false
+        default: "edge"
+        type: string
+  push:
+    branches:
+      - v*.*
+      - edge
+    paths:
+      - "samples/**"
+      - ".github/workflows/**"
+  pull_request:
+    types: [opened, synchronize, reopened]
+    branches:
+      - v*.*
+      - edge
+  schedule: # Run every 2 hours
+    - cron: "0 */2 * * *"
+env:
+  RUN_IDENTIFIER: samplestest-${{ github.run_id }}-${{ github.run_attempt }}
+jobs:
+  # setup the test environment
+  setup:
+    name: Setup
+    runs-on: ubuntu-latest
+    env:
+      BRANCH: ${{ github.base_ref || github.ref_name }}
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      AZURE_LOCATION: eastus
+      AKS_RESOURCE_GROUP: samples-test-rg
+      AKS_CLUSTER_NAME: samples-test-aks
+      AWS_REGION: us-west-2
+      AWS_ZONES: us-west-2a,us-west-2b,us-west-2c
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 20
+      - name: az CLI login
+        run: |
+          az login --service-principal \
+            --username ${{ secrets.AZURE_SANDBOX_APP_ID }} \
+            --password ${{ secrets.AZURE_SANDBOX_PASSWORD }} \
+            --tenant ${{ secrets.AZURE_SANDBOX_TENANT_ID }}
+      - name: Get kubeconf credential for AKS cluster
+        run: |
+          az aks get-credentials \
+            --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \
+            --resource-group ${{ env.AKS_RESOURCE_GROUP }} \
+            --name ${{ env.AKS_CLUSTER_NAME }}
+      - name: Download rad CLI
+        run: |
+          RADIUS_VERSION="${{ inputs.version }}"
+          if [[ -z "${{ inputs.version }}" ]]; then
+            RADIUS_VERSION=edge
+          fi
+          ./.github/scripts/install-radius.sh $RADIUS_VERSION
+      - name: Clean up cluster
+        run: ./.github/scripts/cleanup-cluster.sh
+      - name: Reinstall Radius after cleanup
+        run: |
+          rad install kubernetes --reinstall
+  test:
+    name: Sample tests
+    runs-on: ${{ matrix.os }}
+    needs: setup
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - name: demo
+            os: ubuntu-latest
+            runOnPullRequest: true
+            app: demo
+            env: demo
+            path: ./samples/demo/app.bicep
+            deployArgs: --application demo -p image=ghcr.io/radius-project/samples/demo:latest
+            exposeArgs: --application demo
+            uiTestFile: tests/demo/demo.app.spec.ts
+            port: 3000
+            container: demo
+          - name: dapr
+            os: ubuntu-latest-m
+            runOnPullRequest: true
+            app: dapr
+            env: dapr
+            path: ./samples/dapr/dapr.bicep
+            deployArgs: -p environment='/planes/radius/local/resourceGroups/dapr/providers/Applications.Core/environments/dapr' -p frontendImage=ghcr.io/radius-project/samples/dapr-frontend:latest -p backendImage=ghcr.io/radius-project/samples/dapr-backend:latest
+          - name: volumes
+            os: ubuntu-latest
+            runOnPullRequest: true
+            app: myapp
+            env: volumes
+            path: ./samples/volumes/app.bicep
+            deployArgs: -p image=ghcr.io/radius-project/samples/volumes:latest
+          - name: eshop-containers
+            os: ubuntu-latest-m
+            runOnPullRequest: true
+            app: eshop
+            env: containers
+            path: ./samples/eshop/eshop.bicep
+            uiTestFile: tests/eshop/eshop.app.spec.ts
+            deployArgs: -p environment='/planes/radius/local/resourceGroups/eshop-containers/providers/Applications.Core/environments/containers'
+          - name: eshop-azure
+            os: ubuntu-latest-m
+            runOnPullRequest: true
+            app: eshop-azure
+            env: azure
+            path: ./samples/eshop/eshop.bicep
+            uiTestFile: tests/eshop/eshop.app.spec.ts
+            deployArgs: -p environment='/planes/radius/local/resourceGroups/eshop-azure/providers/Applications.Core/environments/azure' -p applicationName=eshop-azure
+            credential: azure
+          - name: eshop-aws
+            os: ubuntu-latest-m
+            runOnPullRequest: true
+            app: eshop-aws
+            env: aws
+            path: ./samples/eshop/eshop.bicep
+            uiTestFile: tests/eshop/eshop.app.spec.ts
+            deployArgs: -p environment='/planes/radius/local/resourceGroups/eshop-aws/providers/Applications.Core/environments/aws' -p applicationName=eshop-aws
+            credential: aws
+    env:
+      BRANCH: ${{ github.base_ref || github.ref_name }}
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      AZURE_LOCATION: eastus
+      AKS_RESOURCE_GROUP: samples-test-rg
+      AKS_CLUSTER_NAME: samples-test-aks
+      AWS_REGION: us-west-2
+      AWS_ZONES: us-west-2a,us-west-2b,us-west-2c
+    steps:
+      # Setup the test assets and configuration
+      - name: Generate output variables
+        id: gen-id
+        run: |
+          RUN_IDENTIFIER=${{ env.RUN_IDENTIFIER }}-${{ matrix.name }}
+
+          if [[ "${{ github.event_name }}" == "pull_request" && "${{ matrix.runOnPullRequest }}" == "false" ]]; then
+            RUN_TEST=false
+          else
+            RUN_TEST=true
+          fi
+
+          # Set output variables to be used in the other jobs
+          echo "RUN_IDENTIFIER=${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT
+          echo "TEST_AZURE_RESOURCE_GROUP=rg-${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT
+          echo "TEST_EKS_CLUSTER_NAME=eks-${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT
+          echo "RUN_TEST=${RUN_TEST}" >> $GITHUB_OUTPUT
+      - name: Checkout code
+        if: steps.gen-id.outputs.RUN_TEST == 'true'
+        uses: actions/checkout@v3
+      - name: Ensure inputs.version is valid semver
+        if: steps.gen-id.outputs.RUN_TEST == 'true' && inputs.version != ''
+        run: |
+          python ./.github/scripts/validate_semver.py ${{ inputs.version }}
+      - name: Setup Node
+        if: steps.gen-id.outputs.RUN_TEST == 'true'
+        uses: actions/setup-node@v3
+        with:
+          node-version: 20
+      - name: az CLI login
+        if: steps.gen-id.outputs.RUN_TEST == 'true'
+        run: |
+          az login --service-principal \
+            --username ${{ secrets.AZURE_SANDBOX_APP_ID }} \
+            --password ${{ secrets.AZURE_SANDBOX_PASSWORD }} \
+            --tenant ${{ secrets.AZURE_SANDBOX_TENANT_ID }}
+      - name: Configure AWS
+        if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws'
+        run: |
+          aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          aws configure set region ${{ env.AWS_REGION }}
+          aws configure set output json
+      - name: Get kubeconf credential for AKS cluster
+        if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.name != 'eshop-aws'
+        run: |
+          az aks get-credentials \
+            --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \
+            --resource-group ${{ env.AKS_RESOURCE_GROUP }} \
+            --name ${{ env.AKS_CLUSTER_NAME }}
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v2
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      # Create and install test environment
+      - name: Create Azure resource group
+        if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'azure'
+        id: create-azure-resource-group
+        run: |
+          current_time=$(date +%s)
+          az group create \
+            --location ${{ env.AZURE_LOCATION }} \
+            --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} \
+            --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \
+            --tags creationTime=$current_time
+          while [ $(az group exists --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }}) = false ]; do
+            echo "Waiting for resource group ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} to be created..."
+            sleep 5
+          done
+      - name: Create EKS Cluster
+        if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws'
+        id: create-eks
+        run: |
+          curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
+          sudo mv /tmp/eksctl /usr/local/bin
+          eksctl create cluster \
+            --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} \
+            --nodes-min 1 --nodes-max 2 --node-type t3.large \
+            --zones ${{ env.AWS_ZONES }} \
+            --managed \
+            --region ${{ env.AWS_REGION }}
+          while [[ "$(eksctl get cluster ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} --region ${{ env.AWS_REGION }} -o json | jq -r .[0].Status)" != "ACTIVE" ]]; do
+            echo "Waiting for EKS cluster to be created..."
+            sleep 60
+          done
+          aws eks update-kubeconfig --region ${{ env.AWS_REGION }} --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }}
+        timeout-minutes: 60
+        continue-on-error: false
+      - name: Download rad CLI
+        if: steps.gen-id.outputs.RUN_TEST == 'true'
+        run: |
+          RADIUS_VERSION="${{ inputs.version }}"
+          if [[ -z "${{ inputs.version }}" ]]; then
+            RADIUS_VERSION=edge
+          fi
+          ./.github/scripts/install-radius.sh $RADIUS_VERSION
+      ## This step is temporary until we have Recipe Packs for Azure & AWS and update the eShop sample
+      - name: Configure Radius test workspace
+        if: steps.gen-id.outputs.RUN_TEST == 'true'
+        run: |
+          set -x
+
+          export PATH=$GITHUB_WORKSPACE/bin:$PATH
+          which rad || { echo "cannot find rad"; exit 1; }
+
+          # Install Radius for AWS
+          if [[ "${{ matrix.credential }}" == "aws" ]]; then
+            rad install kubernetes
+          fi
+
+          echo "*** Create workspace, group and environment for test ***"
+          rad workspace create kubernetes --force
+          rad group create ${{ matrix.name }}
+          rad group switch ${{ matrix.name }}
+          rad env create ${{ matrix.env }}
+          rad env switch ${{ matrix.env }}
+
+          if [[ "${{ matrix.credential }}" == "azure" ]]; then
+            rad deploy ./samples/eshop/environments/azure.bicep -p azureResourceGroup=${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} -p azureSubscriptionId=${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }}
+            rad env switch ${{ matrix.env }}
+          elif [[ "${{ matrix.credential }}" == "aws" ]]; then
+            rad deploy ./samples/eshop/environments/aws.bicep -p awsAccountId=${{ secrets.AWS_ACCOUNT_ID }} -p awsRegion=${{ env.AWS_REGION }} -p eksClusterName=${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }}
+            rad env switch ${{ matrix.env }}
+          else
+            echo "Registering recipes for ${{ matrix.env }} environment..."
+            rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/rediscaches:latest --resource-type Applications.Datastores/redisCaches
+            rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/mongodatabases:latest --resource-type Applications.Datastores/mongoDatabases
+            rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/sqldatabases:latest --resource-type Applications.Datastores/sqlDatabases
+            rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/rabbitmqqueues:latest --resource-type Applications.Messaging/rabbitMQQueues
+          fi
+      - name: Configure cloud credentials
+        if: steps.gen-id.outputs.RUN_TEST == 'true' && ( matrix.credential == 'azure' || matrix.credential == 'aws')
+        run: |
+          if [[ "${{ matrix.credential }}" == "azure" ]]; then
+            rad env update ${{ matrix.env }} --azure-subscription-id ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} --azure-resource-group ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }}
+            rad credential register azure --client-id ${{ secrets.AZURE_SANDBOX_APP_ID }} --client-secret ${{ secrets.AZURE_SANDBOX_PASSWORD }} --tenant-id ${{ secrets.AZURE_SANDBOX_TENANT_ID }}
+          fi
+          if [[ "${{ matrix.credential }}" == "aws" ]]; then
+            rad env update ${{ matrix.env }} --aws-region ${{ env.AWS_REGION }} --aws-account-id ${{ secrets.AWS_ACCOUNT_ID }}
+            rad credential register aws --access-key-id ${{ secrets.AWS_ACCESS_KEY_ID }} --secret-access-key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          fi
+      # Deploy application and run tests
+      - name: Deploy app
+        if: steps.gen-id.outputs.RUN_TEST == 'true'
+        id: deploy-app
+        run: rad deploy ${{ matrix.path }} ${{ matrix.deployArgs }} -e ${{ matrix.env }}
+      - name: Wait for all pods to be ready
+        if: steps.gen-id.outputs.RUN_TEST == 'true'
+        id: wait-for-pods
+        run: |
+          namespace="${{ matrix.env }}-${{ matrix.app }}"
+          label="radapp.io/application=${{ matrix.app }}"
+          kubectl rollout status deployment -l $label -n $namespace --timeout=90s
+      - name: Run Playwright Test
+        if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.uiTestFile != ''
+        id: run-playwright-test
+        run: |
+          if [[ "${{ matrix.container }}" != "" ]]; then
+            rad resource expose containers ${{ matrix.container }} ${{ matrix.exposeArgs }} --port ${{ matrix.port }} &
+            export ENDPOINT="http://localhost:3000/"
+          else
+            endpoint="$(rad app status -a ${{ matrix.app }} | sed 's/ /\n/g' | grep http)"
+            echo "Endpoint: $endpoint"
+            export ENDPOINT=$endpoint
+          fi
+
+          cd playwright/
+          npm ci
+          npx playwright install --with-deps
+          npx playwright test ${{ matrix.uiTestFile }} --retries 3
+      - name: Upload Playwright Results
+        uses: actions/upload-artifact@v3
+        if: always() && ( steps.run-playwright-test.outcome == 'success' || steps.run-playwright-test.outcome == 'failure' )
+        with:
+          name: playwright-report-${{ matrix.name }}
+          path: playwright/playwright-report/
+          retention-days: 30
+          if-no-files-found: error
+      # Handle failures
+      - name: Get Pod logs for failed tests
+        id: get-pod-logs
+        if: failure() && (steps.run-playwright-test.outcome == 'failure' || steps.wait-for-pods.outcome == 'failure' || steps.deploy-app.outcome == 'failure')
+        run: |
+          # Create pod-logs directory
+          mkdir -p playwright/pod-logs/${{ matrix.name }}
+          # Get pod logs and save to file
+          namespace="${{ matrix.env }}-${{ matrix.app }}"
+          label="radapp.io/application=${{ matrix.app }}"
+          pod_names=($(kubectl get pods -l $label -n $namespace -o jsonpath='{.items[*].metadata.name}'))
+          for pod_name in "${pod_names[@]}"; do
+            kubectl logs $pod_name -n $namespace > playwright/pod-logs/${{ matrix.name }}/${pod_name}.txt
+          done
+          echo "Pod logs saved to playwright/pod-logs/${{ matrix.name }}/"
+          # Get kubernetes events and save to file
+          kubectl get events -n $namespace > playwright/pod-logs/${{ matrix.name }}/events.txt
+      - name: Upload Pod logs for failed tests
+        uses: actions/upload-artifact@v3
+        if: failure() && steps.get-pod-logs.outcome == 'success'
+        with:
+          name: ${{ matrix.name }}-pod-logs
+          path: playwright/pod-logs/${{ matrix.name }}
+          retention-days: 30
+          if-no-files-found: error
+      - name: Create GitHub issue on failure
+        if: failure() && github.event_name == 'schedule'
+        run: gh issue create --title "Samples deployment failed for ${{ matrix.app }}" --body "Test failed on ${{ github.repository }}. See [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details." --repo ${{ github.repository }} --label test-failure
+      # Cleanup
+      - name: Delete app and environment
+        if: always() && steps.gen-id.outputs.RUN_TEST == 'true'
+        run: |
+          if command -v rad &> /dev/null; then
+            rad app delete ${{ matrix.app }} -y
+            rad env delete ${{ matrix.env }} -y
+          fi
+      - name: Delete Azure resource group
+        if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && steps.create-azure-resource-group.outcome == 'success'
+        run: |
+          # Delete Azure resources created by the test
+          # if deletion fails, purge workflow will purge the resource group and its resources later
+          az group delete \
+            --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \
+            --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} \
+            --yes
+      - name: Delete AWS Resources
+        if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws'
+        run: |
+          # Delete all AWS resources created by the test
+          ./.github/scripts/delete-aws-resources.sh '/planes/radius/local/resourcegroups/${{ matrix.env }}/providers/Applications.Core/applications/${{ matrix.app }}'
+      - name: Delete EKS Cluster
+        if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws'
+        run: |
+          # Delete EKS cluster
+          echo "Deleting EKS cluster: ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }}"
+          eksctl delete cluster --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} --region ${{ env.AWS_REGION }} --wait --force
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 0bee5a8a..043c33b1 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,12 +1,12 @@
-name: Test Samples
+name: Test Samples (k3d and EKS)
 
 on:
   workflow_dispatch:
     inputs:
       version:
-        description: 'Radius version number to use (e.g. 0.1.0, 0.1.0-rc1, edge). Defaults to edge.'
+        description: "Radius version number to use (e.g. 0.1.0, 0.1.0-rc1, edge). Defaults to edge."
         required: false
-        default: 'edge'
+        default: "edge"
         type: string
   push:
     branches:
@@ -20,8 +20,8 @@ on:
     branches:
       - v*.*
       - edge
-  schedule: # 7:45 AM Pacific Time
-    - cron: "45 15 * * *"
+  schedule: # Run every 2 hours
+    - cron: "0 */2 * * *"
 env:
   RUN_IDENTIFIER: samplestest-${{ github.run_id }}-${{ github.run_attempt }}
 jobs:
@@ -75,7 +75,7 @@ jobs:
             uiTestFile: tests/eshop/eshop.app.spec.ts
             enableDapr: false
           - name: eshop-azure
-            os: ubuntu-latest
+            os: ubuntu-latest-m
             runOnPullRequest: false
             app: eshop
             env: azure
@@ -84,7 +84,7 @@ jobs:
             credential: azure
             enableDapr: false
           - name: eshop-aws
-            os: ubuntu-latest
+            os: ubuntu-latest-m
             runOnPullRequest: false
             app: eshop
             env: aws
@@ -134,7 +134,7 @@ jobs:
         if: steps.gen-id.outputs.RUN_TEST == 'true'
         uses: actions/setup-node@v3
         with:
-          node-version: 16
+          node-version: 20
       - name: az CLI login
         if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'azure'
         run: |
@@ -214,7 +214,7 @@ jobs:
         run: |
           RADIUS_VERSION="${{ inputs.version }}"
           if [[ -z "${{ inputs.version }}" ]]; then
-              RADIUS_VERSION=edge
+            RADIUS_VERSION=edge
           fi
           ./.github/scripts/install-radius.sh $RADIUS_VERSION
       - name: Initialize default environment
@@ -258,6 +258,7 @@ jobs:
       # Deploy application and run tests
       - name: Deploy app
         if: steps.gen-id.outputs.RUN_TEST == 'true'
+        id: deploy-app
         run: rad deploy ${{ matrix.path }} ${{ matrix.deployArgs }}
       - name: Wait for all pods to be ready
         if: steps.gen-id.outputs.RUN_TEST == 'true'
@@ -272,6 +273,8 @@ jobs:
         run: |
           if [[ "${{ matrix.container }}" != "" ]]; then
             rad resource expose containers ${{ matrix.container }} ${{ matrix.exposeArgs }} --port ${{ matrix.port }} &
+            echo "Endpoint: http://localhost:${{ matrix.port }}"
+            export ENDPOINT="http://localhost:${{ matrix.port }}"
           else
             endpoint="$(rad app status -a ${{ matrix.app }} | sed 's/ /\n/g' | grep http)"
             echo "Endpoint: $endpoint"
@@ -292,12 +295,12 @@ jobs:
       # Handle failures
       - name: Get Pod logs for failed tests
         id: get-pod-logs
-        if: failure() && (steps.run-playwright-test.outcome == 'failure' || steps.wait-for-pods.outcome == 'failure')
+        if: failure() && (steps.run-playwright-test.outcome == 'failure' || steps.wait-for-pods.outcome == 'failure' || steps.deploy-app.outcome == 'failure')
         run: |
           # Create pod-logs directory
           mkdir -p playwright/pod-logs/${{ matrix.name }}
           # Get pod logs and save to file
-          namespace="default-${{ matrix.app }}"
+          namespace="${{ matrix.env }}-${{ matrix.app }}"
           label="radapp.io/application=${{ matrix.app }}"
           pod_names=($(kubectl get pods -l $label -n $namespace -o jsonpath='{.items[*].metadata.name}'))
           for pod_name in "${pod_names[@]}"; do
diff --git a/playwright/package-lock.json b/playwright/package-lock.json
index 0cd663b4..062e31c2 100644
--- a/playwright/package-lock.json
+++ b/playwright/package-lock.json
@@ -9,44 +9,43 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
-        "uuid": "^9.0.0"
+        "uuid": "^9.0.1"
       },
       "devDependencies": {
-        "@playwright/test": "^1.35.0",
-        "@types/node": "^20.6.0",
-        "@types/uuid": "^9.0.3",
-        "typescript": "^5.2.2"
+        "@playwright/test": "^1.43.0",
+        "@types/node": "^20.12.6",
+        "@types/uuid": "^9.0.8",
+        "typescript": "^5.4.4"
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.35.0",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.0.tgz",
-      "integrity": "sha512-6qXdd5edCBynOwsz1YcNfgX8tNWeuS9fxy5o59D0rvHXxRtjXRebB4gE4vFVfEMXl/z8zTnAzfOs7aQDEs8G4Q==",
+      "version": "1.43.0",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.0.tgz",
+      "integrity": "sha512-Ebw0+MCqoYflop7wVKj711ccbNlrwTBCtjY5rlbiY9kHL2bCYxq+qltK6uPsVBGGAOb033H2VO0YobcQVxoW7Q==",
       "dev": true,
       "dependencies": {
-        "@types/node": "*",
-        "playwright-core": "1.35.0"
+        "playwright": "1.43.0"
       },
       "bin": {
         "playwright": "cli.js"
       },
       "engines": {
         "node": ">=16"
-      },
-      "optionalDependencies": {
-        "fsevents": "2.3.2"
       }
     },
     "node_modules/@types/node": {
-      "version": "20.6.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz",
-      "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==",
-      "dev": true
+      "version": "20.12.6",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.6.tgz",
+      "integrity": "sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==",
+      "dev": true,
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
     },
     "node_modules/@types/uuid": {
-      "version": "9.0.3",
-      "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz",
-      "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==",
+      "version": "9.0.8",
+      "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
+      "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
       "dev": true
     },
     "node_modules/fsevents": {
@@ -63,10 +62,28 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/playwright": {
+      "version": "1.43.0",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz",
+      "integrity": "sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==",
+      "dev": true,
+      "dependencies": {
+        "playwright-core": "1.43.0"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.2"
+      }
+    },
     "node_modules/playwright-core": {
-      "version": "1.35.0",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.0.tgz",
-      "integrity": "sha512-muMXyPmIx/2DPrCHOD1H1ePT01o7OdKxKj2ebmCAYvqhUy+Y1bpal7B0rdoxros7YrXI294JT/DWw2LqyiqTPA==",
+      "version": "1.43.0",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz",
+      "integrity": "sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -76,9 +93,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
-      "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+      "version": "5.4.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
+      "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
       "dev": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -88,10 +105,20 @@
         "node": ">=14.17"
       }
     },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "dev": true
+    },
     "node_modules/uuid": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
-      "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
       "bin": {
         "uuid": "dist/bin/uuid"
       }
diff --git a/playwright/package.json b/playwright/package.json
index 1a52aee0..f29f910b 100644
--- a/playwright/package.json
+++ b/playwright/package.json
@@ -8,12 +8,12 @@
   "author": "",
   "license": "ISC",
   "devDependencies": {
-    "@playwright/test": "^1.35.0",
-    "@types/node": "^20.6.0",
-    "@types/uuid": "^9.0.3",
-    "typescript": "^5.2.2"
+    "@playwright/test": "^1.43.0",
+    "@types/node": "^20.12.6",
+    "@types/uuid": "^9.0.8",
+    "typescript": "^5.4.4"
   },
   "dependencies": {
-    "uuid": "^9.0.0"
+    "uuid": "^9.0.1"
   }
 }
diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts
index e2b6772b..4e300a13 100644
--- a/playwright/playwright.config.ts
+++ b/playwright/playwright.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig, devices } from '@playwright/test';
+import { defineConfig, devices } from "@playwright/test";
 
 /**
  * Read environment variables from file.
@@ -10,7 +10,7 @@ import { defineConfig, devices } from '@playwright/test';
  * See https://playwright.dev/docs/test-configuration.
  */
 export default defineConfig({
-  testDir: './tests',
+  testDir: "./tests",
   /* Run tests in files in parallel */
   fullyParallel: true,
   /* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -20,31 +20,34 @@ export default defineConfig({
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: 'html',
+  reporter: "html",
   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
   use: {
     /* Base URL to use in actions like `await page.goto('/')`. */
     // baseURL: 'http://127.0.0.1:3000',
 
     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
-    trace: 'on-first-retry',
+    trace: "on-first-retry",
   },
-
   /* Configure projects for major browsers */
   projects: [
     {
-      name: 'chromium',
-      use: { ...devices['Desktop Chrome'] },
+      name: "chromium",
+      use: { ...devices["Desktop Chrome"] },
     },
 
     {
-      name: 'firefox',
-      use: { ...devices['Desktop Firefox'] },
+      name: "firefox",
+      use: { ...devices["Desktop Firefox"] },
     },
 
     {
-      name: 'webkit',
-      use: { ...devices['Desktop Safari'] },
+      name: "webkit",
+      use: { ...devices["Desktop Safari"] },
     },
   ],
+  timeout: 1 * 60 * 1000,
+  expect: {
+    timeout: 30 * 1000,
+  },
 });
diff --git a/playwright/tests/demo/demo.app.spec.ts b/playwright/tests/demo/demo.app.spec.ts
index 1d69fa67..d8751c71 100644
--- a/playwright/tests/demo/demo.app.spec.ts
+++ b/playwright/tests/demo/demo.app.spec.ts
@@ -3,82 +3,106 @@ import { v4 as uuidv4 } from "uuid";
 
 test("To-Do App Basic UI Checks", async ({ page }) => {
   // Listen for all console events and handle errors
-  page.on('console', msg => {
-    if (msg.type() === 'error') {
+  page.on("console", (msg) => {
+    if (msg.type() === "error") {
       console.log(`Error text: "${msg.text()}"`);
     }
   });
 
-  // Go to http://localhost:3000/
-  await page.goto("http://localhost:3000/");
+  let endpoint = process.env.ENDPOINT;
+  expect(endpoint).toBeDefined();
+
+  // Remove quotes from the endpoint if they exist
+  endpoint = (endpoint as string).replace(/['"]+/g, "");
+  console.log(`Endpoint: ${endpoint}`);
+  await page.goto(endpoint);
 
   // Expect page to have proper URL
-  expect(page).toHaveURL("http://localhost:3000/");
+  expect(page).toHaveURL(endpoint);
 
   // Expect page to have proper title
   expect(page).toHaveTitle("Radius Demo");
 
   // Make sure the tabs are visible
-  await expect(page.getByRole("link", { name: "Radius Demo" }))
-    .toBeVisible();
-  await expect(page.getByRole("link", { name: "Container Info" }))
-    .toBeVisible();
-  await expect(page.getByRole("link", { name: "Todo List" }))
-    .toBeVisible();
+  await expect(page.getByRole("link", { name: "Radius Demo" })).toBeVisible();
+  await expect(
+    page.getByRole("link", { name: "Container Info" })
+  ).toBeVisible();
+  await expect(page.getByRole("link", { name: "Todo List" })).toBeVisible();
 
   // Make sure the tabs are clickable
-  await page.getByRole("link", { name: "Radius Demo" })
-    .click();
-  await page.getByRole("link", { name: "Container Info" })
-    .click();
-  await page.getByRole("link", { name: "Todo List" })
-    .click();
+  await page.getByRole("link", { name: "Radius Demo" }).click();
+  await page.getByRole("link", { name: "Container Info" }).click();
+  await page.getByRole("link", { name: "Todo List" }).click();
 
   // Go back to Main Page
-  await page.getByRole("link", { name: "Radius Demo" })
-    .click();
+  await page.getByRole("link", { name: "Radius Demo" }).click();
 
-  await page.getByRole('button', { name: '📄 Environment variables' }).click();
+  await page.getByRole("button", { name: "📄 Environment variables" }).click();
 
   // Make sure important environment variables are visible
-  await expect(page.getByRole('cell', { name: 'CONNECTION_REDIS_CONNECTIONSTRING' }).getByText('CONNECTION_REDIS_CONNECTIONSTRING')).toBeVisible();
-  await expect(page.getByRole('cell', { name: 'CONNECTION_REDIS_HOST' }).getByText('CONNECTION_REDIS_HOST')).toBeVisible();
-  await expect(page.getByRole('cell', { name: 'CONNECTION_REDIS_PORT' }).getByText('CONNECTION_REDIS_PORT')).toBeVisible();
+  await expect(
+    page
+      .getByRole("cell", { name: "CONNECTION_REDIS_CONNECTIONSTRING" })
+      .getByText("CONNECTION_REDIS_CONNECTIONSTRING")
+  ).toBeVisible();
+  await expect(
+    page
+      .getByRole("cell", { name: "CONNECTION_REDIS_HOST" })
+      .getByText("CONNECTION_REDIS_HOST")
+  ).toBeVisible();
+  await expect(
+    page
+      .getByRole("cell", { name: "CONNECTION_REDIS_PORT" })
+      .getByText("CONNECTION_REDIS_PORT")
+  ).toBeVisible();
 });
 
 test("Add an item and check basic functionality", async ({ page }) => {
-  await page.goto("http://localhost:3000/");
+  // Listen for all console events and handle errors
+  page.on("console", (msg) => {
+    if (msg.type() === "error") {
+      console.log(`Error text: "${msg.text()}"`);
+    }
+  });
+
+  let endpoint = process.env.ENDPOINT;
+  expect(endpoint).toBeDefined();
+
+  // Remove quotes from the endpoint if they exist
+  endpoint = (endpoint as string).replace(/['"]+/g, "");
+  console.log(`Endpoint: ${endpoint}`);
+  await page.goto(endpoint);
+
   // Go to Todo List Page
   await page.getByRole("link", { name: "Todo List" }).click();
 
   // Make sure the input is visible
-  await expect(page.getByPlaceholder("What do you need to do?"))
-    .toBeVisible();
+  await expect(page.getByPlaceholder("What do you need to do?")).toBeVisible();
 
   // Add a todo item
   const todoItem = `Test Item ${uuidv4()}`;
-  await page.getByPlaceholder("What do you need to do?")
-    .fill(todoItem);
-  await page.getByRole("button", { name: "Add send" })
-    .click();
+  await page.getByPlaceholder("What do you need to do?").fill(todoItem);
+  await page.getByRole("button", { name: "Add send" }).click();
 
   // Make sure the todo item is visible now
-  await expect(page.getByRole("cell", { name: todoItem }))
-    .toBeVisible();
+  await expect(page.getByRole("cell", { name: todoItem })).toBeVisible();
 
   // Complete a todo item
 
   // At first we expect a lightbulb icon in the status column for the given todo item
-  await expect(page.getByRole("row", { name: `${todoItem} lightbulb` }))
-    .toBeVisible();
+  await expect(
+    page.getByRole("row", { name: `${todoItem} lightbulb` })
+  ).toBeVisible();
 
   // Then we expect to have a Complete button for the given todo item
-  await expect(page
-    .getByRole("row", {
-      name: `${todoItem} lightbulb Complete done Delete delete`,
-    })
-    .getByRole("button", { name: "Complete done" }))
-    .toBeVisible();
+  await expect(
+    page
+      .getByRole("row", {
+        name: `${todoItem} lightbulb Complete done Delete delete`,
+      })
+      .getByRole("button", { name: "Complete done" })
+  ).toBeVisible();
 
   // We click the Complete button
   await page
@@ -89,12 +113,13 @@ test("Add an item and check basic functionality", async ({ page }) => {
     .click();
 
   // Now we expect a checkmark icon represented as `done` in the status column for the given todo item
-  await expect(page
-    .getByRole("row", {
-      name: `${todoItem} done Complete done Delete delete`,
-    })
-    .getByRole("button", { name: "Complete done" }))
-    .toBeVisible();
+  await expect(
+    page
+      .getByRole("row", {
+        name: `${todoItem} done Complete done Delete delete`,
+      })
+      .getByRole("button", { name: "Complete done" })
+  ).toBeVisible();
 
   // Delete a todo item
   await page
@@ -105,6 +130,5 @@ test("Add an item and check basic functionality", async ({ page }) => {
     .click();
 
   // Make sure the todo item is not visible now
-  await expect(page.getByRole("cell", { name: todoItem }))
-    .not.toBeVisible();
+  await expect(page.getByRole("cell", { name: todoItem })).not.toBeVisible();
 });
diff --git a/playwright/tests/eshop/eshop.app.spec.ts b/playwright/tests/eshop/eshop.app.spec.ts
index 0b67ab26..8af8ffe8 100644
--- a/playwright/tests/eshop/eshop.app.spec.ts
+++ b/playwright/tests/eshop/eshop.app.spec.ts
@@ -1,103 +1,92 @@
 import { test, expect } from "@playwright/test";
 
-test("eShop on Containers App Basic UI and Functionality Checks", async ({ page }) => {
+test("eShop on Containers App Basic UI and Functionality Checks", async ({
+  page,
+}) => {
   // Listen for all console events and handle errors
-  page.on('console', msg => {
-    if (msg.type() === 'error') {
+  page.on("console", (msg) => {
+    if (msg.type() === "error") {
       console.log(`Error text: "${msg.text()}"`);
     }
   });
 
-  let endpoint = process.env.ENDPOINT
-  expect(endpoint).toBeDefined()
-  
+  let endpoint = process.env.ENDPOINT;
+  expect(endpoint).toBeDefined();
+
   // Remove quotes from the endpoint if they exist
-  endpoint = (endpoint as string).replace(/['"]+/g, '')
-  
+  endpoint = (endpoint as string).replace(/['"]+/g, "");
+  console.log(`Endpoint: ${endpoint}`);
   await page.goto(endpoint);
 
   // Expect page to have proper URL
-  expect(page).toHaveURL(endpoint+"/catalog");
-
+  await expect(page).toHaveURL(new RegExp(`${endpoint}/catalog.*`));
   // Expect page to have proper title
-  expect(page).toHaveTitle("eShopOnContainers - SPA");
+  await expect(page).toHaveTitle("eShopOnContainers - SPA");
 
-  // Check for the LOGIN button
-  await expect(page.getByText("LOGIN"))
-    .toBeVisible();
-
-  // Click on the LOGIN button
-  await page.getByText("LOGIN").click();
+  // Check for the LOGIN button in the home page
+  const loginButton = page.getByText("LOGIN");
+  await expect(loginButton).toBeVisible();
+  await loginButton.click();
 
-  // Expect page to have proper title
-  expect(page).toHaveTitle("eShopOnContainers - Identity");
+  // Expect login page to have proper title
+  await expect(page).toHaveTitle("eShopOnContainers - Identity");
 
   // Fill in the username and password
-  expect(page.getByPlaceholder('Username'))
-    .toBeVisible();
-  await page.getByPlaceholder('Username')
-    .click();
-  await page.getByPlaceholder('Username')
-    .fill('alice');
-
-  expect(page.getByPlaceholder('Password'))
-    .toBeVisible();
-  await page.getByPlaceholder('Password')
-    .click();
-  await page.getByPlaceholder('Password')
-    .fill('Pass123$');
+  const username = page.getByPlaceholder("Username");
+  await expect(username).toBeVisible();
+  await username.click();
+  await username.fill("alice");
+
+  const password = page.getByPlaceholder("Password");
+  await expect(password).toBeVisible();
+  await password.click();
+  await password.fill("Pass123$");
 
   // Click on the LOGIN button
-  await page.getByRole('button', { name: 'Login' })
-    .click();
+  await page.getByRole("button", { name: "Login" }).click();
 
   // After login, expect to be redirected to the catalog page
   // Expect page to have proper URL
-  expect(page).toHaveURL(endpoint+"/catalog");
-
+  await expect(page).toHaveURL(new RegExp(`${endpoint}/catalog.*`));
   // Expect page to have proper title
-  expect(page).toHaveTitle("eShopOnContainers - SPA");
+  await expect(page).toHaveTitle("eShopOnContainers - SPA");
 
   // Logged user details should be visible
-  expect(page.getByText('AliceSmith@email.com'))
-    .toBeVisible();
-
+  const user = page.getByText("AliceSmith@email.com");
+  await expect(user).toBeVisible();
   // Click on the user details
-  await page.getByText('AliceSmith@email.com').click();
+  await user.click();
 
   // Check dropdown menu
-  expect(page.getByText('My orders'))
-    .toBeVisible();
-  expect(page.getByText('Log Out'))
-    .toBeVisible();
+  await expect(page.getByText("My orders")).toBeVisible();
+  await expect(page.getByText("Log Out")).toBeVisible();
 
   let numberOfItemsAdded = 0;
   // Add an item to the cart
-  await page.locator('div:nth-child(2) > .esh-catalog-item > .esh-catalog-thumbnail-wrapper > .esh-catalog-thumbnail-icon > .esh-catalog-thumbnail-icon-svg')
-    .click();
+  const firstItem = page.locator("div:nth-child(1) > .esh-catalog-item");
+  await expect(firstItem).toBeVisible();
+  await firstItem.click();
+  numberOfItemsAdded++;
+
+  // Add an item to the cart
+  const secondItem = page.locator("div:nth-child(2) > .esh-catalog-item");
+  await expect(secondItem).toBeVisible();
+  await secondItem.click();
   numberOfItemsAdded++;
 
   // Go to the cart
-  await page.getByRole('link', { name: `${numberOfItemsAdded}` })
-    .click();
+  const cartLink = page.getByRole("link", { name: `${numberOfItemsAdded}` });
+  await expect(cartLink).toBeVisible();
+  await cartLink.click();
 
   // Expect page to have proper URL
-  expect(page).toHaveURL(endpoint+"/basket");
-
+  await expect(page).toHaveURL(new RegExp(`${endpoint}/basket.*`));
   // Checkout
-  await page.getByRole('button', { name: 'Checkout' })
-    .click();
-
+  await page.getByRole("button", { name: "Checkout" }).click();
   // Place the order
-  await page.getByRole('button', { name: 'Place Order' })
-    .click();
-
+  await page.getByRole("button", { name: "Place Order" }).click();
   // Continue Shopping
-  await page.getByRole('link', { name: 'Continue Shopping' })
-    .click();
-
+  await page.getByRole("link", { name: "Continue Shopping" }).click();
   // Logout
-  await page.locator('div').filter({ hasText: 'Log Out' })
-    .nth(0)
-    .click();
+  await page.locator("div").filter({ hasText: "Log Out" }).nth(0).click();
 });
diff --git a/samples/eshop/eshop.bicep b/samples/eshop/eshop.bicep
index c7a47427..ee77d102 100644
--- a/samples/eshop/eshop.bicep
+++ b/samples/eshop/eshop.bicep
@@ -5,6 +5,9 @@ import radius as rad
 @description('Radius environment ID. Set automatically by Radius')
 param environment string
 
+@description('Application name. Defaults to "eshop"')
+param applicationName string = 'eshop'
+
 @description('Container registry to pull from, with optional path. Defaults to "ghcr.io/radius-project/samples/eshop"')
 param imageRegistry string = 'ghcr.io/radius-project/samples/eshop'
 
@@ -26,7 +29,7 @@ var AZURESERVICEBUSENABLED = contains(eshopEnvironment.properties.recipes, 'Appl
 // Application --------------------------------------------------------
 
 resource eshopApplication 'Applications.Core/applications@2023-10-01-preview' = {
-  name: 'eshop'
+  name: applicationName
   properties: {
     environment: environment
   }
@@ -120,7 +123,7 @@ module payment 'services/payment.bicep' = {
 module seq 'services/seq.bicep' = {
   name: 'seq'
   params: {
-    application: eshopApplication.id 
+    application: eshopApplication.id
   }
 }