From 9c643ba48cda75bffca5278f29bccc1323717469 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Tue, 29 Jul 2025 12:45:00 -0400 Subject: [PATCH 01/83] Enhances Playwright test workflow Adds support for different application types (Vue and .NET) and improves server readiness checks. This change introduces a new `applicationType` parameter to the Playwright test template, allowing it to handle both Vue and .NET applications. It improves the reliability of the test execution by implementing a readiness check mechanism that waits for the server to be fully up and running before starting the tests. This ensures that the tests are not run against an unavailable server, which can lead to false negatives. The changes include: - Parameterization of application type (vue or dotnet) - Server readiness check implementation - Docker build arguments can now be passed through --- azure-pipelines-automated-testing.yml | 1 + azure-pipelines.yml | 1 + temp/playwright-test.yml | 278 +++++++++++++++++++++----- 3 files changed, 229 insertions(+), 51 deletions(-) diff --git a/azure-pipelines-automated-testing.yml b/azure-pipelines-automated-testing.yml index 34cda2917..132da13ce 100644 --- a/azure-pipelines-automated-testing.yml +++ b/azure-pipelines-automated-testing.yml @@ -33,6 +33,7 @@ stages: # TODO: Change to ADO Playwright template - template: temp/playwright-test.yml parameters: + applicationType: 'vue' totalShards: ${{ variables.totalShards }} targetUrl: $(BASE_URL) dockerFileName: 'Dockerfile.playwright' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9774bd48a..39519804c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -64,6 +64,7 @@ stages: jobs: - template: temp/playwright-test.yml parameters: + applicationType: 'vue' totalShards: 2 targetUrl: $(BASE_URL) dockerFileName: 'Dockerfile.playwright' diff --git a/temp/playwright-test.yml b/temp/playwright-test.yml index 5c6692586..474a0bd75 100644 --- a/temp/playwright-test.yml +++ b/temp/playwright-test.yml @@ -32,11 +32,23 @@ parameters: - name: imageTag type: string default: '$(Build.BuildId)' + - name: dockerBuildArgs + type: string + default: '' - name: envFileName type: string default: '.env.ci' + - name: applicationType + type: string + default: 'vue' + values: ['vue', 'dotnet'] + - name: readinessEndpoint + type: string + default: '/healthcheck' + - name: serverTimeoutSeconds + type: number + default: 60 - jobs: - job: playwright_tests continueOnError: true @@ -103,6 +115,14 @@ jobs: echo "Docker cleanup completed!" displayName: "Docker Cleanup" + - bash: | + printenv > "${{ parameters.envFileName }}" + if [ -z "$(GITHUB_TOKEN)" ]; then + echo "Printed env, but GITHUB_TOKEN was undefined" + fi + env: ${{ parameters.secrets }} + displayName: "Make Azure Pipeline Variables Available to Docker" + - task: Docker@2 displayName: 'Build Docker Image' inputs: @@ -110,20 +130,23 @@ jobs: dockerfile: ${{ parameters.dockerFileName }} repository: ${{ parameters.dockerImageName }} tags: ${{ parameters.imageTag }} - arguments: '--no-cache --pull' + arguments: | + --no-cache --pull ${{ parameters.dockerBuildArgs }} - script: | - printenv > "${{ parameters.envFileName }}" - + # Determine JIRA card number and filters JIRA_CARD_NUMBER="" IS_REGRESSION="${{ parameters.isRegression }}" - echo "isRegression: $IS_REGRESSION"; + echo "isRegression: $IS_REGRESSION" + if [[ "${{ parameters.isRegression }}" == "False" ]]; then - branch_name=$(System.PullRequest.SourceBranch) + branch_name="$(System.PullRequest.SourceBranch)" echo "Retrieved branch name: '$branch_name'" JIRA_CARD_NUMBER="${branch_name##*/}" echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'" fi + + # Build test filter FILTER="" if [[ -n "${{ parameters.filterTags }}" && -n "$JIRA_CARD_NUMBER" ]]; then echo "Filtering by filterTags and Jira Card Number..." @@ -140,57 +163,213 @@ jobs: fi echo "Filter value: $FILTER" - TARGET_URL=${{ parameters.targetUrl }} - echo "BASE_URL value: $BASE_URL" + TARGET_URL="${{ parameters.targetUrl }}" + echo "Target URL: $TARGET_URL" + echo "Application Type: ${{ parameters.applicationType }}" - # Create container and run tests + # Set common variables + PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}" + SERVE_PATH="${{ parameters.npmServePath }}" + SHARD_NUMBER=$(shardNumber) + TOTAL_SHARDS=${{ parameters.totalShards }} + APPLICATION_TYPE="${{ parameters.applicationType }}" + ENV_FILE="${{ parameters.envFileName }}" + # Handle localhost URLs if [[ "$TARGET_URL" == *localhost* ]]; then - echo 'Npx version:' - npx --version - port="$TARGET_URL" - echo "URL: '$port'" - echo "URL '$port' contains 'localhost'. Extracting port... " - port=$(echo "$port" | sed -E 's/.*:([0-9]+).*/\1/') - echo "Port is '$port'" - TARGET_URL="$TARGET_URL" - JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER" - FILTER="$FILTER" - PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}" - SERVE_PATH="${{ parameters.npmServePath }}" - SHARD_NUMBER=$(shardNumber) - TOTAL_SHARDS=${{ parameters.totalShards }} + echo "Localhost URL detected, extracting port..." + port=$(echo "$TARGET_URL" | sed -E 's/.*:([0-9]+).*/\1/') + echo "Port extracted: $port" + + # Create readiness check URL based on application type + if [[ "$APPLICATION_TYPE" == "dotnet" ]]; then + # For .NET APIs, append the readiness endpoint to the target URL + READINESS_URL="${TARGET_URL}${{ parameters.readinessEndpoint }}" + echo "Using .NET readiness check URL: $READINESS_URL" + else + # For Vue apps, use the target URL as-is + READINESS_URL="$TARGET_URL" + echo "Using Vue app URL for readiness check: $READINESS_URL" + fi + + echo "Starting tests for shard $SHARD_NUMBER of $TOTAL_SHARDS..." + container_id=$(docker create \ --ipc=host \ --env CI=true \ + --env TARGET_URL="$TARGET_URL" \ + --env READINESS_URL="$READINESS_URL" \ + --env PLAYWRIGHT_PATH="$PLAYWRIGHT_PATH" \ + --env SERVE_PATH="$SERVE_PATH" \ + --env SHARD_NUMBER="$SHARD_NUMBER" \ + --env TOTAL_SHARDS="$TOTAL_SHARDS" \ + --env FILTER="$FILTER" \ + --env APPLICATION_TYPE="$APPLICATION_TYPE" \ + --env SERVER_TIMEOUT="${{ parameters.serverTimeoutSeconds }}" \ + --env ASPNETCORE_URLS="https://localhost:$port" \ + --env ENV_FILE="$ENV_FILE" \ --env-file "${{ parameters.envFileName }}" \ ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \ - npx concurrently -k -n 'server,playwright' \ - "npm --prefix $SERVE_PATH run serve -- --port=$port" \ - "npx --prefix $PLAYWRIGHT_PATH wait-on $TARGET_URL && PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx --prefix $PLAYWRIGHT_PATH playwright test $PLAYWRIGHT_PATH --config=$PLAYWRIGHT_PATH/playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob $FILTER" \ + bash -c ' + echo "=== Starting Application Server ===" + echo "Application Type: $APPLICATION_TYPE" + echo "Target URL: $TARGET_URL" + echo "Readiness Check URL: $READINESS_URL" + echo "Server Timeout: $SERVER_TIMEOUT seconds" + + # Function to wait for server readiness + wait_for_server() { + local url=$1 + local max_attempts=$SERVER_TIMEOUT + local attempt=1 + + echo "Waiting for server at: $url" + + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt/$max_attempts: Checking server health..." + + if [[ "$APPLICATION_TYPE" == "vue" ]]; then + # For Vue apps, use a simple HTTP check + if curl -s -f "$url" --max-time 10 > /dev/null 2>&1; then + echo "Vue server is ready!" + return 0 + fi + else + # For .NET APIs, use healthcheck endpoint + if curl -k -s -f "$url" --max-time 10 > /dev/null 2>&1; then + echo ".NET server is ready!" + return 0 + fi + fi + + if [ $attempt -eq $max_attempts ]; then + echo "Server failed to become ready after $max_attempts attempts" + return 1 + fi + + echo "Server not ready yet, waiting 2 seconds..." + sleep 2 + attempt=$((attempt + 1)) + done + } + + # Install curl if not available + if ! command -v curl &> /dev/null; then + echo "Installing curl..." + apk add --no-cache curl 2>/dev/null || apt-get update && apt-get install -y curl 2>/dev/null || true + fi + + # Start server and run tests based on application type + if [[ "$APPLICATION_TYPE" == "vue" ]]; then + echo "=== Using Vue Mode with wait-on and concurrently ===" + + # Extract port for Vue server + port=$(echo "$TARGET_URL" | sed -E "s/.*:([0-9]+).*/\1/") + echo "Starting Vue server on port: $port" + + # Create blob report directory + mkdir -p /app/blob-report + + # Use concurrently for Vue apps (original approach) + if [ -n "$FILTER" ]; then + echo "Running Vue tests with filter: $FILTER" + npx concurrently -k -n "server,playwright" \ + "npm --prefix $SERVE_PATH run serve -- --port=$port" \ + "npx --prefix $PLAYWRIGHT_PATH wait-on $TARGET_URL && cd $PLAYWRIGHT_PATH && PLAYWRIGHT_BLOB_OUTPUT_DIR=\"/app/blob-report\" npx dotenv-cli -e \"../$ENV_FILE\" -- playwright test --config=./playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob $FILTER" + else + echo "Running all Vue tests" + npx concurrently -k -n "server,playwright" \ + "npm --prefix $SERVE_PATH run serve -- --port=$port" \ + "npx --prefix $PLAYWRIGHT_PATH wait-on $TARGET_URL && cd $PLAYWRIGHT_PATH && PLAYWRIGHT_BLOB_OUTPUT_DIR=\"/app/blob-report\" npx dotenv-cli -e \"../$ENV_FILE\" -- playwright test --config=./playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob" + fi + + else + echo "=== Using .NET Mode with healthcheck ===" + echo "Starting .NET server..." + echo "ASPNETCORE_URLS: $ASPNETCORE_URLS" + + npm --prefix "$SERVE_PATH" run serve & + SERVER_PID=$! + echo ".NET server started with PID: $SERVER_PID" + + # Wait for .NET server readiness check + if ! wait_for_server "$READINESS_URL"; then + kill $SERVER_PID 2>/dev/null || true + exit 1 + fi + + echo "=== Starting Playwright Tests ===" + + # Create blob report directory + mkdir -p /app/blob-report + cd "$PLAYWRIGHT_PATH" + + # Run Playwright tests + if [ -n "$FILTER" ]; then + echo "Running .NET tests with filter: $FILTER" + PLAYWRIGHT_BLOB_OUTPUT_DIR="/app/blob-report" npx dotenv-cli -e "../$ENV_FILE" -- playwright test \ + --config="./playwright.config.ts" \ + --shard="$SHARD_NUMBER/$TOTAL_SHARDS" \ + --reporter=list,blob \ + $FILTER || true + else + echo "Running all .NET tests" + PLAYWRIGHT_BLOB_OUTPUT_DIR="/app/blob-report" npx dotenv-cli -e "../$ENV_FILE" -- playwright test \ + --config="./playwright.config.ts" \ + --shard="$SHARD_NUMBER/$TOTAL_SHARDS" \ + --reporter=list,blob || true + fi + + TEST_EXIT_CODE=$? + echo "Tests completed with exit code: $TEST_EXIT_CODE" + + # Clean up server + echo "Stopping server..." + kill $SERVER_PID 2>/dev/null || true + + exit $TEST_EXIT_CODE + fi + ' ) else - export TARGET_URL="$TARGET_URL" - export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER" - export FILTER="$FILTER" - export PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}" - export SERVE_PATH="${{ parameters.npmServePath }}" - export SHARD_NUMBER=$(shardNumber) - export TOTAL_SHARDS=${{ parameters.totalShards }} - export ENV_FILE="${{ parameters.envFileName }}" + # Remote URL testing + echo "Remote URL testing mode" container_id=$(docker create \ --ipc=host \ --env CI=true \ + --env TARGET_URL="$TARGET_URL" \ + --env PLAYWRIGHT_PATH="$PLAYWRIGHT_PATH" \ + --env SHARD_NUMBER="$SHARD_NUMBER" \ + --env TOTAL_SHARDS="$TOTAL_SHARDS" \ + --env FILTER="$FILTER" \ + --env ENV_FILE="$ENV_FILE" \ --env-file "${{ parameters.envFileName }}" \ ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \ bash -c " - set -a - source $ENV_FILE - set +a - PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx --prefix $PLAYWRIGHT_PATH playwright test $PLAYWRIGHT_PATH --config=$PLAYWRIGHT_PATH/playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob $FILTER - " \ + echo '=== Running Tests Against Remote URL ===' + echo 'Target URL: $TARGET_URL' + + mkdir -p /app/blob-report + cd \$PLAYWRIGHT_PATH + + if [ -n \"\$FILTER\" ]; then + echo 'Running tests with filter: \$FILTER' + PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx dotenv-cli -e \"../\$ENV_FILE\" -- playwright test \ + --config=./playwright.config.ts \ + --shard=\$SHARD_NUMBER/\$TOTAL_SHARDS \ + --reporter=list,blob \ + \$FILTER || true + else + echo 'Running all tests' + PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx dotenv-cli -e \"../\$ENV_FILE\" -- playwright test \ + --config=./playwright.config.ts \ + --shard=\$SHARD_NUMBER/\$TOTAL_SHARDS \ + --reporter=list,blob || true + fi + " ) fi + # Start container and stream logs echo "Starting tests for shard $(shardNumber)..." docker start -a $container_id @@ -206,12 +385,6 @@ jobs: # Remove container echo "Cleaning up container..." docker rm $container_id - - # Check if tests failed - if [ $? -ne 0 ]; then - echo "Tests failed in shard $(shardNumber) or tests don't exist for this shard number!" - exit 0 # Suppress error. It will be visible in report. - fi displayName: 'Run Playwright Tests - Shard $(shardNumber)' env: ${{ parameters.secrets }} @@ -227,6 +400,7 @@ jobs: docker rmi ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} -f displayName: 'Cleanup Docker Image' condition: always() + - job: download_and_merge_reports dependsOn: playwright_tests timeoutInMinutes: 8 @@ -238,6 +412,10 @@ jobs: - task: DownloadPipelineArtifact@2 inputs: targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports' + - bash: | + printenv > "${{ parameters.envFileName }}" + env: ${{ parameters.secrets }} + displayName: "Make Azure Pipeline Variables Available to Docker" - task: Docker@2 displayName: 'Build Docker Image' inputs: @@ -245,9 +423,9 @@ jobs: dockerfile: ${{ parameters.dockerFileName }} repository: ${{ parameters.dockerImageName }} tags: ${{ parameters.imageTag }} - arguments: '--no-cache --pull' + arguments: | + --no-cache --pull ${{ parameters.dockerBuildArgs }} - bash: | - printenv > "${{ parameters.envFileName }}" branch_name=$(System.PullRequest.SourceBranch) echo "Retrieved branch name: '$branch_name'" JIRA_CARD_NUMBER="${branch_name##*/}" @@ -263,14 +441,12 @@ jobs: --env-file "${{ parameters.envFileName }}" \ ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \ bash -c " - set -a - source $ENV_FILE - set +a echo 'Moving Playwright reports out of subfolders...' find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \; echo 'Value of JIRA_CARD_NUMBER in bash -c command: $JIRA_CARD_NUMBER' echo 'Merging reports...' - JIRA_CARD_NUMBER='$JIRA_CARD_NUMBER' PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx --prefix $PLAYWRIGHT_PATH playwright merge-reports --config=$PLAYWRIGHT_PATH/playwright.config.ts ./playwright-reports + cd $PLAYWRIGHT_PATH + JIRA_CARD_NUMBER='$JIRA_CARD_NUMBER' PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx dotenv-cli -e ../$ENV_FILE -- playwright merge-reports --config=./playwright.config.ts ../playwright-reports " ) From d42371683c62dfa0fdbbd406a780ca0e2e926818 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Tue, 29 Jul 2025 12:57:36 -0400 Subject: [PATCH 02/83] Adds CASH-848 tag to relevant tests Adds the '@CASH-848' tag to several Cash and Insurance related E2E tests. This tag likely relates to a specific issue or requirement tracked under CASH-848. --- playwright-tests/tests/CashRepairMobileCreditCard.ts | 2 +- .../tests/CashReplaceGlassAddressLookupInshopAfterPay.ts | 2 +- playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts | 2 +- playwright-tests/tests/CashReplaceSplitWindshield.ts | 2 +- playwright-tests/tests/InsuranceAcuityPaypal.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/playwright-tests/tests/CashRepairMobileCreditCard.ts b/playwright-tests/tests/CashRepairMobileCreditCard.ts index b776f7fa5..8e38129e8 100644 --- a/playwright-tests/tests/CashRepairMobileCreditCard.ts +++ b/playwright-tests/tests/CashRepairMobileCreditCard.ts @@ -55,7 +55,7 @@ const cashRepairMobileCCTests: ITestCase[] = []; const tc = { name: `CashRepairMobileCreditCard`, - tags: ['@E2E','@CashRepairMobileCreditCard', '@test_report', '@CASH'], + tags: ['@E2E','@CashRepairMobileCreditCard', '@test_report', '@CASH', '@CASH-848'], testData: cashRepairMobileCCData }; cashRepairMobileCCTests.push(tc); diff --git a/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts index cfc273c1b..2cdb254eb 100644 --- a/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts +++ b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts @@ -58,7 +58,7 @@ const cashReplaceGlassAddressLookupInshopAfterPayTests: ITestCase[] = []; const tc = { name: `CashReplaceGlassAddressLookupInshopAfterPay`, - tags: ['@E2E','@CashReplaceGlassAddressLookupInshopAfterPay', '@test_report', '@CASH'], + tags: ['@E2E','@CashReplaceGlassAddressLookupInshopAfterPay', '@test_report', '@CASH', '@CASH-848'], testData: cashReplaceGlassAddressLookupInshopAfterPayData }; cashReplaceGlassAddressLookupInshopAfterPayTests.push(tc); diff --git a/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts b/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts index 7237bb530..8151f062f 100644 --- a/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts +++ b/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts @@ -101,7 +101,7 @@ const cashReplaceMultiGlassPromoInshopTests: ITestCase[] = []; const tc = { name: `CashReplaceMultiGlassPromoInshop`, - tags: ['@E2E','@CashReplaceMultiGlassPromoInshop', '@test_report', '@CASH'], + tags: ['@E2E','@CashReplaceMultiGlassPromoInshop', '@test_report', '@CASH', '@CASH-848'], testData: cashReplaceMultiGlassPromoInshopData }; cashReplaceMultiGlassPromoInshopTests.push(tc); diff --git a/playwright-tests/tests/CashReplaceSplitWindshield.ts b/playwright-tests/tests/CashReplaceSplitWindshield.ts index d1f12fef4..4a8d72509 100644 --- a/playwright-tests/tests/CashReplaceSplitWindshield.ts +++ b/playwright-tests/tests/CashReplaceSplitWindshield.ts @@ -66,7 +66,7 @@ const CashReplaceSplitWindshieldTests: ITestCase[] = []; const tc = { name: `CashReplaceSplitWindshield`, - tags: ['@E2E','@CashReplaceSplitWindshield', '@test_report', '@CASH'], + tags: ['@E2E','@CashReplaceSplitWindshield', '@test_report', '@CASH', '@CASH-848'], testData: CashReplaceSplitWindshieldData }; CashReplaceSplitWindshieldTests.push(tc); diff --git a/playwright-tests/tests/InsuranceAcuityPaypal.ts b/playwright-tests/tests/InsuranceAcuityPaypal.ts index 0a97fc794..5cd3c0a40 100644 --- a/playwright-tests/tests/InsuranceAcuityPaypal.ts +++ b/playwright-tests/tests/InsuranceAcuityPaypal.ts @@ -90,7 +90,7 @@ const insuranceAcuityPaypalTests: ITestCase[] = []; const tc = { name: `InsuranceAcuityPaypal`, - tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187'], + tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187', '@CASH-848'], testData: insuranceAcuityPaypalData }; insuranceAcuityPaypalTests.push(tc); From 4484fdfe4f08b2610da952dda7638ab600118bf8 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 31 Jul 2025 07:38:25 -0400 Subject: [PATCH 03/83] Able to move time-slot-question out of date-picker still lots of work to do comments are in commented out timeslotquestion component in date-picker for help seeing what I did --- .../date-picker/date-picker.vue | 57 ++++--------------- src/layouts/schedule/schedule.vue | 25 ++++++-- .../time-slot-question/time-slot-question.vue | 4 ++ 3 files changed, 35 insertions(+), 51 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 72bdcb093..5f508f5be 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -140,9 +140,9 @@ @click="showAnotherMonth"> View more dates - + :selectedDate="selectedDate" // used in date-picker + :appointmentType="appointmentType" // used in date-picker for duration + :premiumAppointmentFee="premiumAppointmentFee" // Removed from date-picker + :displayWaitList="displayWaitList" // Removed from date-picker + :timeSlotsForSelectedDate="timeSlotsForSelectedDate" // Removed from date-picker (but it had a null check for is-same-day) + :estimatedServiceMinutesMinimum="estimatedServiceMinutesMinimum" // used in date-picker for duration + :estimatedServiceMinutesMaximum="estimatedServiceMinutesMaximum" // used in date-picker for duration + @waitListRequested="handleWaitListRequested" // removed, it's a pass-through /> --> @@ -206,7 +205,6 @@ export default { months: null, disableViewMoreDatesButton: false, hideSomeDaysForInitialView: null, - selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(), selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based) selectableDatesMobile: [], durationTextBlockCopyForInshopOrDropoff: null, @@ -243,12 +241,9 @@ export default { pricingByDayBasePrice: Number, pricingByDayUpcharge: Number, isPricingByDayExperiment: Boolean, - timeSlotsForSelectedDate: Object, appointmentType: String, - premiumAppointmentFee: Object, estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMaximum: Number, - displayWaitList: Boolean, isMobileSelected: Boolean, }, setup(props) { @@ -373,9 +368,6 @@ export default { return null; }, isSameDay() { - if (!this.timeSlotsForSelectedDate) { - return false; - } const todaysDate = new Date().toISOString().split("T")[0]; return this.selectedDate === todaysDate; }, @@ -383,7 +375,6 @@ export default { methods: { async initializeComponent(initialData) { await this.setCalendarData(initialData); - this.$refs.timeSlotModalQuestion.initializeComponent(); }, fireDateSelectedEvent(event, date) { // Ignore if arrow key selected radioButton @@ -950,30 +941,9 @@ export default { }; window.requestAnimationFrame(step); }, - getSelectedTimeSlotInfo() { - const supportingItems = this.getSupportingItems(); - - var isPremiumAppointment = false; - if (supportingItems) { - isPremiumAppointment = - !!supportingItems.filter( - (lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE - ).length > 0; - } - - const selectedTimeSlotInfo = { - timeSlot: store.getters.order.schedule, - isPremiumAppointment: isPremiumAppointment, - }; - - return selectedTimeSlotInfo; - }, getSupportingItems() { return store.getters.lineItems.supportingItems; }, - handleWaitListRequested(value) { - this.$emit("waitListRequested", value); - }, getDurationTextBlockCopyForInshopOrDropoff(selectedRouteCode) { if (selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) { return this.overnightDropoffDurationText; @@ -999,17 +969,10 @@ export default { this.scrollToElement("date-of-month-error"); } }, - selectedTimeSlotInfo(newValue) { - const routeCode = newValue?.timeSlot?.routeCode; - this.durationTextBlockCopyForInshopOrDropoff = - this.getDurationTextBlockCopyForInshopOrDropoff(routeCode); - this.$emit("TimeSlotSelected", newValue); // needed to update footer button text on Schedule page and to save to Store correctly - }, }, components: { loader, ErrorMessage, - timeSlotQuestion, textBlock, funnelSubHeader, }, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 0e68cb458..24635a1f5 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -133,13 +133,28 @@ :pricingByDayUpcharge="pricingByDayUpcharge" :showPricingByDay="showPricingByDay" :isPricingByDayExperiment="isPricingByDayExperiment" - :timeSlotsForSelectedDate="timeSlotsForSelectedDate" :appointmentType="appointmentType" - :premiumAppointmentFee="mobilePremiumAppointmentFee" :estimatedServiceMinutesMinimum="getServiceMinutesMin" - :estimatedServiceMinutesMaximum="getServiceMinutesMax" + :estimatedServiceMinutesMaximum="getServiceMinutesMax" /> + Date: Thu, 31 Jul 2025 08:09:52 -0400 Subject: [PATCH 04/83] Moved duration into schedule --- .../date-picker/date-picker.vue | 103 ------------------ src/layouts/schedule/schedule.vue | 91 ++++++++++++++++ 2 files changed, 91 insertions(+), 103 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 5f508f5be..dd3f7862a 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -2,17 +2,6 @@
-
- - -
-
Select a day and time
diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 24635a1f5..67056c67d 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -116,6 +116,16 @@
+
+ + +
{ @@ -835,9 +847,88 @@ export default { ); } }, + durationTextBlockCopy() { + console.log("durationTextBlockCopy", this.appointmentType, this.getServiceMinutesMax, this.getServiceMinutesMin); + if (this.appointmentType === AppointmentTypeStrings.MOBILE) { + return this.mobileDurationText; + } else if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { + return this.inshopDurationText; + } else if ( + this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF || + this.appointmentType === AppointmentTypeStrings.DROP_OFF + ) { + return this.getDurationTextBlockCopyForInshopOrDropoff(this.selectedTimeSlotInfo.timeSlot.routeCode); + } + return null; + }, + isSameDay() { + const todaysDate = new Date().toISOString().split("T")[0]; + return this.selectedDate === todaysDate; + }, + dropOffDurationText() { + return this.getCmsContent("DropOffTimeSlotModal", cmsWidgetFieldMappings.DURATION); + }, + sameDayDropoffDurationText() { + return this.getCmsContent( + "SameDayDropOffTimeSlotModal", + cmsWidgetFieldMappings.DURATION + ); + }, + overnightDropoffDurationText() { + return this.getCmsContent( + "OvernightDropOffTimeSlotModal", + cmsWidgetFieldMappings.DURATION + ); + }, + inshopDurationText() { + const inshopDurationTextWithoutTime = this.getCmsContent( + "TimeSlotModalQuestion", + cmsWidgetFieldMappings.DURATION + ); + + const inshopDurationTime = getDisplayTextForDurationLength( + this.getServiceMinutesMin, + this.getServiceMinutesMax + ); + + if (this.getServiceMinutesMin && this.getServiceMinutesMax) { + return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`; + } + + return null; + }, + mobileDurationText() { + const mobileDurationTextWithoutTime = this.getCmsContent( + "TimeSlotModalQuestion", + cmsWidgetFieldMappings.DURATION + ); + + const mobileDurationTime = getDisplayTextForDurationLength( + this.getServiceMinutesMin, + this.getServiceMinutesMax + ); + + if (this.getServiceMinutesMin && this.getServiceMinutesMax) { + return `${mobileDurationTextWithoutTime} ${mobileDurationTime}`; + } + + return null; + }, }, methods: { splitCopyOnCMSPlaceHolder, + getDurationTextBlockCopyForInshopOrDropoff(selectedRouteCode) { + if (selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) { + return this.overnightDropoffDurationText; + } else if (selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) { + if (this.isSameDay) { + return this.sameDayDropoffDurationText; + } else { + return this.dropOffDurationText; + } + } + return this.inshopDurationText; + }, arePagePrerequisitesValid() { const paymentInfo = store.getters.payment.isInsurance !== null; const damageInfo = From 866eaf0e7b1f281a4356100b877de088309be65b Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 31 Jul 2025 09:48:43 -0400 Subject: [PATCH 05/83] Move duration outside of date-picker into own component --- .../date-picker/date-picker.vue | 3 - .../duration-text-block.vue | 126 ++++++++++++++++++ src/layouts/schedule/schedule.vue | 106 +++------------ 3 files changed, 143 insertions(+), 92 deletions(-) create mode 100644 src/layouts/schedule/duration-text-block/duration-text-block.vue diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index dd3f7862a..1e9d88d48 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -218,9 +218,6 @@ export default { pricingByDayBasePrice: Number, pricingByDayUpcharge: Number, isPricingByDayExperiment: Boolean, - appointmentType: String, - estimatedServiceMinutesMinimum: Number, - estimatedServiceMinutesMaximum: Number, isMobileSelected: Boolean, }, setup(props) { diff --git a/src/layouts/schedule/duration-text-block/duration-text-block.vue b/src/layouts/schedule/duration-text-block/duration-text-block.vue new file mode 100644 index 000000000..957b0cfb0 --- /dev/null +++ b/src/layouts/schedule/duration-text-block/duration-text-block.vue @@ -0,0 +1,126 @@ + + + + + \ No newline at end of file diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 67056c67d..5128d4906 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -118,13 +118,14 @@
- + +
+ :isPricingByDayExperiment="isPricingByDayExperiment" /> { @@ -822,12 +819,12 @@ export default { // Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText); }, - getServiceMinutesMin() { + estimatedServiceMinutesMinimum() { return this.isMobileSelected ? this.selectableDatesMobile.estimatedServiceMinutesMinimum : this.selectableDatesInshop.estimatedServiceMinutesMinimum; }, - getServiceMinutesMax() { + estimatedServiceMinutesMaximum() { return this.isMobileSelected ? this.selectableDatesMobile.estimatedServiceMinutesMaximum : this.selectableDatesInshop.estimatedServiceMinutesMaximum; @@ -847,88 +844,18 @@ export default { ); } }, - durationTextBlockCopy() { - console.log("durationTextBlockCopy", this.appointmentType, this.getServiceMinutesMax, this.getServiceMinutesMin); - if (this.appointmentType === AppointmentTypeStrings.MOBILE) { - return this.mobileDurationText; - } else if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { - return this.inshopDurationText; - } else if ( - this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF || - this.appointmentType === AppointmentTypeStrings.DROP_OFF - ) { - return this.getDurationTextBlockCopyForInshopOrDropoff(this.selectedTimeSlotInfo.timeSlot.routeCode); - } - return null; - }, isSameDay() { const todaysDate = new Date().toISOString().split("T")[0]; return this.selectedDate === todaysDate; }, - dropOffDurationText() { - return this.getCmsContent("DropOffTimeSlotModal", cmsWidgetFieldMappings.DURATION); - }, - sameDayDropoffDurationText() { - return this.getCmsContent( - "SameDayDropOffTimeSlotModal", - cmsWidgetFieldMappings.DURATION + isOvernightDropoff() { + return ( + this.selectedTimeSlotInfo?.timeSlot?.routeCode && this.selectedTimeSlotInfo.timeSlot.routeCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF) ); - }, - overnightDropoffDurationText() { - return this.getCmsContent( - "OvernightDropOffTimeSlotModal", - cmsWidgetFieldMappings.DURATION - ); - }, - inshopDurationText() { - const inshopDurationTextWithoutTime = this.getCmsContent( - "TimeSlotModalQuestion", - cmsWidgetFieldMappings.DURATION - ); - - const inshopDurationTime = getDisplayTextForDurationLength( - this.getServiceMinutesMin, - this.getServiceMinutesMax - ); - - if (this.getServiceMinutesMin && this.getServiceMinutesMax) { - return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`; - } - - return null; - }, - mobileDurationText() { - const mobileDurationTextWithoutTime = this.getCmsContent( - "TimeSlotModalQuestion", - cmsWidgetFieldMappings.DURATION - ); - - const mobileDurationTime = getDisplayTextForDurationLength( - this.getServiceMinutesMin, - this.getServiceMinutesMax - ); - - if (this.getServiceMinutesMin && this.getServiceMinutesMax) { - return `${mobileDurationTextWithoutTime} ${mobileDurationTime}`; - } - - return null; - }, + } }, methods: { splitCopyOnCMSPlaceHolder, - getDurationTextBlockCopyForInshopOrDropoff(selectedRouteCode) { - if (selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) { - return this.overnightDropoffDurationText; - } else if (selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) { - if (this.isSameDay) { - return this.sameDayDropoffDurationText; - } else { - return this.dropOffDurationText; - } - } - return this.inshopDurationText; - }, arePagePrerequisitesValid() { const paymentInfo = store.getters.payment.isInsurance !== null; const damageInfo = @@ -1851,6 +1778,7 @@ export default { contentGroupModal, shopQuestionPopup, buttonQuestion, + durationTextBlock }, }; From 6af4bca738fcf74e6acc9c00c7a288b810eb50a4 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 31 Jul 2025 11:15:35 -0400 Subject: [PATCH 06/83] Time-slot refactor - move logic into parent --- .../duration-text-block.vue | 8 +-- src/layouts/schedule/schedule.vue | 38 ++++++++++-- .../time-slot-question/time-slot-question.vue | 61 +++---------------- 3 files changed, 43 insertions(+), 64 deletions(-) diff --git a/src/layouts/schedule/duration-text-block/duration-text-block.vue b/src/layouts/schedule/duration-text-block/duration-text-block.vue index 957b0cfb0..2ac1c3938 100644 --- a/src/layouts/schedule/duration-text-block/duration-text-block.vue +++ b/src/layouts/schedule/duration-text-block/duration-text-block.vue @@ -1,6 +1,5 @@ @@ -119,9 +145,8 @@ export default { customerReview, damageReview, scheduleReview, - serviceLocationReview, - servicePackageReview, vehicleReview, + serviceLocationReview, }, }; diff --git a/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.vue b/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.vue index 2fd82c716..0aa384682 100644 --- a/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.vue +++ b/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.vue @@ -21,7 +21,8 @@ export default { }, computed: { displayContent() { - return [this.fullName, this.email, this.phoneNumber, this.smsOptIn]; + return [this.email, this.phoneNumber, this.smsOptIn]; + // return [this.fullName, this.email, this.phoneNumber, this.smsOptIn]; // prior to Heritage parity }, header() { return this.getCmsContent(this.cmsWidgetName, "HeaderText"); From fa65a56bf06d78e44d673b74abe606cf31d99b94 Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Tue, 2 Sep 2025 08:12:02 -0400 Subject: [PATCH 51/83] CASH-1450: amend unit test --- .../review-sections/customer-review/customer-review.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.spec.js b/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.spec.js index 13138a182..08dac9e5e 100644 --- a/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.spec.js +++ b/src/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.spec.js @@ -82,7 +82,6 @@ describe("Customer Review Block", () => { // Assert expect(wrapper.vm.displayContent).toEqual([ - testConstants.displayContent.fullName, testConstants.displayContent.emailAddress, testConstants.displayContent.phoneNumber, testConstants.displayContent.smsOptIn, From 5d156b8ea5dcbdd1630fd23a26461e08e4447f51 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Tue, 2 Sep 2025 09:14:17 -0400 Subject: [PATCH 52/83] CASH-1465 | Recycle widget name missing on confirmation --- src/layouts/confirmation/confirmation.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index b497125a1..b4da6c7e4 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -61,6 +61,7 @@ :donationCartItem="donationLineItem" :showAsPaid="isPia" servicePackageOptionsCmsName="ServicePackageTitle" + recyclingModalCmsWidgetName="RecycleModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" From 2cac6c5cdf77af5f412db8ce878e672bad66968a Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Tue, 2 Sep 2025 09:31:48 -0400 Subject: [PATCH 53/83] CASH-1448: style updates --- src/layouts/payment-method/payment-method.vue | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 78bbd2847..00a06fd70 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -933,15 +933,22 @@ export default { :deep(.modal-body) { display: flex; flex-direction: column; + overflow: visible !important; + + p.subheader-text { + color: $red; + order: 1 !important; + margin-top: -2.25rem; + } h5 { - text-align: center; - order: 1; + text-align: left !important; + order: 2 !important; } p { - order: 3; + order: 4; } img { - order: 2; + order: 3 !important; } } } From 3b065579e19b157bee181df1d9cd3786e7f3ad74 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:28:51 -0400 Subject: [PATCH 54/83] fix for playwright tests --- playwright-tests/pages/AfterpayPage.ts | 11 +++++- playwright-tests/pages/PaymentMethodPage.ts | 10 ++--- playwright-tests/pages/PaypalPage.ts | 7 ++-- playwright-tests/pages/SchedulePage.ts | 38 ++++++++----------- playwright-tests/pages/VehicleDamagePage.ts | 6 +-- ...ReplaceGlassAddressLookupInshopAfterPay.ts | 8 ---- .../CashReplaceMultiSlidingGlassDropoff.ts | 2 +- .../tests/CashReplaceWiperDropoff.ts | 4 +- 8 files changed, 41 insertions(+), 45 deletions(-) diff --git a/playwright-tests/pages/AfterpayPage.ts b/playwright-tests/pages/AfterpayPage.ts index 7f54fe336..75f0ca352 100644 --- a/playwright-tests/pages/AfterpayPage.ts +++ b/playwright-tests/pages/AfterpayPage.ts @@ -10,6 +10,8 @@ export class AfterpayPage extends BasePage { readonly passwordTextBox: Locator; // Card details + readonly paymentOptionsButton: Locator; + readonly continueButtonAfterpay: Locator; readonly cardholderNameTextBox: Locator; readonly cardNumberTextBox: Locator; readonly expirationDateTextBox: Locator; @@ -23,6 +25,8 @@ export class AfterpayPage extends BasePage { this.passwordTextBox = page.getByTestId('login-password-input'); this.submitButton = page.getByRole('button', { name: 'Continue' }); + this.paymentOptionsButton = page.locator('div:has(>input[id*=\'payment-types\']) label').nth(1); + this.continueButtonAfterpay = page.getByRole('button').filter({hasText: 'Continue'}); this.cardholderNameTextBox = page.getByTestId('payment-method-cardHolderName-input'); this.cardNumberTextBox = page.getByTestId('payment-method-cardNumber-input'); this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input'); @@ -46,7 +50,12 @@ export class AfterpayPage extends BasePage { async executeAfterpayPayment(paymentDetails: IPaymentDetails) { await this.login(paymentDetails.password!); - + await this.page.locator('div[data-testid=\'loading-icon-svg\']').filter({ visible: true}).first().waitFor({ state: 'hidden' }); + if (await this.paymentOptionsButton.isVisible()) + { + await this.paymentOptionsButton.click(); + await this.continueButtonAfterpay.click(); + } await this.confirmButton.click(); } diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 2ba68dcc3..7bb4a6fcc 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -56,7 +56,7 @@ export class PaymentMethodPage extends BasePage { this.payNowButton = this.page.locator('[buttonlabel="Pay now"]'); this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]'); this.submitButton = this.page.locator('[data-test-id="nav-bar-main-button"]'); - this.recalibrationCheckbox = this.page.getByLabel('I understand after windshield'); + this.recalibrationCheckbox = this.page.locator('label:has(>input[name=\'recalAckOptIn\'])'); // this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1); this.paymentPage = new PaymentPage(page); this.paypalPage = new PaypalPage(page); @@ -101,7 +101,7 @@ export class PaymentMethodPage extends BasePage { let expectedAppointmentDetails = new Map(); expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails); expectedAppointmentDetails = await this.getExpectedVehicleDamage(testData, expectedAppointmentDetails); - expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails); + // expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails); expectedAppointmentDetails = await this.getExpectedServiceLocation(testData, expectedAppointmentDetails); expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails); expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails); @@ -403,7 +403,7 @@ l const { appointmentDetails } = testData; let serviceLocationTitle = appointmentDetails?.serviceLocation == ServiceLocation.Mobile ? "We're coming to you" - : "You're going to a Safelite shop"; + : "You're coming to us"; let serviceLocation: string[] = []; serviceLocation.push( @@ -443,7 +443,7 @@ l customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : "" ); - expectedServicePackageDetails["Appointment date + time"] = appointmentDateText; + expectedServicePackageDetails["Appointment Date + Time"] = appointmentDateText; return expectedServicePackageDetails; } @@ -452,7 +452,7 @@ l let customerDetailsText: string[] = []; customerDetailsText.push( - customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(), + // customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(), customerDetails?.email ? customerDetails?.email.toUpperCase() : "", customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "", "Opted out of text message updates" diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts index 07e2cd1e6..ff2bbd548 100644 --- a/playwright-tests/pages/PaypalPage.ts +++ b/playwright-tests/pages/PaypalPage.ts @@ -24,8 +24,8 @@ export class PaypalPage extends BasePage { this.passwordTextBox = page.getByPlaceholder('Password'); this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true }); this.completePurchaseButton = page.getByTestId('submit-button-initial') - this.payWithRadioButton = page.locator('.py-4').first(); - this.payButton = page.getByRole('button', { name: 'Pay $' }); + this.payWithRadioButton = page.getByRole('button').filter({ hasText: 'Pay with' }); + this.payButton = page.locator('#one-time-cta'); } async completePaypalPurchase(paymentDetails: IPaymentDetails){ @@ -45,7 +45,8 @@ export class PaypalPage extends BasePage { await this.passwordTextBox.fill(paymentDetails.password!); await this.paypalLoginButton.click(); await this.payWithRadioButton.click(); - await this.payButton.click(); + await this.page.waitForTimeout(2000); // wait for 2 seconds to ensure the Pay button is clickable + await this.payButton.dblclick(); } } } diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 312e97b68..a8f01b03a 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -55,7 +55,7 @@ export class SchedulePage extends BasePage { this.selectAShopOptions = this.page.locator('[class="shop-question"]'); this.yourSafeliteShop = this.page.locator("fieldset:has(#chooseShop) label"); - this.allDayDropOffButton = this.page.locator("label[buttonlabel='Drop off all day']"); + this.allDayDropOffButton = this.page.locator("label[buttonlabel*='Drop']"); this.pickATimeButton = this.page.locator("label[buttonlabel='Pick a time']"); this.firstAvailableDate = this.page.locator('.selectable-day').filter({ visible: true}).locator('nth=0'); this.firstAvailableTime = this.page.locator('label').filter({ hasText: /AM|PM/ }).locator('div').locator('nth=0'); @@ -64,7 +64,7 @@ export class SchedulePage extends BasePage { this.dateText = this.page.locator('label.modal-title'); this.viewMoreDatesLink = this.page.getByText(/View more dates/).first(); this.appointmentDuration = this.page.locator('.duration-text-block'); - this.timeSlots = this.page.locator('fieldset:has(>legend#chooseTimeSlot) label'); + this.timeSlots = this.page.locator('fieldset:has(>legend#chooseTimeSlot) label').filter({ visible: true}); } async selectLocation(testData: Partial) { @@ -82,7 +82,8 @@ export class SchedulePage extends BasePage { } async scheduleInShop(appointmentDetails?: IAppointmentDetails) { - await this.inShopButton.click(); + + await this.inShopButton.isVisible() ? await this.inShopButton.click() : null; if (appointmentDetails && appointmentDetails.shopAddress) { const zipCodeMatch = appointmentDetails.shopAddress.match(/\b\d{5}$/); if (zipCodeMatch) { @@ -97,28 +98,20 @@ export class SchedulePage extends BasePage { await this.saveLocationButton.click(); } - await this.inShopButton.click().then(async () => { - if (appointmentDetails && (appointmentDetails.shopAddress === "" || appointmentDetails.shopAddress === undefined )) { - appointmentDetails.shopAddress = ''; - const elements = await this.storeAddressText.all(); - elements.forEach(async(element) => { - appointmentDetails.shopAddress += await element.textContent() || "" + " "; - }); - appointmentDetails.shopAddress?.trim(); - } - }); + // await this.inShopButton.click(); // await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click()); } if (appointmentDetails && (appointmentDetails.shopAddress === "" || appointmentDetails.shopAddress === undefined )) { appointmentDetails.shopAddress = ''; + await this.storeAddressText.first().waitFor({ state: 'visible' }); const elements = await this.storeAddressText.all(); - elements.forEach(async(element) => - { - appointmentDetails.shopAddress += await element.textContent() || "" + " "; - } - ) - appointmentDetails.shopAddress?.trim(); + + for (let element of elements) { + let addressLine = await element.textContent() || ""; + appointmentDetails.shopAddress += addressLine + ", " + }; + appointmentDetails.shopAddress = appointmentDetails.shopAddress?.slice(0, -2); } } @@ -170,6 +163,7 @@ export class SchedulePage extends BasePage { const inshopAvailableDates = this.page.locator('.selectable-day').filter({ visible: true}).all(); for (const inshopAvailableDate of await inshopAvailableDates) { await inshopAvailableDate.click(); + await this.page.waitForTimeout(500); if (await this.allDayDropOffButton.isVisible()) { customerDetails!.apptDate = `${await inshopAvailableDate.getAttribute("id")}`; break; @@ -210,13 +204,13 @@ export class SchedulePage extends BasePage { } // appointmentmentDetails.serviceLocation === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); - customerDetails!.apptDuration = (await this.appointmentDuration.innerText()).replace("Your service will take approximately ", ""); + customerDetails!.apptDuration = (await this.appointmentDuration.innerText()).replace("Your service will take approximately ", "").replace("Duration: ", ""); } - async getFormattedTimeSlot(timeSlot: Locator) { + async getFormattedTimeSlot(timeSlot: Locator) { const selectedTimeSlot = await timeSlot.innerText(); let formattedTimeSlot: string = ""; - if (selectedTimeSlot.toLowerCase().includes("drop off")) + if (selectedTimeSlot.toLowerCase().includes("drop")) { formattedTimeSlot = selectedTimeSlot.includes("overnight") ? "drop off by 5:30 pm on the night of your scheduled appointment. Pick-up time dependent on shop schedule" : "drop off before 9:30 AM"; } diff --git a/playwright-tests/pages/VehicleDamagePage.ts b/playwright-tests/pages/VehicleDamagePage.ts index 5de6cb1d4..465df289d 100644 --- a/playwright-tests/pages/VehicleDamagePage.ts +++ b/playwright-tests/pages/VehicleDamagePage.ts @@ -39,9 +39,9 @@ export class VehicleDamagePage extends BasePage { super(page); this.page = page; this.windshieldChkBox = this.page.locator('[buttonlabel="Windshield"]'); - this.crackButton = this.page.locator('[buttonlabel="Crack"]'); - this.chipButton = this.page.locator('[buttonlabel="Chip(s)"]'); - this.sideDoorButton = this.page.locator('[buttonlabel="Side door"]'); + this.crackButton = this.page.locator('[buttonlabelsubcopy="Replace my windshield"]'); + this.chipButton = this.page.locator('[buttonlabelsubcopy="Repair my windshield"]'); + this.sideDoorButton = this.page.locator('[buttonlabel="Side window"]'); this.driverSideButton = this.page.locator('[buttonlabel="Driver side"]'); this.passengerSideButton = this.page.locator('[buttonlabel="Passenger side"]'); this.driverQuarterPanelChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Quarter panel"]'); diff --git a/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts index 2cdb254eb..43660fe21 100644 --- a/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts +++ b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts @@ -42,14 +42,6 @@ const cashReplaceGlassAddressLookupInshopAfterPayData: Partial = { vehicleLookupType: VehicleLookupType.Address }, - // No need to override vehicleDamage as it already defaults to WindshieldCrack - - // Override appointment details - appointmentDetails: { - ...getDefaultTestData().appointmentDetails!, - shopAddress: "6826 Sawmill Rd, Columbus, OH 43235" - }, - // Override payment details paymentDetails: ClientData.getDefaultAfterpayDetails() } diff --git a/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts index 2da4c6f5e..f488a3555 100644 --- a/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts +++ b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts @@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial = { appointmentDetails: { serviceLocation: ServiceLocation.DropOff, appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate, - shopAddress: "6826 Sawmill Rd, Columbus, OH 43235" + shopAddress: "6826 sawmill rd, Columbus, OH 43235" }, // Override vehicle details diff --git a/playwright-tests/tests/CashReplaceWiperDropoff.ts b/playwright-tests/tests/CashReplaceWiperDropoff.ts index 8080603f6..09083fcd2 100644 --- a/playwright-tests/tests/CashReplaceWiperDropoff.ts +++ b/playwright-tests/tests/CashReplaceWiperDropoff.ts @@ -20,7 +20,7 @@ const cashReplaceWiperDropoffData: Partial = { servicePackage: ServicePackage.Standard, // Special flags - isSkipEstimatePage: true, + isSkipEstimatePage: false, isRecalVehicle: true, // Override customer postal code @@ -28,7 +28,7 @@ const cashReplaceWiperDropoffData: Partial = { ...getDefaultTestData().customerDetails!, address: { ...getDefaultTestData().customerDetails!.address, - postalCode: '43085' + postalCode: '43235' } }, From 5098719644fdd0876dd790a70f33723ecc6cb851 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Wed, 3 Sep 2025 13:12:07 -0400 Subject: [PATCH 55/83] CASH-1477: Modal cta button color/type --- src/digital-components/modal/modal.vue | 3 ++- .../modal-button-main/modal-button-main.vue | 18 +++++++++++------- .../save-progress-modal-question.vue | 1 + .../save-progress-popup-question.vue | 1 + src/styles/ux-variables.scss | 4 ++-- src/ux-components/button-main/button-main.vue | 14 +++++++------- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index f0d546782..89d428363 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -32,7 +32,7 @@
@@ -886,14 +866,6 @@ export default { align-items: flex-start; text-align: left; - .duration-text-block { - text-align: left; - } - - .date-picker-header { - margin-bottom: 1.5rem; - } - fieldset { flex-grow: 1; position: relative; diff --git a/src/layouts/schedule/duration-text-block/duration-text-block.vue b/src/layouts/schedule/duration-text-block/duration-text-block.vue index 48910d851..c5834f685 100644 --- a/src/layouts/schedule/duration-text-block/duration-text-block.vue +++ b/src/layouts/schedule/duration-text-block/duration-text-block.vue @@ -83,12 +83,6 @@ export default { return null; }, durationTextBlockCopy() { - console.log( - "durationTextBlockCopy", - this.appointmentType, - this.estimatedServiceMinutesMaximum, - this.estimatedServiceMinutesMinimum - ); if (this.appointmentType === AppointmentTypeStrings.MOBILE) { return this.mobileDurationText; } else if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) { diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 9214e0a14..188ea5734 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -114,7 +114,7 @@
- + Date: Tue, 16 Sep 2025 13:10:31 -0400 Subject: [PATCH 83/83] CASH-1511: Sub header margin update --- src/layouts/schedule/schedule.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 188ea5734..f7264c7e0 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -10,7 +10,9 @@
- +