Merge branch 'release/2025.09.25' into feature/CASH-DeviceID-Fix
This commit is contained in:
commit
e941181410
81 changed files with 1794 additions and 648 deletions
|
|
@ -33,6 +33,7 @@ stages:
|
||||||
# TODO: Change to ADO Playwright template
|
# TODO: Change to ADO Playwright template
|
||||||
- template: temp/playwright-test.yml
|
- template: temp/playwright-test.yml
|
||||||
parameters:
|
parameters:
|
||||||
|
applicationType: 'vue'
|
||||||
totalShards: ${{ variables.totalShards }}
|
totalShards: ${{ variables.totalShards }}
|
||||||
targetUrl: $(BASE_URL)
|
targetUrl: $(BASE_URL)
|
||||||
dockerFileName: 'Dockerfile.playwright'
|
dockerFileName: 'Dockerfile.playwright'
|
||||||
|
|
|
||||||
|
|
@ -51,19 +51,11 @@ stages:
|
||||||
displayName: Run Prettier check
|
displayName: Run Prettier check
|
||||||
|
|
||||||
- stage: TestPr
|
- stage: TestPr
|
||||||
displayName: Run Unit Tests For PullRequest
|
displayName: Run Tests For PullRequest
|
||||||
jobs:
|
|
||||||
- template: templates/digital/vue-jest-run-unit-tests.yml@AzureDevOps
|
|
||||||
parameters:
|
|
||||||
nodeContainer: node
|
|
||||||
npmLocation: $(Build.SourcesDirectory)
|
|
||||||
testResultsFile: junit.xml
|
|
||||||
summaryFileLocation: $(Build.SourcesDirectory)/coverage/cobertura-coverage.xml
|
|
||||||
- stage: TestPrPlaywright
|
|
||||||
displayName: Run Playwright Tests For PullRequest
|
|
||||||
jobs:
|
jobs:
|
||||||
- template: temp/playwright-test.yml
|
- template: temp/playwright-test.yml
|
||||||
parameters:
|
parameters:
|
||||||
|
applicationType: 'vue'
|
||||||
totalShards: 2
|
totalShards: 2
|
||||||
targetUrl: $(BASE_URL)
|
targetUrl: $(BASE_URL)
|
||||||
dockerFileName: 'Dockerfile.playwright'
|
dockerFileName: 'Dockerfile.playwright'
|
||||||
|
|
@ -75,6 +67,12 @@ stages:
|
||||||
secrets:
|
secrets:
|
||||||
CCIS_API_AUTH: $(CCIS_API_AUTH)
|
CCIS_API_AUTH: $(CCIS_API_AUTH)
|
||||||
JIRA_API_KEY: $(JIRA_API_KEY)
|
JIRA_API_KEY: $(JIRA_API_KEY)
|
||||||
|
- template: templates/digital/vue-jest-run-unit-tests.yml@AzureDevOps
|
||||||
|
parameters:
|
||||||
|
nodeContainer: node
|
||||||
|
npmLocation: $(Build.SourcesDirectory)
|
||||||
|
testResultsFile: junit.xml
|
||||||
|
summaryFileLocation: $(Build.SourcesDirectory)/coverage/cobertura-coverage.xml
|
||||||
- ${{ else }}:
|
- ${{ else }}:
|
||||||
# Dev Build/Deploy
|
# Dev Build/Deploy
|
||||||
- stage: Dev
|
- stage: Dev
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# Environment configuration
|
# Environment configuration
|
||||||
|
|
||||||
# Environment type
|
# Environment type
|
||||||
NODE_ENV="qa"
|
PLAYWRIGHT_ENV="qa"
|
||||||
|
|
||||||
# Skip content site
|
# Skip content site
|
||||||
SKIP_CONTENT_SITE=false
|
SKIP_CONTENT_SITE=false
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# Environment configuration
|
# Environment configuration
|
||||||
|
|
||||||
# Environment type
|
# Environment type
|
||||||
NODE_ENV="qa"
|
PLAYWRIGHT_ENV="qa"
|
||||||
|
|
||||||
# Skip content site
|
# Skip content site
|
||||||
SKIP_CONTENT_SITE=false
|
SKIP_CONTENT_SITE=false
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# Environment configuration
|
# Environment configuration
|
||||||
|
|
||||||
# Environment type
|
# Environment type
|
||||||
NODE_ENV="qa"
|
PLAYWRIGHT_ENV="qa"
|
||||||
|
|
||||||
# Skip content site
|
# Skip content site
|
||||||
SKIP_CONTENT_SITE=false
|
SKIP_CONTENT_SITE=false
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,6 @@ ADMIN_SERVICE_API_URL= <br>
|
||||||
SAUCE_USERNAME= <br>
|
SAUCE_USERNAME= <br>
|
||||||
SAUCE_ACCESS_KEY= <br>
|
SAUCE_ACCESS_KEY= <br>
|
||||||
|
|
||||||
|
|
||||||
## CI/CD Integration
|
## CI/CD Integration
|
||||||
The project uses Azure Pipelines for continuous integration with configurations defined in azure-pipelines.yml.
|
The project uses Azure Pipelines for continuous integration with configurations defined in azure-pipelines.yml.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,23 @@ export enum PaymentMethod {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProgressBarPercentages {
|
export enum ProgressBarPercentages {
|
||||||
VehicleSelectionPage = '4%',
|
VehicleSelectionPage = '8%',
|
||||||
VehicleDamagePage = '16%',
|
VehicleDamagePage = '11%',
|
||||||
EstimatePage = '28%',
|
EstimatePage = '18%',
|
||||||
ServiceZipPage = '32%',
|
ServiceZipPage = '22%',
|
||||||
VehicleLookupAddressPage = '32%',
|
VehicleLookupAddressPage = '22%',
|
||||||
VehicleLookupLicensePage = '32%',
|
VehicleLookupLicensePage = '22%',
|
||||||
VinLookupPage = '32%',
|
VinLookupPage = '22%',
|
||||||
PartQuestionsPage = '40%',
|
PartQuestionsPage = '30%',
|
||||||
MoldingQuestionsPage = '40%',
|
MoldingQuestionsPage = '30%',
|
||||||
CapabilityQuestionsPage = '40%',
|
CapabilityQuestionsPage = '30%',
|
||||||
VehiclePartsPage = '40%',
|
VehiclePartsPage = '30%',
|
||||||
ServicePackagePage = '48%',
|
ServicePackagePage = '62%',
|
||||||
InsuranceCompanyPage = '52%',
|
InsuranceCompanyPage = '52%',
|
||||||
ServiceLocationPage = '60%',
|
ServiceLocationPage = '76%',
|
||||||
SchedulePage = '64%',
|
SchedulePage = '76%',
|
||||||
MobileDetailsPage = '76%',
|
MobileDetailsPage = '76%',
|
||||||
ContactDetailsPage = '84%',
|
ContactDetailsPage = '84%',
|
||||||
PaymentMethodPage = '92%',
|
PaymentMethodPage = '97%',
|
||||||
OrderConfirmationPage = '100%'
|
OrderConfirmationPage = '100%'
|
||||||
}
|
}
|
||||||
8
playwright-tests/package-lock.json
generated
8
playwright-tests/package-lock.json
generated
|
|
@ -22,7 +22,7 @@
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
"ortoni-report": "^3.0.2",
|
"ortoni-report": "^3.0.2",
|
||||||
"playwright-jira-reporter": "^1.0.4",
|
"playwright-jira-reporter": "^1.0.9",
|
||||||
"safelite-playwright-core": "^1.0.26",
|
"safelite-playwright-core": "^1.0.26",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
|
|
@ -3006,9 +3006,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-jira-reporter": {
|
"node_modules/playwright-jira-reporter": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.9",
|
||||||
"resolved": "https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/playwright-jira-reporter/-/playwright-jira-reporter-1.0.4.tgz",
|
"resolved": "https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/playwright-jira-reporter/-/playwright-jira-reporter-1.0.9.tgz",
|
||||||
"integrity": "sha1-uwnLFtj3Pnq5XXMFotlfHItucAw=",
|
"integrity": "sha1-o6+EHQlQ/jY4BtWKA6gSJ5mwK6A=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
"ortoni-report": "^3.0.2",
|
"ortoni-report": "^3.0.2",
|
||||||
"playwright-jira-reporter": "^1.0.4",
|
"playwright-jira-reporter": "^1.0.9",
|
||||||
"safelite-playwright-core": "^1.0.26",
|
"safelite-playwright-core": "^1.0.26",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ export class AfterpayPage extends BasePage {
|
||||||
readonly passwordTextBox: Locator;
|
readonly passwordTextBox: Locator;
|
||||||
|
|
||||||
// Card details
|
// Card details
|
||||||
|
readonly paymentOptionsButton: Locator;
|
||||||
|
readonly continueButtonAfterpay: Locator;
|
||||||
readonly cardholderNameTextBox: Locator;
|
readonly cardholderNameTextBox: Locator;
|
||||||
readonly cardNumberTextBox: Locator;
|
readonly cardNumberTextBox: Locator;
|
||||||
readonly expirationDateTextBox: Locator;
|
readonly expirationDateTextBox: Locator;
|
||||||
|
|
@ -23,6 +25,8 @@ export class AfterpayPage extends BasePage {
|
||||||
this.passwordTextBox = page.getByTestId('login-password-input');
|
this.passwordTextBox = page.getByTestId('login-password-input');
|
||||||
this.submitButton = page.getByRole('button', { name: 'Continue' });
|
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.cardholderNameTextBox = page.getByTestId('payment-method-cardHolderName-input');
|
||||||
this.cardNumberTextBox = page.getByTestId('payment-method-cardNumber-input');
|
this.cardNumberTextBox = page.getByTestId('payment-method-cardNumber-input');
|
||||||
this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input');
|
this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input');
|
||||||
|
|
@ -46,7 +50,12 @@ export class AfterpayPage extends BasePage {
|
||||||
|
|
||||||
async executeAfterpayPayment(paymentDetails: IPaymentDetails) {
|
async executeAfterpayPayment(paymentDetails: IPaymentDetails) {
|
||||||
await this.login(paymentDetails.password!);
|
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();
|
await this.confirmButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export class BasePage {
|
||||||
this.pageSpinner = page.getByRole('status');
|
this.pageSpinner = page.getByRole('status');
|
||||||
this.buttonLoadSpin = page.getByRole('alert');
|
this.buttonLoadSpin = page.getByRole('alert');
|
||||||
this.hamburgerMenu = this.page.getByRole('button', { name: 'Hamburger Menu (modal window)' });
|
this.hamburgerMenu = this.page.getByRole('button', { name: 'Hamburger Menu (modal window)' });
|
||||||
this.progressBar = this.page.locator('.progress-bar-inner');
|
this.progressBar = this.page.locator('.progress-bar-outer.progress-bar-inner');
|
||||||
}
|
}
|
||||||
|
|
||||||
async nextPage() {
|
async nextPage() {
|
||||||
|
|
@ -104,7 +104,7 @@ export class BasePage {
|
||||||
|
|
||||||
async mockScheduleResponseForEarlyBird(customerDetails: ICustomerDetails) {
|
async mockScheduleResponseForEarlyBird(customerDetails: ICustomerDetails) {
|
||||||
|
|
||||||
const apiUrl = `https://digitalapi.${process.env['NODE_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/mobile-time-slots`;
|
const apiUrl = `https://digitalapi.${process.env['PLAYWRIGHT_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/mobile-time-slots`;
|
||||||
await this.page.route(apiUrl, async (route) => {
|
await this.page.route(apiUrl, async (route) => {
|
||||||
const currentDate = new Date().toISOString().split('T')[0]; // e.g., "2025-07-23"
|
const currentDate = new Date().toISOString().split('T')[0]; // e.g., "2025-07-23"
|
||||||
if(route.request().postDataJSON().startDate === currentDate) {
|
if(route.request().postDataJSON().startDate === currentDate) {
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export class PaymentMethodPage extends BasePage {
|
||||||
this.payNowButton = this.page.locator('[buttonlabel="Pay now"]');
|
this.payNowButton = this.page.locator('[buttonlabel="Pay now"]');
|
||||||
this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]');
|
this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]');
|
||||||
this.submitButton = this.page.locator('[data-test-id="nav-bar-main-button"]');
|
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.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1);
|
||||||
this.paymentPage = new PaymentPage(page);
|
this.paymentPage = new PaymentPage(page);
|
||||||
this.paypalPage = new PaypalPage(page);
|
this.paypalPage = new PaypalPage(page);
|
||||||
|
|
@ -101,7 +101,7 @@ export class PaymentMethodPage extends BasePage {
|
||||||
let expectedAppointmentDetails = new Map<string, string[]>();
|
let expectedAppointmentDetails = new Map<string, string[]>();
|
||||||
expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails);
|
expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails);
|
||||||
expectedAppointmentDetails = await this.getExpectedVehicleDamage(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.getExpectedServiceLocation(testData, expectedAppointmentDetails);
|
||||||
expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails);
|
expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails);
|
||||||
expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails);
|
expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails);
|
||||||
|
|
@ -403,7 +403,7 @@ l
|
||||||
const { appointmentDetails } = testData;
|
const { appointmentDetails } = testData;
|
||||||
let serviceLocationTitle = appointmentDetails?.serviceLocation == ServiceLocation.Mobile
|
let serviceLocationTitle = appointmentDetails?.serviceLocation == ServiceLocation.Mobile
|
||||||
? "We're coming to you"
|
? "We're coming to you"
|
||||||
: "You're going to a Safelite shop";
|
: "You're coming to us";
|
||||||
let serviceLocation: string[] = [];
|
let serviceLocation: string[] = [];
|
||||||
|
|
||||||
serviceLocation.push(
|
serviceLocation.push(
|
||||||
|
|
@ -443,7 +443,7 @@ l
|
||||||
customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : ""
|
customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : ""
|
||||||
);
|
);
|
||||||
|
|
||||||
expectedServicePackageDetails["Appointment date + time"] = appointmentDateText;
|
expectedServicePackageDetails["Appointment Date + Time"] = appointmentDateText;
|
||||||
return expectedServicePackageDetails;
|
return expectedServicePackageDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -452,7 +452,7 @@ l
|
||||||
let customerDetailsText: string[] = [];
|
let customerDetailsText: string[] = [];
|
||||||
|
|
||||||
customerDetailsText.push(
|
customerDetailsText.push(
|
||||||
customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(),
|
// customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(),
|
||||||
customerDetails?.email ? customerDetails?.email.toUpperCase() : "",
|
customerDetails?.email ? customerDetails?.email.toUpperCase() : "",
|
||||||
customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "",
|
customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "",
|
||||||
"Opted out of text message updates"
|
"Opted out of text message updates"
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ export class PaypalPage extends BasePage {
|
||||||
this.passwordTextBox = page.getByPlaceholder('Password');
|
this.passwordTextBox = page.getByPlaceholder('Password');
|
||||||
this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true });
|
this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true });
|
||||||
this.completePurchaseButton = page.getByTestId('submit-button-initial')
|
this.completePurchaseButton = page.getByTestId('submit-button-initial')
|
||||||
this.payWithRadioButton = page.locator('.py-4').first();
|
this.payWithRadioButton = page.getByRole('button').filter({ hasText: 'Pay with' });
|
||||||
this.payButton = page.getByRole('button', { name: 'Pay $' });
|
this.payButton = page.locator('#one-time-cta');
|
||||||
}
|
}
|
||||||
|
|
||||||
async completePaypalPurchase(paymentDetails: IPaymentDetails){
|
async completePaypalPurchase(paymentDetails: IPaymentDetails){
|
||||||
|
|
@ -45,7 +45,8 @@ export class PaypalPage extends BasePage {
|
||||||
await this.passwordTextBox.fill(paymentDetails.password!);
|
await this.passwordTextBox.fill(paymentDetails.password!);
|
||||||
await this.paypalLoginButton.click();
|
await this.paypalLoginButton.click();
|
||||||
await this.payWithRadioButton.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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ export class SchedulePage extends BasePage {
|
||||||
readonly militaryWarningMessage: Locator;
|
readonly militaryWarningMessage: Locator;
|
||||||
readonly changeZipButton: Locator;
|
readonly changeZipButton: Locator;
|
||||||
readonly updateZipTextBox: Locator;
|
readonly updateZipTextBox: Locator;
|
||||||
|
readonly safeliteShopsList: Locator;
|
||||||
|
readonly searchButton: Locator;
|
||||||
|
readonly storeAddressText: Locator;
|
||||||
readonly saveLocationButton: Locator;
|
readonly saveLocationButton: Locator;
|
||||||
readonly changeShopLocationLink: Locator;
|
readonly moreLocationsButton: Locator;
|
||||||
readonly selectAShopOptions: Locator;
|
readonly selectAShopOptions: Locator;
|
||||||
readonly yourSafeliteShop: Locator;
|
readonly yourSafeliteShop: Locator;
|
||||||
|
|
||||||
|
|
@ -44,12 +47,15 @@ export class SchedulePage extends BasePage {
|
||||||
this.militaryWarningMessage = this.page.locator('[class*="widget-name-AlertMilitaryBaseZipWidget"]');
|
this.militaryWarningMessage = this.page.locator('[class*="widget-name-AlertMilitaryBaseZipWidget"]');
|
||||||
this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))');
|
this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))');
|
||||||
this.updateZipTextBox = this.page.locator('#serviceZipCode').filter({ visible: true });
|
this.updateZipTextBox = this.page.locator('#serviceZipCode').filter({ visible: true });
|
||||||
|
this.safeliteShopsList = this.page.locator('fieldset:has(#chooseShop) label');
|
||||||
|
this.searchButton = this.page.locator('.search-icon-button');
|
||||||
|
this.storeAddressText = this.page.locator('#shop-location label:not(:first-child)');
|
||||||
this.saveLocationButton = this.page.getByText('Save location', { exact: true });
|
this.saveLocationButton = this.page.getByText('Save location', { exact: true });
|
||||||
this.changeShopLocationLink = this.page.locator(".shop-question a").filter({ hasText: "Change shop location " });
|
this.moreLocationsButton = this.page.locator(".shop-question a").filter({ hasText: "More locations " });
|
||||||
this.selectAShopOptions = this.page.locator('[class="shop-question"]');
|
this.selectAShopOptions = this.page.locator('[class="shop-question"]');
|
||||||
this.yourSafeliteShop = this.page.locator("fieldset:has(#chooseShop) label");
|
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.pickATimeButton = this.page.locator("label[buttonlabel='Pick a time']");
|
||||||
this.firstAvailableDate = this.page.locator('.selectable-day').filter({ visible: true}).locator('nth=0');
|
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');
|
this.firstAvailableTime = this.page.locator('label').filter({ hasText: /AM|PM/ }).locator('div').locator('nth=0');
|
||||||
|
|
@ -58,7 +64,7 @@ export class SchedulePage extends BasePage {
|
||||||
this.dateText = this.page.locator('label.modal-title');
|
this.dateText = this.page.locator('label.modal-title');
|
||||||
this.viewMoreDatesLink = this.page.getByText(/View more dates/).first();
|
this.viewMoreDatesLink = this.page.getByText(/View more dates/).first();
|
||||||
this.appointmentDuration = this.page.locator('.duration-text-block');
|
this.appointmentDuration = this.page.locator('.duration-text-block');
|
||||||
this.timeSlots = this.page.locator('fieldset[aria-labelledby=\'chooseTimeSlot\'] label');
|
this.timeSlots = this.page.locator('fieldset:has(>legend#chooseTimeSlot) label').filter({ visible: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectLocation(testData: Partial<ITestData>) {
|
async selectLocation(testData: Partial<ITestData>) {
|
||||||
|
|
@ -76,25 +82,36 @@ export class SchedulePage extends BasePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async scheduleInShop(appointmentDetails?: IAppointmentDetails) {
|
async scheduleInShop(appointmentDetails?: IAppointmentDetails) {
|
||||||
await this.inShopButton.click();
|
|
||||||
|
await this.inShopButton.isVisible() ? await this.inShopButton.click() : null;
|
||||||
if (appointmentDetails && appointmentDetails.shopAddress) {
|
if (appointmentDetails && appointmentDetails.shopAddress) {
|
||||||
const zipCodeMatch = appointmentDetails.shopAddress.match(/\b\d{5}$/);
|
const zipCodeMatch = appointmentDetails.shopAddress.match(/\b\d{5}$/);
|
||||||
if (zipCodeMatch) {
|
if (zipCodeMatch) {
|
||||||
const zipCode = zipCodeMatch[0];
|
const zipCode = zipCodeMatch[0];
|
||||||
// Enter the ZIP code into the updateZipTextBox
|
// Enter the ZIP code into the updateZipTextBox
|
||||||
await this.changeShopLocationLink.click();
|
await this.moreLocationsButton.click();
|
||||||
await this.page.waitForTimeout(500);
|
await this.page.waitForTimeout(500);
|
||||||
await this.updateZipTextBox.fill(zipCode);
|
await this.updateZipTextBox.fill(zipCode);
|
||||||
|
await this.searchButton.click().then( async() => await this.page.locator('loader-wrapper').waitFor({state: 'hidden'}));
|
||||||
|
await this.safeliteShopsList.first().click();
|
||||||
// Click the save location button
|
// Click the save location button
|
||||||
await this.saveLocationButton.click();
|
await this.saveLocationButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.inShopButton.click();
|
// await this.inShopButton.click();
|
||||||
// await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click());
|
// await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appointmentDetails && (appointmentDetails.shopAddress === "" || appointmentDetails.shopAddress === undefined )) {
|
if (appointmentDetails && (appointmentDetails.shopAddress === "" || appointmentDetails.shopAddress === undefined )) {
|
||||||
appointmentDetails.shopAddress = await this.yourSafeliteShop.getAttribute("buttonbodycopy") || "";
|
appointmentDetails.shopAddress = '';
|
||||||
|
await this.storeAddressText.first().waitFor({ state: 'visible' });
|
||||||
|
const elements = await this.storeAddressText.all();
|
||||||
|
|
||||||
|
for (let element of elements) {
|
||||||
|
let addressLine = await element.textContent() || "";
|
||||||
|
appointmentDetails.shopAddress += addressLine + ", "
|
||||||
|
};
|
||||||
|
appointmentDetails.shopAddress = appointmentDetails.shopAddress?.slice(0, -2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +163,7 @@ export class SchedulePage extends BasePage {
|
||||||
const inshopAvailableDates = this.page.locator('.selectable-day').filter({ visible: true}).all();
|
const inshopAvailableDates = this.page.locator('.selectable-day').filter({ visible: true}).all();
|
||||||
for (const inshopAvailableDate of await inshopAvailableDates) {
|
for (const inshopAvailableDate of await inshopAvailableDates) {
|
||||||
await inshopAvailableDate.click();
|
await inshopAvailableDate.click();
|
||||||
|
await this.page.waitForTimeout(500);
|
||||||
if (await this.allDayDropOffButton.isVisible()) {
|
if (await this.allDayDropOffButton.isVisible()) {
|
||||||
customerDetails!.apptDate = `${await inshopAvailableDate.getAttribute("id")}`;
|
customerDetails!.apptDate = `${await inshopAvailableDate.getAttribute("id")}`;
|
||||||
break;
|
break;
|
||||||
|
|
@ -186,13 +204,13 @@ export class SchedulePage extends BasePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// appointmentmentDetails.serviceLocation === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click();
|
// appointmentmentDetails.serviceLocation === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click();
|
||||||
customerDetails!.apptDuration = (await this.appointmentDuration.innerText()).replace("Duration: ", "");
|
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();
|
const selectedTimeSlot = await timeSlot.innerText();
|
||||||
let formattedTimeSlot: string = "";
|
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";
|
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";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export class ServicePackagesPage extends BasePage {
|
||||||
this.iHavePromoCodeButton = this.page.getByRole('link', { name: 'I have a promo code' });
|
this.iHavePromoCodeButton = this.page.getByRole('link', { name: 'I have a promo code' });
|
||||||
this.skipQuoteEmailButton = this.page.getByRole('button', { name: 'Skip' });
|
this.skipQuoteEmailButton = this.page.getByRole('button', { name: 'Skip' });
|
||||||
this.emailInput = this.page.getByRole('textbox', { name: 'Enter your email address' });
|
this.emailInput = this.page.getByRole('textbox', { name: 'Enter your email address' });
|
||||||
this.getMyQuoteButton = this.page.getByRole('button', { name: 'Get my quote' });
|
this.getMyQuoteButton = this.page.getByRole('button', { name: 'Send' });
|
||||||
this.closeButton = this.page. getByRole('dialog').locator('button').filter({ hasText: 'Close' });
|
this.closeButton = this.page. getByRole('dialog').locator('button').filter({ hasText: 'Close' });
|
||||||
this.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
|
this.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
|
||||||
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
|
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,9 @@ export class VehicleDamagePage extends BasePage {
|
||||||
super(page);
|
super(page);
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.windshieldChkBox = this.page.locator('[buttonlabel="Windshield"]');
|
this.windshieldChkBox = this.page.locator('[buttonlabel="Windshield"]');
|
||||||
this.crackButton = this.page.locator('[buttonlabel="Crack"]');
|
this.crackButton = this.page.locator('[buttonlabelsubcopy="Replace my windshield"]');
|
||||||
this.chipButton = this.page.locator('[buttonlabel="Chip(s)"]');
|
this.chipButton = this.page.locator('[buttonlabelsubcopy="Repair my windshield"]');
|
||||||
this.sideDoorButton = this.page.locator('[buttonlabel="Side door"]');
|
this.sideDoorButton = this.page.locator('[buttonlabel="Side window"]');
|
||||||
this.driverSideButton = this.page.locator('[buttonlabel="Driver side"]');
|
this.driverSideButton = this.page.locator('[buttonlabel="Driver side"]');
|
||||||
this.passengerSideButton = this.page.locator('[buttonlabel="Passenger side"]');
|
this.passengerSideButton = this.page.locator('[buttonlabel="Passenger side"]');
|
||||||
this.driverQuarterPanelChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Quarter panel"]');
|
this.driverQuarterPanelChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Quarter panel"]');
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ if (!process.env.CI) {
|
||||||
// Environment variables are present in CI environment, no need to read from file
|
// Environment variables are present in CI environment, no need to read from file
|
||||||
const basePath = __dirname; // This gets the directory where the config file is located
|
const basePath = __dirname; // This gets the directory where the config file is located
|
||||||
|
|
||||||
if (process.env.NODE_ENV == 'undefined' || process.env.NODE_ENV == null) {
|
if (process.env.PLAYWRIGHT_ENV == 'undefined' || process.env.PLAYWRIGHT_ENV == null) {
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: path.join(basePath, '.env.dev'),
|
path: path.join(basePath, '.env.dev'),
|
||||||
example: path.join(basePath, '.env.example')
|
example: path.join(basePath, '.env.example')
|
||||||
|
|
@ -17,7 +17,7 @@ if (!process.env.CI) {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: path.join(basePath, `.env.${process.env.NODE_ENV}`),
|
path: path.join(basePath, `.env.${process.env.PLAYWRIGHT_ENV}`),
|
||||||
example: path.join(basePath, '.env.example')
|
example: path.join(basePath, '.env.example')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ const ortoniReportConfig: OrtoniReportConfig = {
|
||||||
filename: `ortoni_report_${formatDateForFilename(new Date())}.html`,
|
filename: `ortoni_report_${formatDateForFilename(new Date())}.html`,
|
||||||
showProject: false,
|
showProject: false,
|
||||||
projectName: "FMG-Nextgen-Playwright-Report",
|
projectName: "FMG-Nextgen-Playwright-Report",
|
||||||
testType: `E2E- Environment: ${process.env.NODE_ENV} `,
|
testType: `E2E- Environment: ${process.env.PLAYWRIGHT_ENV} `,
|
||||||
preferredTheme: "light",
|
preferredTheme: "light",
|
||||||
base64Image: true,
|
base64Image: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,6 @@ async function runWorkflow(page: Page, testCase: TestCase) {
|
||||||
// let serviceLocationPage = testCase.pages.serviceLocationPage;
|
// let serviceLocationPage = testCase.pages.serviceLocationPage;
|
||||||
// await serviceLocationPage.handleServiceLocationPage(testCase.testData);
|
// await serviceLocationPage.handleServiceLocationPage(testCase.testData);
|
||||||
|
|
||||||
|
|
||||||
// Schedule appointment
|
// Schedule appointment
|
||||||
let schedulePage = testCase.pages.schedulePage;
|
let schedulePage = testCase.pages.schedulePage;
|
||||||
await schedulePage.handleSchedulePage(testCase.testData);
|
await schedulePage.handleSchedulePage(testCase.testData);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ const cashRepairMobileCCTests: ITestCase[] = [];
|
||||||
|
|
||||||
const tc = {
|
const tc = {
|
||||||
name: `CashRepairMobileCreditCard`,
|
name: `CashRepairMobileCreditCard`,
|
||||||
tags: ['@E2E','@CashRepairMobileCreditCard', '@test_report', '@CASH'],
|
tags: ['@E2E','@CashRepairMobileCreditCard', '@test_report', '@CASH', '@CASH-848'],
|
||||||
testData: cashRepairMobileCCData
|
testData: cashRepairMobileCCData
|
||||||
};
|
};
|
||||||
cashRepairMobileCCTests.push(tc);
|
cashRepairMobileCCTests.push(tc);
|
||||||
|
|
|
||||||
|
|
@ -42,14 +42,6 @@ const cashReplaceGlassAddressLookupInshopAfterPayData: Partial<ITestData> = {
|
||||||
vehicleLookupType: VehicleLookupType.Address
|
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
|
// Override payment details
|
||||||
paymentDetails: ClientData.getDefaultAfterpayDetails()
|
paymentDetails: ClientData.getDefaultAfterpayDetails()
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +50,7 @@ const cashReplaceGlassAddressLookupInshopAfterPayTests: ITestCase[] = [];
|
||||||
|
|
||||||
const tc = {
|
const tc = {
|
||||||
name: `CashReplaceGlassAddressLookupInshopAfterPay`,
|
name: `CashReplaceGlassAddressLookupInshopAfterPay`,
|
||||||
tags: ['@E2E','@CashReplaceGlassAddressLookupInshopAfterPay', '@test_report', '@CASH'],
|
tags: ['@E2E','@CashReplaceGlassAddressLookupInshopAfterPay', '@test_report', '@CASH', '@CASH-848'],
|
||||||
testData: cashReplaceGlassAddressLookupInshopAfterPayData
|
testData: cashReplaceGlassAddressLookupInshopAfterPayData
|
||||||
};
|
};
|
||||||
cashReplaceGlassAddressLookupInshopAfterPayTests.push(tc);
|
cashReplaceGlassAddressLookupInshopAfterPayTests.push(tc);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
|
||||||
appointmentDetails: {
|
appointmentDetails: {
|
||||||
serviceLocation: ServiceLocation.DropOff,
|
serviceLocation: ServiceLocation.DropOff,
|
||||||
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
|
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
|
||||||
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
|
shopAddress: "6826 sawmill rd, Columbus, OH 43235"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override vehicle details
|
// Override vehicle details
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ const CashReplaceSplitWindshieldTests: ITestCase[] = [];
|
||||||
|
|
||||||
const tc = {
|
const tc = {
|
||||||
name: `CashReplaceSplitWindshield`,
|
name: `CashReplaceSplitWindshield`,
|
||||||
tags: ['@E2E','@CashReplaceSplitWindshield', '@test_report', '@CASH'],
|
tags: ['@E2E','@CashReplaceSplitWindshield', '@test_report', '@CASH', '@CASH-848'],
|
||||||
testData: CashReplaceSplitWindshieldData
|
testData: CashReplaceSplitWindshieldData
|
||||||
};
|
};
|
||||||
CashReplaceSplitWindshieldTests.push(tc);
|
CashReplaceSplitWindshieldTests.push(tc);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const cashReplaceWiperDropoffData: Partial<ITestData> = {
|
||||||
servicePackage: ServicePackage.Standard,
|
servicePackage: ServicePackage.Standard,
|
||||||
|
|
||||||
// Special flags
|
// Special flags
|
||||||
isSkipEstimatePage: true,
|
isSkipEstimatePage: false,
|
||||||
isRecalVehicle: true,
|
isRecalVehicle: true,
|
||||||
|
|
||||||
// Override customer postal code
|
// Override customer postal code
|
||||||
|
|
@ -28,7 +28,7 @@ const cashReplaceWiperDropoffData: Partial<ITestData> = {
|
||||||
...getDefaultTestData().customerDetails!,
|
...getDefaultTestData().customerDetails!,
|
||||||
address: {
|
address: {
|
||||||
...getDefaultTestData().customerDetails!.address,
|
...getDefaultTestData().customerDetails!.address,
|
||||||
postalCode: '43085'
|
postalCode: '43235'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ const insuranceAcuityPaypalTests: ITestCase[] = [];
|
||||||
|
|
||||||
const tc = {
|
const tc = {
|
||||||
name: `InsuranceAcuityPaypal`,
|
name: `InsuranceAcuityPaypal`,
|
||||||
tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187'],
|
tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187', '@CASH-848'],
|
||||||
testData: insuranceAcuityPaypalData
|
testData: insuranceAcuityPaypalData
|
||||||
};
|
};
|
||||||
insuranceAcuityPaypalTests.push(tc);
|
insuranceAcuityPaypalTests.push(tc);
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ body select {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
body input:focus, body input:focus-visible,
|
body input:focus, body input:focus-visible,
|
||||||
body select:focus,
|
body select:focus,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ body {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||||
&:focus,
|
&:focus,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
box-shadow: 0 0 0 2.5px #1574a1;
|
box-shadow: 0 0 0 2.5px #1574a1;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ const damageLocationsSelected = {
|
||||||
PASSENGERSIDE: "PassengerSide",
|
PASSENGERSIDE: "PassengerSide",
|
||||||
STATIONARY: "Stationary",
|
STATIONARY: "Stationary",
|
||||||
SLIDER: "Slider",
|
SLIDER: "Slider",
|
||||||
|
DOOR: "Door",
|
||||||
|
GLASS: "Glass",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { damageLocationsSelected };
|
export { damageLocationsSelected };
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,6 @@
|
||||||
<div
|
<div
|
||||||
class="date-picker text-center"
|
class="date-picker text-center"
|
||||||
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`">
|
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`">
|
||||||
<div class="date-picker-header">
|
|
||||||
<funnelSubHeader cmsWidgetName="DatePickerSubHeaderWidget" class="mt-5" />
|
|
||||||
<textBlock
|
|
||||||
v-show="durationTextBlockCopy"
|
|
||||||
:customText="durationTextBlockCopy"
|
|
||||||
typeStyle="small"
|
|
||||||
marginTopSizeOverride="0"
|
|
||||||
class="duration-text-block" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
||||||
<legend class="sr-only">Select a day and time</legend>
|
<legend class="sr-only">Select a day and time</legend>
|
||||||
<div
|
<div
|
||||||
|
|
@ -139,36 +129,11 @@
|
||||||
@click="showAnotherMonth">
|
@click="showAnotherMonth">
|
||||||
View more dates
|
View more dates
|
||||||
</button>
|
</button>
|
||||||
<timeSlotQuestion
|
|
||||||
ref="timeSlotModalQuestion"
|
|
||||||
v-model="selectedTimeSlotInfo"
|
|
||||||
cmsWidgetName="TimeSlotModalQuestion"
|
|
||||||
waitListLabelWidget="WaitListLabelWidget"
|
|
||||||
waitListQuestionWidget="WaitListQuestionWidget"
|
|
||||||
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
|
|
||||||
mobileCmsWidgetName="MobileTimeSlotModal"
|
|
||||||
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
|
||||||
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
|
|
||||||
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
|
|
||||||
dropOffOrPickATimeQuestionCmsWidgetName="DropOffOrPickATimeQuestionWidget"
|
|
||||||
:selectedDate="selectedDate"
|
|
||||||
:appointmentType="appointmentType"
|
|
||||||
:premiumAppointmentFee="premiumAppointmentFee"
|
|
||||||
:displayWaitList="displayWaitList"
|
|
||||||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
|
||||||
:estimatedServiceMinutesMinimum="estimatedServiceMinutesMinimum"
|
|
||||||
:estimatedServiceMinutesMaximum="estimatedServiceMinutesMaximum"
|
|
||||||
@waitListRequested="handleWaitListRequested"
|
|
||||||
@time-slot-modal-closed="timeSlotModalClosed" />
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
|
||||||
import textBlock from "@/digital-components/text-block/text-block";
|
|
||||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
|
||||||
|
|
||||||
// Supporting files
|
// Supporting files
|
||||||
import loader from "@/ux-components/loader/loader";
|
import loader from "@/ux-components/loader/loader";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
@ -178,12 +143,6 @@ import {
|
||||||
MONTHS_OF_YEAR,
|
MONTHS_OF_YEAR,
|
||||||
DAYS_OF_WEEK,
|
DAYS_OF_WEEK,
|
||||||
} from "@/digital-components/date-picker/mixins/constants";
|
} from "@/digital-components/date-picker/mixins/constants";
|
||||||
import {
|
|
||||||
AppointmentTypeStrings,
|
|
||||||
RouteCodeFlags,
|
|
||||||
PREMIUM_FEE_PART_TYPE,
|
|
||||||
cmsWidgetFieldMappings,
|
|
||||||
} from "@/constants/schedule-constants";
|
|
||||||
import {
|
import {
|
||||||
selectableDaysOptions,
|
selectableDaysOptions,
|
||||||
requiredParameter,
|
requiredParameter,
|
||||||
|
|
@ -195,7 +154,6 @@ import {
|
||||||
import { useField, ErrorMessage } from "vee-validate";
|
import { useField, ErrorMessage } from "vee-validate";
|
||||||
import { deepClone } from "@/helpers/object-helper";
|
import { deepClone } from "@/helpers/object-helper";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "datePicker",
|
name: "datePicker",
|
||||||
|
|
@ -205,10 +163,8 @@ export default {
|
||||||
months: null,
|
months: null,
|
||||||
disableViewMoreDatesButton: false,
|
disableViewMoreDatesButton: false,
|
||||||
hideSomeDaysForInitialView: null,
|
hideSomeDaysForInitialView: null,
|
||||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
|
||||||
selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based)
|
selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based)
|
||||||
selectableDatesMobile: [],
|
selectableDatesMobile: [],
|
||||||
durationTextBlockCopyForInshopOrDropoff: null,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -242,12 +198,6 @@ export default {
|
||||||
pricingByDayBasePrice: Number,
|
pricingByDayBasePrice: Number,
|
||||||
pricingByDayUpcharge: Number,
|
pricingByDayUpcharge: Number,
|
||||||
isPricingByDayExperiment: Boolean,
|
isPricingByDayExperiment: Boolean,
|
||||||
timeSlotsForSelectedDate: Object,
|
|
||||||
appointmentType: String,
|
|
||||||
premiumAppointmentFee: Object,
|
|
||||||
estimatedServiceMinutesMinimum: Number,
|
|
||||||
estimatedServiceMinutesMaximum: Number,
|
|
||||||
displayWaitList: Boolean,
|
|
||||||
isMobileSelected: Boolean,
|
isMobileSelected: Boolean,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
|
@ -309,80 +259,10 @@ export default {
|
||||||
this.dispatchStoreAction(this.storeActions.SAVE_WAITLIST_REQUESTED, false, false);
|
this.dispatchStoreAction(this.storeActions.SAVE_WAITLIST_REQUESTED, false, false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
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.estimatedServiceMinutesMinimum,
|
|
||||||
this.estimatedServiceMinutesMaximum
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
|
|
||||||
return `${inshopDurationTextWithoutTime} <b>${inshopDurationTime}</b>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
mobileDurationText() {
|
|
||||||
const mobileDurationTextWithoutTime = this.getCmsContent(
|
|
||||||
"TimeSlotModalQuestion",
|
|
||||||
cmsWidgetFieldMappings.DURATION
|
|
||||||
);
|
|
||||||
|
|
||||||
const mobileDurationTime = getDisplayTextForDurationLength(
|
|
||||||
this.estimatedServiceMinutesMinimum,
|
|
||||||
this.estimatedServiceMinutesMaximum
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
|
|
||||||
return `${mobileDurationTextWithoutTime} <b>${mobileDurationTime}</b>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
durationTextBlockCopy() {
|
|
||||||
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.durationTextBlockCopyForInshopOrDropoff ?? this.inshopDurationText;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
isSameDay() {
|
|
||||||
if (!this.timeSlotsForSelectedDate) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const todaysDate = new Date().toISOString().split("T")[0];
|
|
||||||
return this.selectedDate === todaysDate;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async initializeComponent(initialData) {
|
async initializeComponent(initialData) {
|
||||||
await this.setCalendarData(initialData);
|
await this.setCalendarData(initialData);
|
||||||
this.$refs.timeSlotModalQuestion.initializeComponent();
|
|
||||||
},
|
},
|
||||||
fireDateSelectedEvent(event, date) {
|
fireDateSelectedEvent(event, date) {
|
||||||
// Ignore if arrow key selected radioButton
|
// Ignore if arrow key selected radioButton
|
||||||
|
|
@ -949,42 +829,9 @@ export default {
|
||||||
};
|
};
|
||||||
window.requestAnimationFrame(step);
|
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() {
|
getSupportingItems() {
|
||||||
return store.getters.lineItems.supportingItems;
|
return store.getters.lineItems.supportingItems;
|
||||||
},
|
},
|
||||||
handleWaitListRequested(value) {
|
|
||||||
this.$emit("waitListRequested", value);
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue(newValue) {
|
modelValue(newValue) {
|
||||||
|
|
@ -998,19 +845,10 @@ export default {
|
||||||
this.scrollToElement("date-of-month-error");
|
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: {
|
components: {
|
||||||
loader,
|
loader,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
timeSlotQuestion,
|
|
||||||
textBlock,
|
|
||||||
funnelSubHeader,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1028,14 +866,6 @@ export default {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
.duration-text-block {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-picker-header {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div
|
<div
|
||||||
class="modal fade modal-component"
|
class="modal fade modal-component"
|
||||||
:class="modalId"
|
:class="[modalId, { 'recall-show': isRecal }]"
|
||||||
v-on="{ 'hidden.bs.modal': onModalClosed, 'shown.bs.modal': onModalOpened }"
|
v-on="{ 'hidden.bs.modal': onModalClosed, 'shown.bs.modal': onModalOpened }"
|
||||||
:data-bs-backdrop="backdropSetting"
|
:data-bs-backdrop="backdropSetting"
|
||||||
:id="modalId"
|
:id="modalId"
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
@keypress.enter="onEnter"
|
@keypress.enter="onEnter"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<!--Close modal when clicking other than close button-->
|
<!--Close modal when clicking other than close button-->
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog" :class="{ 'modal-dialog-centered': !isRecal }">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header mb-0 mt-6">
|
<div class="modal-header mb-0 mt-6">
|
||||||
<label
|
<label
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<slot name="modal-footer-slot"></slot>
|
<slot name="modal-footer-slot"></slot>
|
||||||
<modalButtonMain
|
<modalButtonMain
|
||||||
isPrimary
|
:isPrimary="isFooterButtonPrimary"
|
||||||
class="w-100 modal-footer-button"
|
class="w-100 modal-footer-button"
|
||||||
:id="modalId + '-modalbtn'"
|
:id="modalId + '-modalbtn'"
|
||||||
ref="modalButtonMain"
|
ref="modalButtonMain"
|
||||||
|
|
@ -65,12 +65,17 @@ export default {
|
||||||
suppressPageScroll: Boolean,
|
suppressPageScroll: Boolean,
|
||||||
staticBackdrop: Boolean,
|
staticBackdrop: Boolean,
|
||||||
footerButtonDisabled: Boolean,
|
footerButtonDisabled: Boolean,
|
||||||
|
isFooterButtonPrimary: Boolean,
|
||||||
onModalOpenedCallback: {
|
onModalOpenedCallback: {
|
||||||
type: Function,
|
type: Function,
|
||||||
},
|
},
|
||||||
onModalClosedCallback: {
|
onModalClosedCallback: {
|
||||||
type: Function,
|
type: Function,
|
||||||
},
|
},
|
||||||
|
isRecal: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
|
|
@ -195,6 +200,19 @@ export default {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
&.modal-component {
|
&.modal-component {
|
||||||
|
&.recall-show {
|
||||||
|
.modal-dialog {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
width: auto;
|
||||||
|
max-width: 600px;
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
top: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
max-width: 767px;
|
max-width: 767px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
@ -250,6 +268,9 @@ export default {
|
||||||
button {
|
button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 50rem;
|
border-radius: 50rem;
|
||||||
|
&.btn-secondary {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,14 +115,16 @@ export default {
|
||||||
&.btn-secondary {
|
&.btn-secondary {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid $red;
|
border: 1px solid $blue;
|
||||||
color: $blue;
|
color: $blue;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 150ms linear;
|
transition: all 150ms linear;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:active {
|
||||||
color: $white;
|
color: $white;
|
||||||
@include blue-gradient;
|
background: $blue;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
&:focus, // Mouse, touch, stylus focus
|
&:focus, // Mouse, touch, stylus focus
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
|
@ -130,16 +132,18 @@ export default {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 3px $white,
|
0 0 0 3px $white,
|
||||||
0 0 0 5.5px $red;
|
0 0 0 5.5px $blue;
|
||||||
color: $white;
|
color: $blue;
|
||||||
@include blue-gradient;
|
&:active {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $gray-550 !important;
|
color: $gray-550 !important;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border: 1px solid $red;
|
border: 1px solid $blue;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
:max-length="12"
|
:max-length="12"
|
||||||
v-model="selectedValue"
|
v-model="selectedValue"
|
||||||
:mask="mask"
|
:mask="mask"
|
||||||
|
placeholderText="###-###-####"
|
||||||
:isRequired="isRequired"
|
:isRequired="isRequired"
|
||||||
:validationRules="validationRulesForTextBoxQuestion"
|
:validationRules="validationRulesForTextBoxQuestion"
|
||||||
:cmsWidgetName="cmsWidgetName" />
|
:cmsWidgetName="cmsWidgetName" />
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,47 @@ export default {
|
||||||
|
|
||||||
&.is-insurance {
|
&.is-insurance {
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
.pricing-info {
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
text-align: end;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
min-height: 80px;
|
||||||
|
max-height: 120px;
|
||||||
|
|
||||||
|
.pricing-info {
|
||||||
|
top: 2.1rem;
|
||||||
|
right: auto;
|
||||||
|
left: 2.3rem;
|
||||||
|
bottom: auto;
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - 2.5rem);
|
||||||
|
text-align: left;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-subheader {
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 160px;
|
||||||
|
|
||||||
|
.pricing-info {
|
||||||
|
top: 3.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-subheader {
|
&.has-subheader {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@
|
||||||
<div class="px-0" v-if="isCartReadyToLoad">
|
<div class="px-0" v-if="isCartReadyToLoad">
|
||||||
<div
|
<div
|
||||||
class="row vin-toggle flex align-items-center pt-4"
|
class="row vin-toggle flex align-items-center pt-4"
|
||||||
:class="[isExpanded ? 'expanded' : '']"
|
:class="[
|
||||||
|
isCollapsible ? (isExpanded ? 'expanded' : '') : 'expanded non-collapsible',
|
||||||
|
]"
|
||||||
@click="toggleIsExpanded()">
|
@click="toggleIsExpanded()">
|
||||||
<a
|
<a
|
||||||
aria-label="expand cart"
|
aria-label="expand cart"
|
||||||
href="javascript:void(0)"
|
href="javascript:void(0)"
|
||||||
class="col d-flex justify-content-between py-0">
|
class="col d-flex justify-content-between py-0">
|
||||||
<span class="label">{{ OrderDetailsText }}</span>
|
<span class="label">{{ cartHeaderText }}</span>
|
||||||
<span class="label amount-due larger">
|
<span class="label amount-due larger">
|
||||||
{{ getLineItemAmount(amountDue, showCoverageAsPending) }}
|
{{ getLineItemAmount(amountDue, showCoverageAsPending) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -208,6 +210,7 @@ export default {
|
||||||
isItac: Boolean,
|
isItac: Boolean,
|
||||||
isNoComp: Boolean,
|
isNoComp: Boolean,
|
||||||
isExpandedOnLoad: Boolean,
|
isExpandedOnLoad: Boolean,
|
||||||
|
isCollapsible: { type: Boolean, default: true },
|
||||||
isMSRFeeApplicable: Boolean,
|
isMSRFeeApplicable: Boolean,
|
||||||
donationCartItem: Object,
|
donationCartItem: Object,
|
||||||
},
|
},
|
||||||
|
|
@ -1094,9 +1097,19 @@ export default {
|
||||||
amountDueText() {
|
amountDueText() {
|
||||||
return this.getCmsContent("AmountDueTextWidget", "Text");
|
return this.getCmsContent("AmountDueTextWidget", "Text");
|
||||||
},
|
},
|
||||||
OrderDetailsText() {
|
orderDetailsText() {
|
||||||
return this.getCmsContent("OrderDetailsTextWidget", "Text");
|
return this.getCmsContent("OrderDetailsTextWidget", "Text");
|
||||||
},
|
},
|
||||||
|
estimateDetailsText() {
|
||||||
|
return this.getCmsContent("EstimateDetailsTextWidget", "Text");
|
||||||
|
},
|
||||||
|
cartHeaderText() {
|
||||||
|
if (!this.isInsurance && this.shouldHideRecalibration) {
|
||||||
|
return this.estimateDetailsText;
|
||||||
|
} else {
|
||||||
|
return this.orderDetailsText;
|
||||||
|
}
|
||||||
|
},
|
||||||
amountPaidText() {
|
amountPaidText() {
|
||||||
return this.getCmsContent("AmountPaidTextWidget", "Text");
|
return this.getCmsContent("AmountPaidTextWidget", "Text");
|
||||||
},
|
},
|
||||||
|
|
@ -1182,6 +1195,11 @@ export default {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
}
|
}
|
||||||
|
.non-collapsible {
|
||||||
|
.amount-due {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
.amount-due {
|
.amount-due {
|
||||||
color: $green;
|
color: $green;
|
||||||
|
|
||||||
|
|
@ -1283,23 +1301,26 @@ export default {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.vin-toggle {
|
.vin-toggle {
|
||||||
&:after {
|
&:not(.non-collapsible) {
|
||||||
content: "";
|
&:after {
|
||||||
transition: all 0.5s ease;
|
content: "";
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath fill='%230070D1' d='M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24Zm6.113 11.28-5.52 5.52a.84.84 0 0 1-1.186 0l-5.52-5.52a.843.843 0 1 1 1.186-1.2L12 15.012l4.927-4.932a.843.843 0 1 1 1.186 1.2Z'/%3E%3C/svg%3E");
|
transition: all 0.5s ease;
|
||||||
background-repeat: no-repeat;
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath fill='%230070D1' d='M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24Zm6.113 11.28-5.52 5.52a.84.84 0 0 1-1.186 0l-5.52-5.52a.843.843 0 1 1 1.186-1.2L12 15.012l4.927-4.932a.843.843 0 1 1 1.186 1.2Z'/%3E%3C/svg%3E");
|
||||||
background-position: right center;
|
background-repeat: no-repeat;
|
||||||
width: 24px;
|
background-position: right center;
|
||||||
height: 24px;
|
width: 24px;
|
||||||
display: inline-flex;
|
height: 24px;
|
||||||
position: relative;
|
display: inline-flex;
|
||||||
right: 0.75rem;
|
position: relative;
|
||||||
margin: 0.5rem 0 0.5rem 1rem;
|
right: 0.75rem;
|
||||||
cursor: pointer;
|
margin: 0.5rem 0 0.5rem 1rem;
|
||||||
}
|
cursor: pointer;
|
||||||
&.expanded:after {
|
}
|
||||||
transform: rotate(180deg);
|
&.expanded:after {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded + .cart-panel {
|
&.expanded + .cart-panel {
|
||||||
max-height: 650px;
|
max-height: 650px;
|
||||||
transition: all 150ms ease-in;
|
transition: all 150ms ease-in;
|
||||||
|
|
@ -1307,6 +1328,13 @@ export default {
|
||||||
padding: 1rem 0 0.5rem;
|
padding: 1rem 0 0.5rem;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.non-collapsible {
|
||||||
|
& > a {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-family: UrbanistSemibold;
|
font-family: UrbanistSemibold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
:ref="ModalName"
|
:ref="ModalName"
|
||||||
:modalId="ModalName"
|
:modalId="ModalName"
|
||||||
:footerButtonText="ModalCloseButtonText"
|
:footerButtonText="ModalCloseButtonText"
|
||||||
@footer-button-event="footerButtonClick">
|
@footer-button-event="footerButtonClick"
|
||||||
|
:isRecal="isRecal">
|
||||||
<div :class="[isRecal ? 'recal-modal' : '']">
|
<div :class="[isRecal ? 'recal-modal' : '']">
|
||||||
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
|
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
|
||||||
<h5 class="mb-4" v-html="ModalHeadline"></h5>
|
<h5 class="mb-4" v-html="ModalHeadline"></h5>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="funnel-header" v-if="imageSrc">
|
<div class="funnel-header" :class="{ scrolled: isScrolled }" v-if="imageSrc">
|
||||||
<div class="d-flex w-100">
|
<div class="d-flex w-100">
|
||||||
<progress-bar :page="$route.name" />
|
<progress-bar :page="$route.name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -61,6 +61,7 @@ export default {
|
||||||
sierraChatOpen: false,
|
sierraChatOpen: false,
|
||||||
salesforceChatOpen: false,
|
salesforceChatOpen: false,
|
||||||
isSalesforceTransferInProgress: false,
|
isSalesforceTransferInProgress: false,
|
||||||
|
isScrolled: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -108,6 +109,15 @@ export default {
|
||||||
}
|
}
|
||||||
this.globalAlertMessages.push(alertToPush);
|
this.globalAlertMessages.push(alertToPush);
|
||||||
},
|
},
|
||||||
|
removeGlobalAlert(alertId) {
|
||||||
|
const alertIndex = this.globalAlertMessages.findIndex((alert) => alert.id === alertId);
|
||||||
|
if (alertIndex !== -1) {
|
||||||
|
this.globalAlertMessages.splice(alertIndex, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getGlobalAlerts() {
|
||||||
|
return this.globalAlertMessages;
|
||||||
|
},
|
||||||
webchatClicked(event) {
|
webchatClicked(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.launchWebchat();
|
this.launchWebchat();
|
||||||
|
|
@ -185,6 +195,12 @@ export default {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
|
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
|
||||||
|
|
||||||
|
// Add scroll listener for header shadow effect
|
||||||
|
this._handleScroll = () => {
|
||||||
|
this.isScrolled = window.scrollY > 0;
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", this._handleScroll);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("sierra-chat-closed", this._handleSierraChatClosed);
|
window.removeEventListener("sierra-chat-closed", this._handleSierraChatClosed);
|
||||||
|
|
@ -195,6 +211,7 @@ export default {
|
||||||
);
|
);
|
||||||
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
|
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
|
||||||
window.removeEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
|
window.removeEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
|
||||||
|
window.removeEventListener("scroll", this._handleScroll);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -209,9 +226,18 @@ export default {
|
||||||
padding: 2rem 0.75rem;
|
padding: 2rem 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
position: relative;
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&.scrolled {
|
||||||
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.button-container {
|
.button-container {
|
||||||
&.webchat {
|
&.webchat {
|
||||||
margin-right: 0.875rem;
|
margin-right: 0.875rem;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.bolded-words) {
|
:deep(.bolded-words) {
|
||||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||||
|
color: $black;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
h5 {
|
h5 {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
|
@ -87,7 +90,7 @@ h5 {
|
||||||
}
|
}
|
||||||
|
|
||||||
p.small {
|
p.small {
|
||||||
color: $gray-550;
|
color: $gray-600;
|
||||||
}
|
}
|
||||||
.sub-text {
|
.sub-text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
suppressPageScroll
|
suppressPageScroll
|
||||||
:onModalClosedCallback="onModalClosed"
|
:onModalClosedCallback="onModalClosed"
|
||||||
:footerButtonText="modalButtonText"
|
:footerButtonText="modalButtonText"
|
||||||
|
:isFooterButtonPrimary="true"
|
||||||
@footer-button-event="saveProgress">
|
@footer-button-event="saveProgress">
|
||||||
<p class="modal-body-inner" v-html="modalBodyText"></p>
|
<p class="modal-body-inner" v-html="modalBodyText"></p>
|
||||||
<saveProgressQuestion
|
<saveProgressQuestion
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
suppressPageScroll
|
suppressPageScroll
|
||||||
:onModalClosedCallback="onModalClosed"
|
:onModalClosedCallback="onModalClosed"
|
||||||
:footerButtonText="modalButtonText"
|
:footerButtonText="modalButtonText"
|
||||||
|
:isFooterButtonPrimary="true"
|
||||||
@footer-button-event="saveProgress">
|
@footer-button-event="saveProgress">
|
||||||
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
||||||
<saveProgressQuestion
|
<saveProgressQuestion
|
||||||
|
|
|
||||||
|
|
@ -82,3 +82,56 @@ export async function isGlassAvailableForCarId(carId, pageNameToLog) {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSideDoorGlassString(glassLocation, glassName) {
|
||||||
|
let glassNameString;
|
||||||
|
switch (glassName) {
|
||||||
|
case glassLocations.BACK:
|
||||||
|
glassNameString = glassLocations.REAR + " " + glassLocations.DOOR; // "Back Door"
|
||||||
|
break;
|
||||||
|
case glassLocations.FRONT:
|
||||||
|
glassNameString = glassName + " " + glassLocations.DOOR; // "Front Door"
|
||||||
|
break;
|
||||||
|
case glassLocations.VENT:
|
||||||
|
case glassLocations.QUARTER:
|
||||||
|
glassNameString = glassName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return glassLocation + " " + glassName + " " + glassLocations.GLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDamageInfoWordingText(damageInfo) {
|
||||||
|
const glassTextArray = [];
|
||||||
|
|
||||||
|
damageInfo.glassToReplace.forEach((item) => {
|
||||||
|
let string = "";
|
||||||
|
if (item.glassLocation === glassLocations.WINDSHIELD) {
|
||||||
|
string = item.glassLocation;
|
||||||
|
}
|
||||||
|
if (item.glassLocation === glassLocations.REAR) {
|
||||||
|
string = glassLocations.BACK + " " + glassLocations.GLASS;
|
||||||
|
}
|
||||||
|
if (item.glassLocation === glassLocations.DRIVER) {
|
||||||
|
string = getSideDoorGlassString(item.glassLocation, item.glassName);
|
||||||
|
// ONLY INCLUDE "DOOR" IF IT'S REAR or FRONT (vent or quarter should NOT)
|
||||||
|
}
|
||||||
|
if (item.glassLocation === glassLocations.PASSENGER) {
|
||||||
|
string = getSideDoorGlassString(item.glassLocation, item.glassName);
|
||||||
|
}
|
||||||
|
glassTextArray.push(string);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (glassTextArray.length === 1) {
|
||||||
|
return glassTextArray[0];
|
||||||
|
}
|
||||||
|
let damageInfoWordingText = glassTextArray.join(", "); // string
|
||||||
|
let lastCommaIndex = damageInfoWordingText.lastIndexOf(", ");
|
||||||
|
if (lastCommaIndex > -1) {
|
||||||
|
return (
|
||||||
|
damageInfoWordingText.slice(0, lastCommaIndex) +
|
||||||
|
" and " +
|
||||||
|
damageInfoWordingText.slice(lastCommaIndex + 2, damageInfoWordingText.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -757,7 +757,7 @@ function setupMocks({
|
||||||
wrapper.vm.$refs.navbar.removeLoader = jest.fn();
|
wrapper.vm.$refs.navbar.removeLoader = jest.fn();
|
||||||
const closestShops = {
|
const closestShops = {
|
||||||
data: {
|
data: {
|
||||||
providers: [
|
inShopProviders: [
|
||||||
{ id: 1, name: "Shop 1" },
|
{ id: 1, name: "Shop 1" },
|
||||||
{ id: 2, name: "Shop 2" },
|
{ id: 2, name: "Shop 2" },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ export default {
|
||||||
this.serviceZipCode,
|
this.serviceZipCode,
|
||||||
this.carId
|
this.carId
|
||||||
);
|
);
|
||||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||||
if (this.displayNoServiceAlert) {
|
if (this.displayNoServiceAlert) {
|
||||||
return this.$refs.navbar.removeLoader();
|
return this.$refs.navbar.removeLoader();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export default {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $white;
|
background: $white;
|
||||||
transition: all 150ms linear;
|
transition: all 150ms linear;
|
||||||
border-radius: $border-radius-lg;
|
border-radius: $border-radius-pill;
|
||||||
border: 1px solid $gray-500;
|
border: 1px solid $gray-500;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,15 @@
|
||||||
:donationCartItem="donationLineItem"
|
:donationCartItem="donationLineItem"
|
||||||
:showAsPaid="isPia"
|
:showAsPaid="isPia"
|
||||||
servicePackageOptionsCmsName="ServicePackageTitle"
|
servicePackageOptionsCmsName="ServicePackageTitle"
|
||||||
|
recyclingModalCmsWidgetName="RecycleModal"
|
||||||
:isInsurance="isInsurance"
|
:isInsurance="isInsurance"
|
||||||
:insuranceDeductible="currentDeductible"
|
:insuranceDeductible="currentDeductible"
|
||||||
:insuranceCompanyName="insuranceCompanyName"
|
:insuranceCompanyName="insuranceCompanyName"
|
||||||
:showInsuranceCoverageAs="showInsuranceCoverageAs"
|
:showInsuranceCoverageAs="showInsuranceCoverageAs"
|
||||||
:shouldHideRecalibration="shouldHideRecalibration"
|
:shouldHideRecalibration="shouldHideRecalibration"
|
||||||
:isExpandedOnLoad="false"
|
|
||||||
:isItac="isItac"
|
:isItac="isItac"
|
||||||
:isNoComp="isNoComp"
|
:isNoComp="isNoComp"
|
||||||
|
:isExpandedOnLoad="false"
|
||||||
:isMSRFeeApplicable="isMSRFeeApplicable" />
|
:isMSRFeeApplicable="isMSRFeeApplicable" />
|
||||||
|
|
||||||
<hr class="mb-5" />
|
<hr class="mb-5" />
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
<div class="container page-container-grouped-styles">
|
<div class="container page-container-grouped-styles">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-4 mb-5" />
|
<funnelSubHeader
|
||||||
|
cmsWidgetName="FunnelSubHeaderWidget"
|
||||||
|
class="mt-4 mb-5"
|
||||||
|
alignLeft />
|
||||||
<textboxQuestion
|
<textboxQuestion
|
||||||
isRequired
|
isRequired
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
|
@ -215,3 +218,8 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.form-check {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
@back-clicked="backButtonAction"
|
@back-clicked="backButtonAction"
|
||||||
@ForwardClicked="forwardButtonAction" />
|
@ForwardClicked="forwardButtonAction" />
|
||||||
|
|
||||||
<div class="text-center" v-html="this.VinLookupQuestionFooterText2"></div>
|
<div class="text-center mb-5" v-html="this.VinLookupQuestionFooterText2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -268,6 +268,15 @@ export default {
|
||||||
.header-sub-copy {
|
.header-sub-copy {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
.funnel-sub-header {
|
||||||
|
padding-bottom: 0;
|
||||||
|
h5.dark-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.sub-text strong {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.select-an-option {
|
.select-an-option {
|
||||||
font-family: UrbanistSemibold;
|
font-family: UrbanistSemibold;
|
||||||
|
|
@ -277,5 +286,6 @@ export default {
|
||||||
.select-option {
|
.select-option {
|
||||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: $black;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<textboxQuestion
|
<textboxQuestion
|
||||||
v-model="selectedAccountName"
|
v-model="selectedAccountName"
|
||||||
|
ref="insuranceCoQuestion"
|
||||||
customInputId="autocomplete"
|
customInputId="autocomplete"
|
||||||
class="mb-4 mt-5"
|
class="mb-4 mt-5"
|
||||||
cmsWidgetName="InsuranceCoQuestionWidget"
|
cmsWidgetName="InsuranceCoQuestionWidget"
|
||||||
includeSearchIcon
|
|
||||||
:validationRules="validationRules" />
|
:validationRules="validationRules" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="container page-container-grouped-styles">
|
<div class="container page-container-grouped-styles">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-4" />
|
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-4" alignLeft />
|
||||||
|
|
||||||
<div v-if="originalList.length > 0">
|
<div v-if="originalList.length > 0">
|
||||||
<insurance-company-question
|
<insurance-company-question
|
||||||
|
|
|
||||||
|
|
@ -722,7 +722,7 @@ function setupMocks({
|
||||||
wrapper.vm.$refs.navbar.removeLoader = jest.fn();
|
wrapper.vm.$refs.navbar.removeLoader = jest.fn();
|
||||||
const closestShops = {
|
const closestShops = {
|
||||||
data: {
|
data: {
|
||||||
providers: [
|
inShopProviders: [
|
||||||
{ id: 1, name: "Shop 1" },
|
{ id: 1, name: "Shop 1" },
|
||||||
{ id: 2, name: "Shop 2" },
|
{ id: 2, name: "Shop 2" },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ export default {
|
||||||
this.serviceZipCode,
|
this.serviceZipCode,
|
||||||
this.carId
|
this.carId
|
||||||
);
|
);
|
||||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||||
if (this.displayNoServiceAlert) {
|
if (this.displayNoServiceAlert) {
|
||||||
return this.$refs.navbar.removeLoader();
|
return this.$refs.navbar.removeLoader();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<div class="row mb-4" aria-live="polite">
|
<div class="mb-4" aria-live="polite">
|
||||||
<div class="col-8">
|
<div class="col mb-4">
|
||||||
<dropdownQuestion
|
<dropdownQuestion
|
||||||
customDropdownId="state"
|
customDropdownId="state"
|
||||||
cmsWidgetName="StateQuestionWidget"
|
cmsWidgetName="StateQuestionWidget"
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
:labelBold="labelBold"
|
:labelBold="labelBold"
|
||||||
:isDisabled="this.isStateDisabled" />
|
:isDisabled="this.isStateDisabled" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col">
|
||||||
<textboxQuestion
|
<textboxQuestion
|
||||||
customInputId="zipCode"
|
customInputId="zipCode"
|
||||||
cmsWidgetName="ZipQuestionWidget"
|
cmsWidgetName="ZipQuestionWidget"
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ beforeEach(() => {
|
||||||
describe("mobile-details.vue", () => {
|
describe("mobile-details.vue", () => {
|
||||||
test("if the continue button is clicked, navigate forward", async () => {
|
test("if the continue button is clicked, navigate forward", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const { wrapper } = setupMocks(mobileDetails, {
|
const { wrapper } = setupMocks({
|
||||||
mixins: [mockMixin],
|
mixins: [mockMixin],
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
});
|
});
|
||||||
|
|
@ -91,7 +91,7 @@ describe("mobile-details.vue", () => {
|
||||||
});
|
});
|
||||||
test("arePagePrerequisitesValid should be true ", async () => {
|
test("arePagePrerequisitesValid should be true ", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
const { wrapper } = setupMocks(mobileDetails, {
|
const { wrapper } = setupMocks({
|
||||||
mixins: [mockMixin],
|
mixins: [mockMixin],
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
});
|
});
|
||||||
|
|
@ -105,7 +105,7 @@ describe("mobile-details.vue", () => {
|
||||||
|
|
||||||
test("if the back button is clicked, navigate back", async () => {
|
test("if the back button is clicked, navigate back", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const { wrapper } = setupMocks(mobileDetails, {
|
const { wrapper } = setupMocks({
|
||||||
mixins: [mockMixin],
|
mixins: [mockMixin],
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,26 @@
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
||||||
<hr class="my-3" />
|
<hr class="my-3" />
|
||||||
<textBlock cmsWidgetName="AppointmentTimeOfService" class="time-header" />
|
<div class="date-time-edit">
|
||||||
|
<textBlock cmsWidgetName="AppointmentTimeOfService" class="time-header" />
|
||||||
|
<textLink
|
||||||
|
linkType="navigation"
|
||||||
|
:text="editLink"
|
||||||
|
@click-event="linkClick"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
data-bs-target="#footerModal"
|
||||||
|
data-bs-dismiss="modal" />
|
||||||
|
</div>
|
||||||
<textBlock cmsWidgetName="AppointmentDateAndTime" />
|
<textBlock cmsWidgetName="AppointmentDateAndTime" />
|
||||||
<hr class="my-3" />
|
<hr class="my-3" />
|
||||||
<div class="mobile-location-questions">
|
<div class="mobile-location-questions">
|
||||||
<div class="address-questions-container">
|
<div class="address-questions-container">
|
||||||
|
<textBlock
|
||||||
|
cmsWidgetName="PleaseProvideAddressWidget"
|
||||||
|
class="provide-address-header" />
|
||||||
|
<textBlock
|
||||||
|
cmsWidgetName="VehicleKeysMessageWidget"
|
||||||
|
class="keys-message" />
|
||||||
<mobileAddressQuestions
|
<mobileAddressQuestions
|
||||||
ref="addressQuestions"
|
ref="addressQuestions"
|
||||||
v-model="this.addressQuestions"
|
v-model="this.addressQuestions"
|
||||||
|
|
@ -56,6 +71,8 @@ import store from "@/store";
|
||||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||||
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
|
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||||
export default {
|
export default {
|
||||||
name: "MobileDetails",
|
name: "MobileDetails",
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -142,6 +159,17 @@ export default {
|
||||||
this.pageName
|
this.pageName
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
linkClick() {
|
||||||
|
// check session expired and initSession to recreate cookies
|
||||||
|
if (analyticsMixin.methods.sessionExpired()) {
|
||||||
|
this.routeReturnUser();
|
||||||
|
} else {
|
||||||
|
this.backButtonAction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routeReturnUser() {
|
||||||
|
this.$router.sendToReturnUser();
|
||||||
|
},
|
||||||
backButtonAction() {
|
backButtonAction() {
|
||||||
this.$router.navigateWithoutSaving(
|
this.$router.navigateWithoutSaving(
|
||||||
this.navigationScenarios.CLICKED_BACK,
|
this.navigationScenarios.CLICKED_BACK,
|
||||||
|
|
@ -156,6 +184,9 @@ export default {
|
||||||
AppointmentTimeOfService() {
|
AppointmentTimeOfService() {
|
||||||
return this.getCmsContent("AppointmentDateAndTime", "Text");
|
return this.getCmsContent("AppointmentDateAndTime", "Text");
|
||||||
},
|
},
|
||||||
|
editLink() {
|
||||||
|
return this.getCmsContent("EditLinkWidget", "Text");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
mobileAddressQuestions,
|
mobileAddressQuestions,
|
||||||
|
|
@ -166,6 +197,7 @@ export default {
|
||||||
funnelSubHeader,
|
funnelSubHeader,
|
||||||
Form,
|
Form,
|
||||||
loadingModal,
|
loadingModal,
|
||||||
|
textLink,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -175,5 +207,40 @@ export default {
|
||||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $black;
|
color: $black;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.provide-address-header {
|
||||||
|
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $black;
|
||||||
|
line-height: 1.625rem;
|
||||||
|
letter-spacing: 0.03rem;
|
||||||
|
}
|
||||||
|
.keys-message {
|
||||||
|
background-color: #fff5eb;
|
||||||
|
color: #4d5151;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem 1rem 0.5rem 1rem;
|
||||||
|
min-height: 3.656rem;
|
||||||
|
line-height: 1.625rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.date-time-edit {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
:deep(span.unbolded-text) {
|
||||||
|
font-weight: 400;
|
||||||
|
color: $gray-600;
|
||||||
|
font-family: UrbanistRegular;
|
||||||
|
}
|
||||||
|
:deep(.caption) {
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ export default {
|
||||||
.question-text span {
|
.question-text span {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,8 @@
|
||||||
<contentGroupModal
|
<contentGroupModal
|
||||||
ref="RecalModal"
|
ref="RecalModal"
|
||||||
cmsWidgetName="RecalModal"
|
cmsWidgetName="RecalModal"
|
||||||
class="recal-modal" />
|
class="recal-modal"
|
||||||
|
isRecal />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -222,7 +223,8 @@ export default {
|
||||||
"payment-method"
|
"payment-method"
|
||||||
);
|
);
|
||||||
|
|
||||||
const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
|
// const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
|
||||||
|
// (removed temporarily for Heritage parity effort)
|
||||||
|
|
||||||
const promiseResultMap = [
|
const promiseResultMap = [
|
||||||
{
|
{
|
||||||
|
|
@ -237,10 +239,11 @@ export default {
|
||||||
resultKey: "rainDefense",
|
resultKey: "rainDefense",
|
||||||
promise: rainDefensePromise,
|
promise: rainDefensePromise,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
resultKey: "reviewDropdownData",
|
// resultKey: "reviewDropdownData",
|
||||||
promise: reviewDropdownPromise,
|
// promise: reviewDropdownPromise,
|
||||||
},
|
// },
|
||||||
|
// (removed temporarily for Heritage parity effort)
|
||||||
];
|
];
|
||||||
|
|
||||||
const resultMap = await settleAllPromises(promiseResultMap);
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
@ -345,7 +348,8 @@ export default {
|
||||||
);
|
);
|
||||||
vm.updateFooterButtonText(vm.customCtaCopy);
|
vm.updateFooterButtonText(vm.customCtaCopy);
|
||||||
|
|
||||||
vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
|
// vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
|
||||||
|
// (removed temporarily for Heritage parity effort)
|
||||||
|
|
||||||
if (revalidatePromoResponse) {
|
if (revalidatePromoResponse) {
|
||||||
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||||
|
|
@ -751,15 +755,15 @@ export default {
|
||||||
customCtaCopy() {
|
customCtaCopy() {
|
||||||
switch (this.paymentMethod) {
|
switch (this.paymentMethod) {
|
||||||
case paymentMethods.PAY_NOW:
|
case paymentMethods.PAY_NOW:
|
||||||
return "Continue to checkout";
|
return "Continue"; // Before Heritage Parity it was "Continue to checkout"
|
||||||
case paymentMethods.INSURANCE:
|
case paymentMethods.INSURANCE:
|
||||||
return "Continue to insurance";
|
return "Continue"; // Before Heritage Parity it was "Continue to insurance"
|
||||||
case paymentMethods.CREDIT_CARD:
|
case paymentMethods.CREDIT_CARD:
|
||||||
return "Continue to checkout";
|
return "Continue"; // Before Heritage Parity it was "Continue to checkout"
|
||||||
case paymentMethods.PAYPAL:
|
case paymentMethods.PAYPAL:
|
||||||
return "Continue to Paypal";
|
return "Continue"; // Before Heritage Parity it was "Continue to Paypal"
|
||||||
case paymentMethods.AFTERPAY:
|
case paymentMethods.AFTERPAY:
|
||||||
return "Continue to Afterpay";
|
return "Continue"; // Before Heritage Parity it was "Continue to Afterpay"
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -846,6 +850,18 @@ export default {
|
||||||
(newPromo) => !oldPromoCodes.includes(newPromo.promoCode)
|
(newPromo) => !oldPromoCodes.includes(newPromo.promoCode)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Remove any existing promo error alerts for this promo code
|
||||||
|
const existingAlerts = this.$refs.funnelHeader.getGlobalAlerts();
|
||||||
|
if (existingAlerts.length > 0) {
|
||||||
|
const errorPromoAlertToRemove = existingAlerts.find(
|
||||||
|
(alert) =>
|
||||||
|
alert.messageHeadline === "Promo code error" &&
|
||||||
|
alert.messageCopy.includes(newlyActivatedPromoCodes[0].promoCode)
|
||||||
|
);
|
||||||
|
errorPromoAlertToRemove &&
|
||||||
|
this.$refs.funnelHeader.removeGlobalAlert(errorPromoAlertToRemove.id);
|
||||||
|
}
|
||||||
|
|
||||||
const alert = createPromoSuccessAlert(newlyActivatedPromoCodes[0].promoCode);
|
const alert = createPromoSuccessAlert(newlyActivatedPromoCodes[0].promoCode);
|
||||||
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
||||||
} else if (
|
} else if (
|
||||||
|
|
@ -891,6 +907,9 @@ export default {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:deep(.review-table p) {
|
||||||
|
margin: 0.25rem 0 0 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.checkbox-group {
|
.checkbox-group {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
@ -932,17 +951,31 @@ export default {
|
||||||
:deep(.modal-body) {
|
:deep(.modal-body) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
p.subheader-text {
|
||||||
|
color: $red;
|
||||||
|
order: 1 !important;
|
||||||
|
margin-top: -2.25rem;
|
||||||
|
}
|
||||||
h5 {
|
h5 {
|
||||||
text-align: center;
|
text-align: left !important;
|
||||||
order: 1;
|
order: 2 !important;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
order: 3;
|
order: 4;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
order: 2;
|
order: 3 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:deep(span.label) {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
:deep(div.service-questions span) {
|
||||||
|
color: $black;
|
||||||
|
font-family: UrbanistSemibold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,28 @@
|
||||||
<div v-html="apptWordingText" v-show="!isExpanded"></div>
|
<div v-html="apptWordingText" v-show="!isExpanded"></div>
|
||||||
|
|
||||||
<div class="review-table px-4">
|
<div class="review-table px-4">
|
||||||
|
<appointmentReview
|
||||||
|
class="service-location"
|
||||||
|
cmsWidgetName="AppointmentWidget"
|
||||||
|
:apptWordingText="apptWordingText" />
|
||||||
|
|
||||||
|
<hr class="my-0" />
|
||||||
|
|
||||||
|
<parityDamageReview
|
||||||
|
cmsWidgetName="DamageReviewWidget"
|
||||||
|
damageLocationsWidgetName="DamageLocationsWidget"
|
||||||
|
:damage="damageInfo"
|
||||||
|
:vehicle="vehicleInfo" />
|
||||||
|
|
||||||
|
<hr class="my-0" />
|
||||||
|
|
||||||
|
<customerReview cmsWidgetName="CustomerReviewWidget" :customer="customerInfo" />
|
||||||
|
|
||||||
|
<hr class="my-0" />
|
||||||
|
|
||||||
|
<optInReview cmsWidgetName="OptInReviewWidget" :customer="customerInfo" />
|
||||||
|
|
||||||
|
<!-- the following was the order prior to Heritage parity
|
||||||
<vehicleReview cmsWidgetName="VehicleReviewWidget" :vehicle="vehicleInfo" />
|
<vehicleReview cmsWidgetName="VehicleReviewWidget" :vehicle="vehicleInfo" />
|
||||||
|
|
||||||
<hr class="my-0" />
|
<hr class="my-0" />
|
||||||
|
|
@ -48,6 +70,7 @@
|
||||||
<hr class="my-0" />
|
<hr class="my-0" />
|
||||||
|
|
||||||
<customerReview cmsWidgetName="CustomerReviewWidget" :customer="customerInfo" />
|
<customerReview cmsWidgetName="CustomerReviewWidget" :customer="customerInfo" />
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -56,8 +79,11 @@
|
||||||
import textBlock from "@/digital-components/text-block/text-block";
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
|
||||||
import customerReview from "@/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review";
|
import customerReview from "@/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review";
|
||||||
|
import optInReview from "@/layouts/payment-method/review-dropdown/review-sections/opt-in-review/opt-in-review";
|
||||||
import damageReview from "@/layouts/payment-method/review-dropdown/review-sections/damage-review/damage-review";
|
import damageReview from "@/layouts/payment-method/review-dropdown/review-sections/damage-review/damage-review";
|
||||||
|
import parityDamageReview from "@/layouts/payment-method/review-dropdown/review-sections/parity-damage-review/parity-damage-review";
|
||||||
import scheduleReview from "@/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review";
|
import scheduleReview from "@/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review";
|
||||||
|
import appointmentReview from "@/layouts/payment-method/review-dropdown/review-sections/appointment-review/appointment-review";
|
||||||
import serviceLocationReview from "@/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review";
|
import serviceLocationReview from "@/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review";
|
||||||
import servicePackageReview from "@/layouts/payment-method/review-dropdown/review-sections/service-package-review/service-package-review";
|
import servicePackageReview from "@/layouts/payment-method/review-dropdown/review-sections/service-package-review/service-package-review";
|
||||||
import vehicleReview from "@/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review";
|
import vehicleReview from "@/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review";
|
||||||
|
|
@ -69,6 +95,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
apptWordingText: String,
|
apptWordingText: String,
|
||||||
serviceLocationFullAddress: String,
|
serviceLocationFullAddress: String,
|
||||||
|
damageInfoHeader: String,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -76,21 +103,22 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadInitialData() {
|
// the following was used prior to Heritage parity
|
||||||
const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData();
|
// async loadInitialData() {
|
||||||
|
// const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData();
|
||||||
|
|
||||||
const promiseResultMap = [
|
// const promiseResultMap = [
|
||||||
{
|
// {
|
||||||
resultKey: "servicePackageData",
|
// resultKey: "servicePackageData",
|
||||||
promise: servicePackageInitialDataPromise,
|
// promise: servicePackageInitialDataPromise,
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
return settleAllPromises(promiseResultMap);
|
// return settleAllPromises(promiseResultMap);
|
||||||
},
|
// },
|
||||||
initializeComponent(apiResponses) {
|
// initializeComponent(apiResponses) {
|
||||||
this.$refs.servicePackageReview.initializeComponent(apiResponses.servicePackageData);
|
// this.$refs.servicePackageReview.initializeComponent(apiResponses.servicePackageData);
|
||||||
},
|
// },
|
||||||
toggleIsExpanded() {
|
toggleIsExpanded() {
|
||||||
this.isExpanded = !this.isExpanded;
|
this.isExpanded = !this.isExpanded;
|
||||||
},
|
},
|
||||||
|
|
@ -117,11 +145,9 @@ export default {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
customerReview,
|
customerReview,
|
||||||
damageReview,
|
optInReview,
|
||||||
scheduleReview,
|
appointmentReview,
|
||||||
serviceLocationReview,
|
parityDamageReview,
|
||||||
servicePackageReview,
|
|
||||||
vehicleReview,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -138,6 +164,10 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|
||||||
|
& > :first-child {
|
||||||
|
margin-top: -0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.service-location) {
|
:deep(.service-location) {
|
||||||
.review-block-content {
|
.review-block-content {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
|
||||||
|
import appointmentReview from "@/layouts/payment-method/review-dropdown/review-sections/appointment-review/appointment-review";
|
||||||
|
|
||||||
|
const testConstants = {
|
||||||
|
cms: {
|
||||||
|
header: {
|
||||||
|
text: "Appointment Details",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
appointment: {
|
||||||
|
apptWordingText:
|
||||||
|
"September 10, 2025 arriving between 9:00 AM — 11:00 AM at 123 Main St, anytown, OH 43215",
|
||||||
|
},
|
||||||
|
displayContent: {
|
||||||
|
apptWordingText:
|
||||||
|
"September 10, 2025 arriving between 9:00 AM — 11:00 AM at 123 Main St, anytown, OH 43215",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmsContent;
|
||||||
|
|
||||||
|
describe("Appointment Review Block", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cmsContent = {
|
||||||
|
AppointmentWidget: {
|
||||||
|
HeaderText: testConstants.cms.header.text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should display header text from cms", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.headerText).toEqual(testConstants.cms.header.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should render correct display content", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.displayContent).toEqual([testConstants.displayContent.apptWordingText]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should emit edit-clicked when editClicked method is called", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.editClicked();
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted("edit-clicked")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateDefaultProps() {
|
||||||
|
return {
|
||||||
|
cmsWidgetName: "AppointmentWidget",
|
||||||
|
apptWordingText: testConstants.appointment.apptWordingText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMocks(customMountOptions) {
|
||||||
|
const mountOptions = getMountOptions(customMountOptions);
|
||||||
|
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||||
|
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.global.mixins = [mockMixin];
|
||||||
|
|
||||||
|
const wrapper = shallowMount(appointmentReview, mountOptions);
|
||||||
|
wrapper.vm.setCmsContent = jest.fn();
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<reviewBlock
|
||||||
|
:customHeaderText="headerText"
|
||||||
|
:content="displayContent"
|
||||||
|
@edit-clicked="editClicked" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import reviewBlock from "@/layouts/payment-method/review-dropdown/review-block/review-block";
|
||||||
|
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "appointment-review",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
apptWordingText: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClicked() {
|
||||||
|
this.$emit("edit-clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayContent() {
|
||||||
|
return [this.apptWordingText];
|
||||||
|
},
|
||||||
|
headerText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
reviewBlock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -13,17 +13,17 @@ const testConstants = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
customer: {
|
customer: {
|
||||||
firstName: "First",
|
// firstName: "First",
|
||||||
lastName: "Last",
|
// lastName: "Last",
|
||||||
phoneNumber: "111-111-1111",
|
phoneNumber: "111-111-1111",
|
||||||
emailAddress: "builddigitaltest@safelite.com",
|
emailAddress: "builddigitaltest@safelite.com",
|
||||||
isSmsOptIn: false,
|
// isSmsOptIn: false,
|
||||||
},
|
},
|
||||||
displayContent: {
|
displayContent: {
|
||||||
fullName: "First Last",
|
// fullName: "First Last",
|
||||||
phoneNumber: "111-111-1111",
|
phoneNumber: "111-111-1111",
|
||||||
emailAddress: "builddigitaltest@safelite.com",
|
emailAddress: "builddigitaltest@safelite.com",
|
||||||
smsOptIn: "Sms",
|
// smsOptIn: "Sms",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,20 +54,20 @@ describe("Customer Review Block", () => {
|
||||||
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
|
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should display sms text from cms", async () => {
|
// test("Should display sms text from cms", async () => {
|
||||||
// Arrange
|
// // Arrange
|
||||||
let props = generateDefaultProps();
|
// let props = generateDefaultProps();
|
||||||
|
|
||||||
const { wrapper } = setupMocks({
|
// const { wrapper } = setupMocks({
|
||||||
propsData: props,
|
// propsData: props,
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Act
|
// // Act
|
||||||
await wrapper.vm.$nextTick();
|
// await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// Assert
|
// // Assert
|
||||||
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
|
// expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
|
||||||
});
|
// });
|
||||||
|
|
||||||
test("Should render correct display content", async () => {
|
test("Should render correct display content", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -82,10 +82,9 @@ describe("Customer Review Block", () => {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(wrapper.vm.displayContent).toEqual([
|
expect(wrapper.vm.displayContent).toEqual([
|
||||||
testConstants.displayContent.fullName,
|
|
||||||
testConstants.displayContent.emailAddress,
|
testConstants.displayContent.emailAddress,
|
||||||
testConstants.displayContent.phoneNumber,
|
testConstants.displayContent.phoneNumber,
|
||||||
testConstants.displayContent.smsOptIn,
|
// testConstants.displayContent.smsOptIn,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -94,11 +93,11 @@ function generateDefaultProps() {
|
||||||
return {
|
return {
|
||||||
cmsWidgetName: "CustomerWidget",
|
cmsWidgetName: "CustomerWidget",
|
||||||
customer: {
|
customer: {
|
||||||
firstName: testConstants.customer.firstName,
|
// firstName: testConstants.customer.firstName,
|
||||||
lastName: testConstants.customer.lastName,
|
// lastName: testConstants.customer.lastName,
|
||||||
phoneNumber: testConstants.customer.phoneNumber,
|
phoneNumber: testConstants.customer.phoneNumber,
|
||||||
emailAddress: testConstants.customer.emailAddress,
|
emailAddress: testConstants.customer.emailAddress,
|
||||||
isSmsOptIn: testConstants.customer.isSmsOptIn,
|
// isSmsOptIn: testConstants.customer.isSmsOptIn,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
displayContent() {
|
displayContent() {
|
||||||
return [this.fullName, this.email, this.phoneNumber, this.smsOptIn];
|
return [this.email, this.phoneNumber];
|
||||||
|
// return [this.fullName, this.email, this.phoneNumber, this.smsOptIn]; // prior to Heritage parity
|
||||||
},
|
},
|
||||||
header() {
|
header() {
|
||||||
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
|
||||||
|
import optInReview from "@/layouts/payment-method/review-dropdown/review-sections/opt-in-review/opt-in-review";
|
||||||
|
|
||||||
|
const testConstants = {
|
||||||
|
cms: {
|
||||||
|
header: {
|
||||||
|
text: "Text Message Updates",
|
||||||
|
},
|
||||||
|
bodyText: {
|
||||||
|
text: "We'll send appointment updates to {custom:SMSNUMBER}",
|
||||||
|
},
|
||||||
|
processedBodyText: {
|
||||||
|
text: "We'll send appointment updates to 111-111-1111",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
phoneNumber: "111-111-1111",
|
||||||
|
},
|
||||||
|
displayContent: {
|
||||||
|
processedBodyText: "We'll send appointment updates to 111-111-1111",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmsContent;
|
||||||
|
|
||||||
|
describe("Opt-In Review Block", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cmsContent = {
|
||||||
|
OptInWidget: {
|
||||||
|
HeaderText: testConstants.cms.header.text,
|
||||||
|
BodyText: testConstants.cms.bodyText.text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should display header text from cms", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should render correct display content with phone number replacement", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.displayContent).toEqual([testConstants.displayContent.processedBodyText]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should return customer phone number", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.phoneNumber).toEqual(testConstants.customer.phoneNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should emit edit-clicked when editClicked method is called", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.editClicked();
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted("edit-clicked")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should handle missing customer gracefully", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
props.customer = null;
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.phoneNumber).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateDefaultProps() {
|
||||||
|
return {
|
||||||
|
cmsWidgetName: "OptInWidget",
|
||||||
|
customer: {
|
||||||
|
phoneNumber: testConstants.customer.phoneNumber,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMocks(customMountOptions) {
|
||||||
|
const mountOptions = getMountOptions(customMountOptions);
|
||||||
|
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||||
|
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.global.mixins = [mockMixin];
|
||||||
|
|
||||||
|
const wrapper = shallowMount(optInReview, mountOptions);
|
||||||
|
wrapper.vm.setCmsContent = jest.fn();
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<reviewBlock :customHeaderText="header" :content="displayContent" @edit-clicked="editClicked" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import reviewBlock from "@/layouts/payment-method/review-dropdown/review-block/review-block";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "opt-in-review",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
customer: Object,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClicked() {
|
||||||
|
this.$emit("edit-clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayContent() {
|
||||||
|
return [
|
||||||
|
this.getCmsContent(this.cmsWidgetName, "BodyText")?.replaceAll(
|
||||||
|
"{custom:SMSNUMBER}",
|
||||||
|
this.phoneNumber
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
header() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||||
|
},
|
||||||
|
phoneNumber() {
|
||||||
|
return this.customer?.phoneNumber;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
reviewBlock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import { getDamageInfoWordingText } from "@/helpers/damage-helper";
|
||||||
|
|
||||||
|
import parityDamageReview from "@/layouts/payment-method/review-dropdown/review-sections/parity-damage-review/parity-damage-review";
|
||||||
|
|
||||||
|
// Mock the damage helper
|
||||||
|
jest.mock("@/helpers/damage-helper", () => ({
|
||||||
|
getDamageInfoWordingText: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const testConstants = {
|
||||||
|
cms: {
|
||||||
|
damageInfoHeader: {
|
||||||
|
text: "We'll replace your {custom:GLASSPARTS}",
|
||||||
|
},
|
||||||
|
processedHeader: {
|
||||||
|
text: "We'll replace your windshield and passenger side window",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vehicle: {
|
||||||
|
year: "2020",
|
||||||
|
make: "Honda",
|
||||||
|
model: "Civic",
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
glassType: "windshield",
|
||||||
|
location: "passenger",
|
||||||
|
},
|
||||||
|
damageInfoWordingText: "windshield and passenger side window",
|
||||||
|
displayContent: {
|
||||||
|
vehicleInfo: "2020 Honda Civic",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmsContent;
|
||||||
|
|
||||||
|
describe("Parity Damage Review Block", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cmsContent = {
|
||||||
|
DamageInfoHeaderWidget: {
|
||||||
|
Text: testConstants.cms.damageInfoHeader.text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock the damage helper function
|
||||||
|
getDamageInfoWordingText.mockReturnValue(testConstants.damageInfoWordingText);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should display processed damage info header text with custom replacement", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.damageInfoHeader).toEqual(testConstants.cms.processedHeader.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should render correct display content with vehicle information", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.displayContent).toEqual([testConstants.displayContent.vehicleInfo]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should call getDamageInfoWordingText with damage prop", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
const result = wrapper.vm.damageInfoWordingText;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(getDamageInfoWordingText).toHaveBeenCalledWith(testConstants.damage);
|
||||||
|
expect(result).toEqual(testConstants.damageInfoWordingText);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should emit edit-clicked when editClicked method is called", async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.editClicked();
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted("edit-clicked")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateDefaultProps() {
|
||||||
|
return {
|
||||||
|
cmsWidgetName: "DamageReviewWidget",
|
||||||
|
vehicle: {
|
||||||
|
year: testConstants.vehicle.year,
|
||||||
|
make: testConstants.vehicle.make,
|
||||||
|
model: testConstants.vehicle.model,
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
glassType: testConstants.damage.glassType,
|
||||||
|
location: testConstants.damage.location,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMocks(customMountOptions) {
|
||||||
|
const mountOptions = getMountOptions(customMountOptions);
|
||||||
|
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||||
|
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.global.mixins = [mockMixin];
|
||||||
|
|
||||||
|
const wrapper = shallowMount(parityDamageReview, mountOptions);
|
||||||
|
wrapper.vm.setCmsContent = jest.fn();
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<reviewBlock
|
||||||
|
:headerCmsWidgetName="cmsWidgetName"
|
||||||
|
:content="displayContent"
|
||||||
|
:customHeaderText="damageInfoHeader"
|
||||||
|
@edit-clicked="editClicked" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import reviewBlock from "@/layouts/payment-method/review-dropdown/review-block/review-block";
|
||||||
|
import { getDamageInfoWordingText } from "@/helpers/damage-helper";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle-review",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
vehicle: Object,
|
||||||
|
damage: Object,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClicked() {
|
||||||
|
this.$emit("edit-clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayContent() {
|
||||||
|
return [`${this.vehicle.year} ${this.vehicle.make} ${this.vehicle.model}`];
|
||||||
|
},
|
||||||
|
damageInfoWordingText() {
|
||||||
|
return getDamageInfoWordingText(this.damage);
|
||||||
|
},
|
||||||
|
damageInfoHeader() {
|
||||||
|
return this.getCmsContent("DamageInfoHeaderWidget", "Text")?.replaceAll(
|
||||||
|
"{custom:GLASSPARTS}",
|
||||||
|
this.damageInfoWordingText
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
reviewBlock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
:showInsuranceCoverageAs="showInsuranceCoverageAs"
|
:showInsuranceCoverageAs="showInsuranceCoverageAs"
|
||||||
:isItac="isItac"
|
:isItac="isItac"
|
||||||
:isNoComp="isNoComp"
|
:isNoComp="isNoComp"
|
||||||
:isExpandedOnLoad="true" />
|
:isCollapsible="false" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="showSwitchPaymentMethod"
|
v-if="showSwitchPaymentMethod"
|
||||||
|
|
@ -919,10 +919,6 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 650px;
|
min-height: 650px;
|
||||||
border: 0 none;
|
border: 0 none;
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
|
||||||
min-height: 1688px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#pageLoadingModal {
|
#pageLoadingModal {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
|
|
@ -936,16 +932,18 @@ export default {
|
||||||
}
|
}
|
||||||
& > #cart-container {
|
& > #cart-container {
|
||||||
flex-basis: 36%;
|
flex-basis: 36%;
|
||||||
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
@include media-breakpoint-down(lg) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
& > iframe {
|
& > iframe {
|
||||||
flex-basis: 100%;
|
flex-basis: auto;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
& > #cart-container {
|
& > #cart-container {
|
||||||
flex-basis: 100%;
|
flex-basis: auto;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
|
||||||
& > button#submit-payment {
|
& > button#submit-payment {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
<span v-else>{{ getInlineAltText(token) }}</span>
|
<span v-else>{{ getInlineAltText(token) }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else v-html="token"></span>
|
<span v-else v-html="token"></span>
|
||||||
|
<span class="nbsp"> </span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
id="afterpay-learnmore"
|
id="afterpay-learnmore"
|
||||||
href="#"
|
href="#"
|
||||||
|
|
@ -139,7 +139,7 @@ export default {
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
|
@ -170,4 +170,8 @@ export default {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#afterpay-learnmore {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
alertClass="alert-info" />
|
alertClass="alert-info" />
|
||||||
|
|
||||||
<promoModalQuestion
|
<promoModalQuestion
|
||||||
class="mt-6 d-flex"
|
class="mt-4 d-flex"
|
||||||
v-model="lineItems"
|
v-model="lineItems"
|
||||||
:addableVaps="addableVaps"
|
:addableVaps="addableVaps"
|
||||||
pageName="quote"
|
pageName="quote"
|
||||||
|
|
@ -67,11 +67,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8 col-xl-6 nav-container">
|
<div class="col-md-8 col-xl-6">
|
||||||
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
|
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
|
||||||
<contentGroupModal ref="FrontWiperModal" cmsWidgetName="FrontWiperModal" />
|
<contentGroupModal ref="FrontWiperModal" cmsWidgetName="FrontWiperModal" />
|
||||||
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
|
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
|
||||||
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" isRecal />
|
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
|
||||||
|
|
||||||
<navbar
|
<navbar
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
|
@ -902,21 +902,19 @@ export default {
|
||||||
.quote {
|
.quote {
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
|
|
||||||
.nav-container {
|
|
||||||
/* Create correct positioning point for .nav-bar a.navigation-link */
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.nav-bar) {
|
:deep(.nav-bar) {
|
||||||
position: initial;
|
position: initial;
|
||||||
button.btn {
|
|
||||||
width: 100%;
|
@media only screen and (min-width: 340px) {
|
||||||
justify-content: center;
|
.col button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
a.navigation-link {
|
a.navigation-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
bottom: -1rem;
|
bottom: 1rem;
|
||||||
}
|
}
|
||||||
.col-auto {
|
.col-auto {
|
||||||
width: -webkit-fill-available;
|
width: -webkit-fill-available;
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.start-over {
|
.start-over {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
a {
|
a {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $black;
|
color: $black;
|
||||||
|
|
@ -114,7 +115,7 @@ export default {
|
||||||
}
|
}
|
||||||
.return-user {
|
.return-user {
|
||||||
.return-user-spacing {
|
.return-user-spacing {
|
||||||
margin-top: 14rem;
|
margin-top: 8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:deep(.menu-modal-container) {
|
:deep(.menu-modal-container) {
|
||||||
|
|
|
||||||
114
src/layouts/schedule/duration-text-block/duration-text-block.vue
Normal file
114
src/layouts/schedule/duration-text-block/duration-text-block.vue
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<template>
|
||||||
|
<textBlock
|
||||||
|
:customText="durationTextBlockCopy"
|
||||||
|
typeStyle="small"
|
||||||
|
marginTopSizeOverride="0"
|
||||||
|
class="duration-text-block" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Components
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
import { AppointmentTypeStrings, cmsWidgetFieldMappings } from "@/constants/schedule-constants";
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "duration-text-block",
|
||||||
|
props: {
|
||||||
|
isSameDay: Boolean,
|
||||||
|
appointmentType: String,
|
||||||
|
estimatedServiceMinutesMinimum: String,
|
||||||
|
estimatedServiceMinutesMaximum: String,
|
||||||
|
isOvernightDropoff: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedRouteCode: null,
|
||||||
|
selectedAnswerForDropOffOrInshop: null,
|
||||||
|
selectedAnswerForTimeSlots: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
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.estimatedServiceMinutesMinimum,
|
||||||
|
this.estimatedServiceMinutesMaximum
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
|
||||||
|
return `${inshopDurationTextWithoutTime} <b>${inshopDurationTime}</b>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
mobileDurationText() {
|
||||||
|
const mobileDurationTextWithoutTime = this.getCmsContent(
|
||||||
|
"TimeSlotModalQuestion",
|
||||||
|
cmsWidgetFieldMappings.DURATION
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobileDurationTime = getDisplayTextForDurationLength(
|
||||||
|
this.estimatedServiceMinutesMinimum,
|
||||||
|
this.estimatedServiceMinutesMaximum
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
|
||||||
|
return `${mobileDurationTextWithoutTime} ${mobileDurationTime}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
durationTextBlockCopy() {
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
return this.mobileDurationText;
|
||||||
|
} else if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||||
|
return this.getDurationTextBlockCopyForDropoff();
|
||||||
|
} else {
|
||||||
|
return this.inshopDurationText;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getDurationTextBlockCopyForDropoff() {
|
||||||
|
if (this.isOvernightDropoff) {
|
||||||
|
return this.overnightDropoffDurationText;
|
||||||
|
} else {
|
||||||
|
if (this.isSameDay) {
|
||||||
|
return this.sameDayDropoffDurationText;
|
||||||
|
} else {
|
||||||
|
return this.dropOffDurationText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
textBlock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="mobile-fee-waiver-alert-container py-3 px-4 my-2"
|
||||||
|
:class="[isExpanded ? 'expanded' : '']">
|
||||||
|
<div
|
||||||
|
class="mobile-fee-waiver-header d-flex align-items-center justify-content-between"
|
||||||
|
@click="toggleIsExpanded">
|
||||||
|
<span>
|
||||||
|
{{ headerText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-fee-waiver-body">
|
||||||
|
<div class="mobile-fee-waiver-copy">
|
||||||
|
{{ subheaderText }}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-fee-waiver-price-comp">
|
||||||
|
<span class="mobile-fee-waiver-strikethrough-price">
|
||||||
|
{{ priceStrikethroughText }}
|
||||||
|
</span>
|
||||||
|
<span class="mobile-fee-waiver-free-price">{{ freePriceText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "mobile-fee-waiver-alert",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isExpanded: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
headerText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||||
|
},
|
||||||
|
subheaderText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
|
||||||
|
},
|
||||||
|
priceStrikethroughText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||||
|
},
|
||||||
|
freePriceText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "FooterText2");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleIsExpanded() {
|
||||||
|
this.setIsExpanded(!this.isExpanded);
|
||||||
|
},
|
||||||
|
setIsExpanded(newVal) {
|
||||||
|
this.isExpanded = newVal;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.mobile-fee-waiver-alert-container {
|
||||||
|
background: $green-100;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
border: 1px solid $green-400;
|
||||||
|
|
||||||
|
.mobile-fee-waiver-header {
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex-grow: 1;
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath fill='%230C7E47' d='M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0Zm3.7 6.171-4.449 4.45a.559.559 0 0 1-.8 0l-2.16-2.16a.563.563 0 0 1 .792-.8l1.76 1.76 4.066-4.042a.563.563 0 1 1 .792.8v-.008Z'/%3E%3C/svg%3E");
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 9'%3E%3Cpath d='M7.99282 0.117702C7.75716 0.116049 7.53044 0.207797 7.3623 0.37286L0.255553 7.46878C0.0862976 7.63796 -0.00878906 7.86742 -0.00878906 8.10667C-0.00878906 8.34593 0.0862976 8.57539 0.255553 8.74457C0.424808 8.91374 0.654368 9.00879 0.893731 9.00879C1.13309 9.00879 1.36265 8.91374 1.53191 8.74457L7.99282 2.27378L14.4614 8.74457C14.6307 8.91205 14.8595 9.00547 15.0977 9.00428C15.3359 9.00308 15.5638 8.90737 15.7314 8.73819C15.8989 8.56901 15.9924 8.34022 15.9912 8.10216C15.99 7.8641 15.8942 7.63627 15.725 7.46878L8.6259 0.377963C8.45765 0.21088 8.22999 0.117289 7.99282 0.117702Z' fill='%230C7E47'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-fee-waiver-body {
|
||||||
|
transition: all 500ms ease-out;
|
||||||
|
visibility: hidden;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
|
||||||
|
.mobile-fee-waiver-price-comp {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
.mobile-fee-waiver-strikethrough-price {
|
||||||
|
font-size: smaller;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-fee-waiver-free-price {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bolder;
|
||||||
|
border-radius: 1em;
|
||||||
|
background-color: $green-300;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
margin-inline-start: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
.mobile-fee-waiver-header {
|
||||||
|
&::after {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-fee-waiver-body {
|
||||||
|
visibility: visible;
|
||||||
|
max-height: none;
|
||||||
|
|
||||||
|
border-top: 1px solid $green-400;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,17 +5,14 @@
|
||||||
<div class="container page-container-grouped-styles">
|
<div class="container page-container-grouped-styles">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
<alert
|
<mobileFeeWaiverAlert cmsWidgetName="MobileFeeFreeDropdownWidget" />
|
||||||
ref="alertMobileFeeFree"
|
|
||||||
class="mb-5"
|
|
||||||
cmsWidgetName="AlertMobileFeeFreeWidget"
|
|
||||||
v-if="showMobileFreeAlert"
|
|
||||||
alertClass="alert-success" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row appointment-type">
|
<div class="row appointment-type">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
<funnelSubHeader cmsWidgetName="ScheduleYourServiceWidget" class="mb-5" />
|
<funnelSubHeader
|
||||||
|
cmsWidgetName="ScheduleYourServiceWidget"
|
||||||
|
id="schedule-your-service" />
|
||||||
<appointmentTypeQuestion
|
<appointmentTypeQuestion
|
||||||
v-model="appointmentTypeFromAppointmentTypeQuestion"
|
v-model="appointmentTypeFromAppointmentTypeQuestion"
|
||||||
v-show="isAppointmentTypeDisplayed"
|
v-show="isAppointmentTypeDisplayed"
|
||||||
|
|
@ -118,6 +115,15 @@
|
||||||
<div class="row date-picker-wrapper">
|
<div class="row date-picker-wrapper">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
||||||
|
<div v-show="appointmentType" class="date-picker-header fw-normal mt-5 mb-5">
|
||||||
|
<textBlock :customText="datePickerHeaderText" typeStyle="button-question" />
|
||||||
|
<durationTextBlock
|
||||||
|
:isSameDay="isSameDay"
|
||||||
|
:appointmentType="appointmentType"
|
||||||
|
:estimatedServiceMinutesMinimum="estimatedServiceMinutesMinimum"
|
||||||
|
:estimatedServiceMinutesMaximum="estimatedServiceMinutesMaximum"
|
||||||
|
:isOvernightDropoff="isOvernightDropoff" />
|
||||||
|
</div>
|
||||||
<datePicker
|
<datePicker
|
||||||
:currentZip="zipCode"
|
:currentZip="zipCode"
|
||||||
:currentProviderNumber="selectedProvider?.providerNumber"
|
:currentProviderNumber="selectedProvider?.providerNumber"
|
||||||
|
|
@ -134,14 +140,25 @@
|
||||||
:pricingByDayBasePrice="pricingByDayBasePrice"
|
:pricingByDayBasePrice="pricingByDayBasePrice"
|
||||||
:pricingByDayUpcharge="pricingByDayUpcharge"
|
:pricingByDayUpcharge="pricingByDayUpcharge"
|
||||||
:showPricingByDay="showPricingByDay"
|
:showPricingByDay="showPricingByDay"
|
||||||
:isPricingByDayExperiment="isPricingByDayExperiment"
|
:isPricingByDayExperiment="isPricingByDayExperiment" />
|
||||||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
<timeSlotQuestion
|
||||||
|
ref="timeSlotModalQuestion"
|
||||||
|
v-if="showTimeSlotQuestion"
|
||||||
|
@time-slot-selection-changed="updateTimeSlot"
|
||||||
|
waitListLabelWidget="WaitListLabelWidget"
|
||||||
|
waitListQuestionWidget="WaitListQuestionWidget"
|
||||||
|
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
|
||||||
|
mobileCmsWidgetName="MobileTimeSlotModal"
|
||||||
|
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
||||||
|
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
|
||||||
|
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
|
||||||
|
dropOffOrPickATimeQuestionCmsWidgetName="DropOffOrPickATimeQuestionWidget"
|
||||||
:appointmentType="appointmentType"
|
:appointmentType="appointmentType"
|
||||||
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
||||||
:estimatedServiceMinutesMinimum="getServiceMinutesMin"
|
|
||||||
:estimatedServiceMinutesMaximum="getServiceMinutesMax"
|
|
||||||
@timeSlotSelected="updateTimeSlot"
|
|
||||||
:displayWaitList="displayWaitList"
|
:displayWaitList="displayWaitList"
|
||||||
|
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
||||||
|
:isSameDay="isSameDay"
|
||||||
|
:selectedRouteCodeData="selectedRouteCodeData"
|
||||||
@waitListRequested="handleWaitListRequested" />
|
@waitListRequested="handleWaitListRequested" />
|
||||||
<navbar
|
<navbar
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
|
@ -160,9 +177,10 @@
|
||||||
import alert from "@/ux-components/alert/alert";
|
import alert from "@/ux-components/alert/alert";
|
||||||
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
|
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
|
||||||
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
|
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
|
||||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
|
||||||
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
|
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
|
||||||
import shopLocation from "@/layouts/schedule/shop-location/shop-location";
|
import shopLocation from "@/layouts/schedule/shop-location/shop-location";
|
||||||
|
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
||||||
|
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
|
||||||
|
|
||||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||||
|
|
@ -174,6 +192,7 @@ import datePicker from "@/digital-components/date-picker/date-picker";
|
||||||
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
||||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||||
import textBlock from "@/digital-components/text-block/text-block";
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
import mobileFeeWaiverAlert from "./mobile-fee-waiver-alert/mobile-fee-waiver-alert.vue";
|
||||||
|
|
||||||
// Supporting files
|
// Supporting files
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
|
|
@ -626,6 +645,9 @@ export default {
|
||||||
return this.isGlassServiceableMobile;
|
return this.isGlassServiceableMobile;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showTimeSlotQuestion() {
|
||||||
|
return this.selectedDate && this.appointmentTypeFromAppointmentTypeQuestion;
|
||||||
|
},
|
||||||
isMobileStaticRecalibrationApplicable() {
|
isMobileStaticRecalibrationApplicable() {
|
||||||
return (
|
return (
|
||||||
this.displayMSR &&
|
this.displayMSR &&
|
||||||
|
|
@ -785,6 +807,21 @@ export default {
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
selectedRouteCodeData() {
|
||||||
|
if (!this.selectedTimeSlotInfo?.timeSlot?.routeCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
routeCode: this.selectedTimeSlotInfo.timeSlot.routeCode,
|
||||||
|
isPremiumAppointment: this.selectedTimeSlotInfo.isPremiumAppointment,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
questionText() {
|
||||||
|
return this.getCmsContent("YourSafeliteShopWidget", "QuestionText");
|
||||||
|
},
|
||||||
|
datePickerHeaderText() {
|
||||||
|
return this.getCmsContent("DatePickerSubHeaderWidget", "HeaderText");
|
||||||
|
},
|
||||||
appointmentTypeStrings() {
|
appointmentTypeStrings() {
|
||||||
return AppointmentTypeStrings;
|
return AppointmentTypeStrings;
|
||||||
},
|
},
|
||||||
|
|
@ -795,12 +832,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
|
// 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);
|
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
|
||||||
},
|
},
|
||||||
getServiceMinutesMin() {
|
estimatedServiceMinutesMinimum() {
|
||||||
return this.isMobileSelected
|
return this.isMobileSelected
|
||||||
? this.selectableDatesMobile.estimatedServiceMinutesMinimum
|
? this.selectableDatesMobile.estimatedServiceMinutesMinimum
|
||||||
: this.selectableDatesInshop.estimatedServiceMinutesMinimum;
|
: this.selectableDatesInshop.estimatedServiceMinutesMinimum;
|
||||||
},
|
},
|
||||||
getServiceMinutesMax() {
|
estimatedServiceMinutesMaximum() {
|
||||||
return this.isMobileSelected
|
return this.isMobileSelected
|
||||||
? this.selectableDatesMobile.estimatedServiceMinutesMaximum
|
? this.selectableDatesMobile.estimatedServiceMinutesMaximum
|
||||||
: this.selectableDatesInshop.estimatedServiceMinutesMaximum;
|
: this.selectableDatesInshop.estimatedServiceMinutesMaximum;
|
||||||
|
|
@ -820,6 +857,18 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isSameDay() {
|
||||||
|
const todaysDate = new Date().toISOString().split("T")[0];
|
||||||
|
return this.selectedDate === todaysDate;
|
||||||
|
},
|
||||||
|
isOvernightDropoff() {
|
||||||
|
return (
|
||||||
|
this.selectedTimeSlotInfo?.timeSlot?.routeCode &&
|
||||||
|
this.selectedTimeSlotInfo.timeSlot.routeCode.includes(
|
||||||
|
RouteCodeFlags.OVERNIGHT_DROP_OFF
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
splitCopyOnCMSPlaceHolder,
|
splitCopyOnCMSPlaceHolder,
|
||||||
|
|
@ -1238,7 +1287,9 @@ export default {
|
||||||
return store.getters.lineItems.supportingItems;
|
return store.getters.lineItems.supportingItems;
|
||||||
},
|
},
|
||||||
updateFooterButtonText(timeSlotInfo) {
|
updateFooterButtonText(timeSlotInfo) {
|
||||||
let navbarButtonText;
|
let navbarButtonText = "Continue";
|
||||||
|
// Archiving the following in case we revert back from Heritage parity
|
||||||
|
/*
|
||||||
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
|
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
|
||||||
navbarButtonText = "Continue";
|
navbarButtonText = "Continue";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1273,6 +1324,7 @@ export default {
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
this.$refs.navbar.updateButtonText(navbarButtonText);
|
this.$refs.navbar.updateButtonText(navbarButtonText);
|
||||||
},
|
},
|
||||||
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
||||||
|
|
@ -1555,22 +1607,46 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateTimeSlot(timeSlotObj) {
|
updateTimeSlot(timeSlotObj) {
|
||||||
if (this.preSelectedDate && !timeSlotObj?.timeSlot?.date) return;
|
if (!timeSlotObj?.routeCode) {
|
||||||
this.selectedTimeSlotInfo = timeSlotObj;
|
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
|
||||||
|
this.selectedTimeSlotInfo = {
|
||||||
|
timeSlot: {
|
||||||
|
date: null,
|
||||||
|
routeCode: null,
|
||||||
|
startTime: null,
|
||||||
|
endTime: null,
|
||||||
|
jobMaxMinutes: null,
|
||||||
|
jobMinMinutes: null,
|
||||||
|
},
|
||||||
|
isPremiumAppointment: null,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
|
||||||
|
(slot) => slot.id == timeSlotObj.routeCode
|
||||||
|
);
|
||||||
|
|
||||||
|
this.selectedTimeSlotInfo = {
|
||||||
|
timeSlot: {
|
||||||
|
date: this.timeSlotsForSelectedDate.date,
|
||||||
|
routeCode: timeSlotObj.routeCode,
|
||||||
|
startTime: timeSlot.startTime,
|
||||||
|
endTime: timeSlot.endTime,
|
||||||
|
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
|
||||||
|
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(),
|
||||||
|
},
|
||||||
|
isPremiumAppointment: timeSlotObj.isPremiumAppointment ? true : false,
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.appointmentType !== AppointmentTypeStrings.MOBILE &&
|
this.appointmentType !== AppointmentTypeStrings.MOBILE &&
|
||||||
this.appointmentType !== null
|
this.appointmentType !== null
|
||||||
) {
|
) {
|
||||||
// update apptType based on routeCode to determine if it should be dropoff or inshop
|
// update apptType based on routeCode to determine if it should be dropoff or inshop
|
||||||
debugLog(`Updating appointment type based on route code.`);
|
debugLog(`Updating appointment type based on route code.`);
|
||||||
debugLog(`Route code =`, timeSlotObj.timeSlot.routeCode);
|
debugLog(`Route code =`, timeSlotObj.routeCode);
|
||||||
debugLog(
|
debugLog(`Parsed type = `, this.getInShopOrDropOffApptType(timeSlotObj.routeCode));
|
||||||
`Parsed type = `,
|
this.appointmentType = this.getInShopOrDropOffApptType(timeSlotObj.routeCode);
|
||||||
this.getInShopOrDropOffApptType(timeSlotObj.timeSlot.routeCode)
|
|
||||||
);
|
|
||||||
this.appointmentType = this.getInShopOrDropOffApptType(
|
|
||||||
timeSlotObj.timeSlot.routeCode
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getInShopOrDropOffApptType(routeCode) {
|
getInShopOrDropOffApptType(routeCode) {
|
||||||
|
|
@ -1618,7 +1694,7 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
!this.selectedProvider?.address?.streetAddress &&
|
(!this.selectedProvider?.address?.streetAddress || this.isVehicleHeavyTruck) &&
|
||||||
this.shopProviderData?.shopProviders?.length
|
this.shopProviderData?.shopProviders?.length
|
||||||
) {
|
) {
|
||||||
this.selectedProvider = this.shopProviderData.shopProviders[0];
|
this.selectedProvider = this.shopProviderData.shopProviders[0];
|
||||||
|
|
@ -1737,12 +1813,15 @@ export default {
|
||||||
datePicker,
|
datePicker,
|
||||||
locationAlerts,
|
locationAlerts,
|
||||||
textBlock,
|
textBlock,
|
||||||
|
timeSlotQuestion,
|
||||||
|
|
||||||
alert,
|
alert,
|
||||||
serviceZipModalQuestion,
|
serviceZipModalQuestion,
|
||||||
appointmentTypeQuestion,
|
appointmentTypeQuestion,
|
||||||
contentGroupModal,
|
contentGroupModal,
|
||||||
|
durationTextBlock,
|
||||||
shopLocation,
|
shopLocation,
|
||||||
|
mobileFeeWaiverAlert,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1777,7 +1856,7 @@ export default {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.funnel-sub-header:not(.mb-5)) {
|
:deep(.funnel-sub-header:not(#schedule-your-service)) {
|
||||||
h5.dark-header {
|
h5.dark-header {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
@ -1791,7 +1870,7 @@ export default {
|
||||||
text-underline-offset: 4px;
|
text-underline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-block.duration-text-block b {
|
:deep(.text-block.duration-text-block b) {
|
||||||
font-family: UrbanistSemibold;
|
font-family: UrbanistSemibold;
|
||||||
}
|
}
|
||||||
:deep(.alert-warning.widget-name-AlertRecalNoMobileWidget .alert-heading),
|
:deep(.alert-warning.widget-name-AlertRecalNoMobileWidget .alert-heading),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<buttonQuestion
|
<buttonQuestion
|
||||||
v-if="isDropOffAppointmentAvailable"
|
v-if="isDropOffAppointmentAvailable"
|
||||||
v-model="selectedAnswerForDropOffOrInshop"
|
v-model="selectedAnswerForDropOffOrInshop"
|
||||||
|
@update:modelValue="dropOffSelectionChanged"
|
||||||
:questionText="dropOffOrPickATimeQuestionLabelText"
|
:questionText="dropOffOrPickATimeQuestionLabelText"
|
||||||
:questionTextDescription="dropOffOrPickATimeQuestionDescription"
|
:questionTextDescription="dropOffOrPickATimeQuestionDescription"
|
||||||
:answers="answersForDropOffQuestion"
|
:answers="answersForDropOffQuestion"
|
||||||
|
|
@ -15,7 +16,7 @@
|
||||||
<alert
|
<alert
|
||||||
v-if="shouldDisplayDropOffAlert"
|
v-if="shouldDisplayDropOffAlert"
|
||||||
ref="alertDropoffInformation"
|
ref="alertDropoffInformation"
|
||||||
class="mb-4 drop-off-alert"
|
class="mt-4 mb-4 drop-off-alert"
|
||||||
:cmsWidgetName="dropOffTimeSlotAlertCMSWidgetName"
|
:cmsWidgetName="dropOffTimeSlotAlertCMSWidgetName"
|
||||||
alertClass="alert-info"
|
alertClass="alert-info"
|
||||||
v-bind:isDismissible="false" />
|
v-bind:isDismissible="false" />
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
<buttonQuestion
|
<buttonQuestion
|
||||||
ref="buttonQuestion"
|
ref="buttonQuestion"
|
||||||
v-if="shouldDisplayTimeSlotQuestion"
|
v-if="shouldDisplayTimeSlotQuestion"
|
||||||
|
@update:modelValue="timeSlotSelectionChanged"
|
||||||
buttonTypeString="timeSlotModalListButton"
|
buttonTypeString="timeSlotModalListButton"
|
||||||
:buttonTypeObject="timeSlotModalListButton"
|
:buttonTypeObject="timeSlotModalListButton"
|
||||||
class="mt-4 timeslots"
|
class="mt-4 timeslots"
|
||||||
|
|
@ -44,7 +46,7 @@
|
||||||
v-if="supplementalInformationBlock"
|
v-if="supplementalInformationBlock"
|
||||||
v-html="supplementalInformationBlock" />
|
v-html="supplementalInformationBlock" />
|
||||||
<div
|
<div
|
||||||
v-if="hasWaitListExperiment && displayWaitListFeature && selectedDate"
|
v-if="hasWaitListExperiment && displayWaitListFeature"
|
||||||
class="mt-5 bg-light rounded waitlist">
|
class="mt-5 bg-light rounded waitlist">
|
||||||
<textBlock
|
<textBlock
|
||||||
cmsWidgetName="WaitListLabelWidget"
|
cmsWidgetName="WaitListLabelWidget"
|
||||||
|
|
@ -59,7 +61,7 @@
|
||||||
@click="waitListChecked" />
|
@click="waitListChecked" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="waitListRequested && displayWaitList && selectedDate"
|
v-if="waitListRequested && displayWaitList"
|
||||||
class="rounded waitlist-success"
|
class="rounded waitlist-success"
|
||||||
ref="waitlistSuccessMessage">
|
ref="waitlistSuccessMessage">
|
||||||
<img :src="waitListSuccessImage" class="success-image" />
|
<img :src="waitListSuccessImage" class="success-image" />
|
||||||
|
|
@ -92,7 +94,6 @@ import {
|
||||||
isDropOffRouteCode,
|
isDropOffRouteCode,
|
||||||
militaryToTwelveHourTime,
|
militaryToTwelveHourTime,
|
||||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||||
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
import { defineRule, useField } from "vee-validate";
|
import { defineRule, useField } from "vee-validate";
|
||||||
|
|
@ -116,23 +117,8 @@ defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "time-slot-question",
|
name: "time-slot-question",
|
||||||
emits: ["update:modelValue", "TimeSlotSelected", "click-event"],
|
emits: ["update:modelValue", "time-slot-selection-changed"],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
timeSlot: {
|
|
||||||
routeCode: null,
|
|
||||||
date: null,
|
|
||||||
startTime: null,
|
|
||||||
endTime: null,
|
|
||||||
jobMaxMinutes: null,
|
|
||||||
jobMinMinutes: null,
|
|
||||||
},
|
|
||||||
isPremiumAppointment: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
cmsWidgetName: String,
|
|
||||||
mobileCmsWidgetName: String,
|
mobileCmsWidgetName: String,
|
||||||
mobilePremiumCmsWidgetName: String,
|
mobilePremiumCmsWidgetName: String,
|
||||||
dropoffCmsWidgetName: String,
|
dropoffCmsWidgetName: String,
|
||||||
|
|
@ -142,21 +128,23 @@ export default {
|
||||||
appointmentType: String,
|
appointmentType: String,
|
||||||
timeSlotsForSelectedDate: Object,
|
timeSlotsForSelectedDate: Object,
|
||||||
premiumAppointmentFee: Object,
|
premiumAppointmentFee: Object,
|
||||||
estimatedServiceMinutesMinimum: Number,
|
|
||||||
estimatedServiceMinutesMaximum: Number,
|
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
customComponentId: String,
|
customComponentId: String,
|
||||||
selectedDate: String,
|
isSameDay: Boolean,
|
||||||
displayWaitList: Boolean,
|
displayWaitList: Boolean,
|
||||||
|
selectedRouteCodeData: Object,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedRouteCode: null,
|
|
||||||
timeSlotModalListButton: timeSlotModalListButton,
|
timeSlotModalListButton: timeSlotModalListButton,
|
||||||
selectedAnswerForDropOffOrInshop: null,
|
|
||||||
selectedAnswerForTimeSlots: null,
|
selectedAnswerForTimeSlots: null,
|
||||||
|
selectedAnswerForDropOffOrInshop: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(this.selectedRouteCodeData);
|
||||||
|
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
||||||
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
const componentId = !props.customComponentId
|
const componentId = !props.customComponentId
|
||||||
|
|
@ -186,20 +174,33 @@ export default {
|
||||||
errors,
|
errors,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
waitListRequested(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToSuccessMessage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedRouteCodeData(newValue) {
|
||||||
|
this.updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(newValue);
|
||||||
|
},
|
||||||
|
timeSlotsForSelectedDate() {
|
||||||
|
this.selectedAnswerForDropOffOrInshop = null;
|
||||||
|
this.selectedAnswerForTimeSlots = null;
|
||||||
|
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
supplementalInformationBlock() {
|
supplementalInformationBlock() {
|
||||||
let appointmentTypeCmsWidgetName;
|
let appointmentTypeCmsWidgetName;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.selectedDate == null ||
|
|
||||||
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
|
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
|
||||||
this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
|
this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
|
||||||
this.appointmentType === AppointmentTypeStrings.DROP_OFF ||
|
this.appointmentType === AppointmentTypeStrings.DROP_OFF ||
|
||||||
!this.availableTimeSlots
|
!this.availableTimeSlots
|
||||||
) {
|
) {
|
||||||
if (!this.selectedAnswerForTimeSlots && !this.selectedAnswerForDropOffOrInshop) {
|
|
||||||
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
// This is planned to be used again for drop-off
|
// This is planned to be used again for drop-off
|
||||||
// Wrote this to be ready for that eventuality, is untested and no HTML work done yet
|
// Wrote this to be ready for that eventuality, is untested and no HTML work done yet
|
||||||
|
|
@ -214,11 +215,7 @@ export default {
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
} else {
|
} else {
|
||||||
if (!this.selectedAnswerForTimeSlots && this.selectedRouteCode) {
|
appointmentTypeCmsWidgetName = this.selectedRouteCodeData?.routeCode?.includes(
|
||||||
// Clearing selectedRouteCode if no time slot is selected
|
|
||||||
this.resetSelectedTimeSlot();
|
|
||||||
}
|
|
||||||
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(
|
|
||||||
PREMIUM_TIME_SLOT_ID_FLAG
|
PREMIUM_TIME_SLOT_ID_FLAG
|
||||||
)
|
)
|
||||||
? this.mobilePremiumCmsWidgetName
|
? this.mobilePremiumCmsWidgetName
|
||||||
|
|
@ -299,13 +296,6 @@ export default {
|
||||||
// return null;
|
// return null;
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
isSameDay() {
|
|
||||||
if (!this.timeSlotsForSelectedDate) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const todaysDate = new Date().toISOString().split("T")[0];
|
|
||||||
return this.selectedDate === todaysDate;
|
|
||||||
},
|
|
||||||
availableTimeSlots() {
|
availableTimeSlots() {
|
||||||
if (!this.timeSlotsForSelectedDate) {
|
if (!this.timeSlotsForSelectedDate) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -407,29 +397,60 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initializeComponent() {
|
updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(selectedRouteCodeData) {
|
||||||
this.setSelectedRouteCodeFromParent();
|
if (selectedRouteCodeData?.routeCode) {
|
||||||
},
|
let routeCode = selectedRouteCodeData.routeCode;
|
||||||
async setSelectedTimeSlot() {
|
if (isDropOffRouteCode(routeCode)) {
|
||||||
this.selectedRouteCode =
|
this.selectedAnswerForDropOffOrInshop = routeCode;
|
||||||
this.selectedAnswerForTimeSlots || this.selectedAnswerForDropOffOrInshop;
|
} else {
|
||||||
this.$emit(
|
this.selectedAnswerForDropOffOrInshop = PICK_A_TIME_BUTTON_VALUE;
|
||||||
"update:modelValue",
|
if (this.selectedRouteCodeData.isPremiumAppointment) {
|
||||||
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode)
|
routeCode = this.addPremiumFlagToInput(routeCode);
|
||||||
);
|
}
|
||||||
},
|
this.selectedAnswerForTimeSlots = routeCode;
|
||||||
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
}
|
||||||
selectedRouteCode,
|
} else {
|
||||||
isSameDayRelevant = false
|
this.selectedAnswerForDropOffOrInshop = null;
|
||||||
) {
|
this.selectedAnswerForTimeSlots = null;
|
||||||
if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
|
||||||
return this.overnightDropOffCmsWidgetName;
|
|
||||||
} else if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
|
||||||
return this.isSameDay && isSameDayRelevant
|
|
||||||
? this.sameDayDropOffCmsWidgetName
|
|
||||||
: this.dropoffCmsWidgetName;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dropOffSelectionChanged(newValue) {
|
||||||
|
let newSelectedRouteCodeData = null;
|
||||||
|
if (newValue && newValue != PICK_A_TIME_BUTTON_VALUE) {
|
||||||
|
newSelectedRouteCodeData = {
|
||||||
|
routeCode: newValue,
|
||||||
|
isPremiumAppointment: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.selectedAnswerForTimeSlots = null;
|
||||||
|
this.$emit("time-slot-selection-changed", newSelectedRouteCodeData);
|
||||||
|
},
|
||||||
|
timeSlotSelectionChanged(newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
let routeCode = newValue;
|
||||||
|
let isPremiumAppointment = routeCode.includes(PREMIUM_TIME_SLOT_ID_FLAG);
|
||||||
|
if (isPremiumAppointment) {
|
||||||
|
routeCode = this.removePremiumFlagFromInput(routeCode);
|
||||||
|
}
|
||||||
|
this.$emit("time-slot-selection-changed", {
|
||||||
|
routeCode: routeCode,
|
||||||
|
isPremiumAppointment: isPremiumAppointment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// This is unused but planned to be used again
|
||||||
|
// getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||||
|
// selectedRouteCode,
|
||||||
|
// isSameDayRelevant = false
|
||||||
|
// ) {
|
||||||
|
// if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||||
|
// return this.overnightDropOffCmsWidgetName;
|
||||||
|
// } else if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||||
|
// return this.isSameDay && isSameDayRelevant
|
||||||
|
// ? this.sameDayDropOffCmsWidgetName
|
||||||
|
// : this.dropoffCmsWidgetName;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
|
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
|
||||||
return timeSlotsForSelectedDate
|
return timeSlotsForSelectedDate
|
||||||
.filter((timeSlot) => {
|
.filter((timeSlot) => {
|
||||||
|
|
@ -463,8 +484,6 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resetSelectedTimeSlotIfDateChange(timeSlotsForSelectedDate);
|
|
||||||
|
|
||||||
return availableTimeSlots;
|
return availableTimeSlots;
|
||||||
},
|
},
|
||||||
getPremiumAppointmentTimeSlot(timeSlotData) {
|
getPremiumAppointmentTimeSlot(timeSlotData) {
|
||||||
|
|
@ -480,35 +499,19 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
setSelectedRouteCodeFromParent() {
|
|
||||||
if (!this.modelValue?.timeSlot?.routeCode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const routeCodeFromParent = this.modelValue.timeSlot.routeCode;
|
|
||||||
if (this.modelValue?.isPremiumAppointment) {
|
|
||||||
// Mobile Only
|
|
||||||
this.selectedAnswerForTimeSlots = this.addPremiumFlagToInput(routeCodeFromParent);
|
|
||||||
} else {
|
|
||||||
if (isDropOffRouteCode(routeCodeFromParent)) {
|
|
||||||
this.selectedAnswerForDropOffOrInshop = routeCodeFromParent;
|
|
||||||
} else {
|
|
||||||
this.selectedAnswerForDropOffOrInshop = PICK_A_TIME_BUTTON_VALUE;
|
|
||||||
this.selectedAnswerForTimeSlots = routeCodeFromParent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getWaitListRequestedFromStore() {
|
getWaitListRequestedFromStore() {
|
||||||
return store.getters.order.customer?.waitListRequested;
|
return store.getters.order.customer?.waitListRequested;
|
||||||
},
|
},
|
||||||
autoSelectTimeSlotIfOnlyOneIsAvailable() {
|
autoSelectTimeSlotIfOnlyOneIsAvailable() {
|
||||||
const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length;
|
const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length;
|
||||||
if (this.appointmentType && numberOfOptions === 1) {
|
if (numberOfOptions === 1) {
|
||||||
if (this.availableTimeSlots.length > 0) {
|
if (this.availableTimeSlots.length > 0) {
|
||||||
|
// this.availableTimeSlots only returns mobile/inshop slots so we know
|
||||||
|
// the only available slot is not dropOFf
|
||||||
this.selectedAnswerForTimeSlots = this.availableTimeSlots[0].value;
|
this.selectedAnswerForTimeSlots = this.availableTimeSlots[0].value;
|
||||||
} else {
|
} else {
|
||||||
this.selectedAnswerForDropOffOrInshop = this.answersForDropOffQuestion[0].value;
|
this.selectedAnswerForDropOffOrInshop = this.answersForDropOffQuestion[0].value;
|
||||||
}
|
}
|
||||||
this.setSelectedTimeSlot();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addPremiumFlagToInput(routeCode) {
|
addPremiumFlagToInput(routeCode) {
|
||||||
|
|
@ -517,41 +520,6 @@ export default {
|
||||||
removePremiumFlagFromInput(routeCode) {
|
removePremiumFlagFromInput(routeCode) {
|
||||||
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, "");
|
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, "");
|
||||||
},
|
},
|
||||||
getSelectedTimeSlotInfoObject(routeCode) {
|
|
||||||
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
|
|
||||||
if (routeCodeIncludesPremium) {
|
|
||||||
routeCode = this.removePremiumFlagFromInput(routeCode);
|
|
||||||
}
|
|
||||||
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
|
|
||||||
(slot) => slot.id == routeCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (timeSlot) {
|
|
||||||
return {
|
|
||||||
timeSlot: {
|
|
||||||
date: this.timeSlotsForSelectedDate.date,
|
|
||||||
routeCode: timeSlot.id,
|
|
||||||
startTime: timeSlot.startTime,
|
|
||||||
endTime: timeSlot.endTime,
|
|
||||||
jobMaxMinutes: this.estimatedServiceMinutesMaximum?.toString() || null,
|
|
||||||
jobMinMinutes: this.estimatedServiceMinutesMinimum?.toString() || null,
|
|
||||||
},
|
|
||||||
isPremiumAppointment: routeCodeIncludesPremium ? true : false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeSlot: {
|
|
||||||
date: null,
|
|
||||||
routeCode: null,
|
|
||||||
startTime: null,
|
|
||||||
endTime: null,
|
|
||||||
jobMaxMinutes: null,
|
|
||||||
jobMinMinutes: null,
|
|
||||||
},
|
|
||||||
isPremiumAppointment: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
waitListChecked(event) {
|
waitListChecked(event) {
|
||||||
this.$emit("waitListRequested", event.target.checked);
|
this.$emit("waitListRequested", event.target.checked);
|
||||||
},
|
},
|
||||||
|
|
@ -561,65 +529,6 @@ export default {
|
||||||
successMessageElement.scrollIntoView({ behavior: "smooth" });
|
successMessageElement.scrollIntoView({ behavior: "smooth" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetSelectedTimeSlotIfDateChange(timeSlotsForSelectedDate) {
|
|
||||||
if (this.selectedRouteCode) {
|
|
||||||
const selectedRouteCodeString = this.selectedRouteCode.replace(
|
|
||||||
PREMIUM_TIME_SLOT_ID_FLAG,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
const matchingTimeSlot = timeSlotsForSelectedDate.find((slot) => {
|
|
||||||
return slot.id === selectedRouteCodeString;
|
|
||||||
});
|
|
||||||
if (!matchingTimeSlot) {
|
|
||||||
this.resetSelectedTimeSlot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetSelectedTimeSlot() {
|
|
||||||
this.selectedRouteCode = null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
waitListRequested(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToSuccessMessage();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modelValue: {
|
|
||||||
handler(newValue) {
|
|
||||||
this.handleChange(newValue);
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
},
|
|
||||||
selectedDate: {
|
|
||||||
handler() {
|
|
||||||
this.resetSelectedTimeSlot();
|
|
||||||
this.selectedAnswerForDropOffOrInshop = null;
|
|
||||||
this.selectedAnswerForTimeSlots = null;
|
|
||||||
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
selectedRouteCode(newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.setSelectedTimeSlot();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectedAnswerForDropOffOrInshop(newValue) {
|
|
||||||
if (newValue == PICK_A_TIME_BUTTON_VALUE) {
|
|
||||||
this.selectedRouteCode = null;
|
|
||||||
this.setSelectedTimeSlot();
|
|
||||||
} else {
|
|
||||||
this.selectedAnswerForTimeSlots = null;
|
|
||||||
this.selectedRouteCode = newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectedAnswerForTimeSlots(newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.selectedRouteCode = newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
textBlock,
|
textBlock,
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ export default {
|
||||||
"service-location"
|
"service-location"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!closestShops || closestShops.providers?.length === 0) {
|
if (!closestShops || closestShops.inShopProviders?.length === 0) {
|
||||||
this.displayNoServiceAlert = true;
|
this.displayNoServiceAlert = true;
|
||||||
this.focusOnZipInput();
|
this.focusOnZipInput();
|
||||||
this.resetModalButtonStyle();
|
this.resetModalButtonStyle();
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
</transition>
|
</transition>
|
||||||
<modal
|
<modal
|
||||||
ref="shopQuestionModal"
|
ref="shopQuestionModal"
|
||||||
|
class="shop-question-popup"
|
||||||
:headerText="modalHeaderText"
|
:headerText="modalHeaderText"
|
||||||
:onModalOpenedCallback="updateSelectedAnswer"
|
:onModalOpenedCallback="updateSelectedAnswer"
|
||||||
:onModalClosedCallback="resetZipCodeData"
|
:onModalClosedCallback="resetZipCodeData"
|
||||||
|
|
@ -290,7 +291,7 @@ export default {
|
||||||
this.$store.getters.order.vehicle.carId,
|
this.$store.getters.order.vehicle.carId,
|
||||||
"service-location"
|
"service-location"
|
||||||
);
|
);
|
||||||
if (!closestShops || closestShops.providers?.length === 0) {
|
if (!closestShops || closestShops.inShopProviders?.length === 0) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.displayNoServiceAlert = true;
|
this.displayNoServiceAlert = true;
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -392,6 +393,25 @@ export default {
|
||||||
.modal.modal-component .modal-dialog .modal-content .modal-body .textbox-question {
|
.modal.modal-component .modal-dialog .modal-content .modal-body .textbox-question {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
.modal.shop-question-popup .btn.btn-primary {
|
||||||
|
background: $blue;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 3px,
|
||||||
|
0 0 0 5.5px $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.form-test-invalid {
|
||||||
|
background: #d4d6d8;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.shop-question-zipcode {
|
.shop-question-zipcode {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ export default {
|
||||||
this.serviceZipCode,
|
this.serviceZipCode,
|
||||||
this.carId
|
this.carId
|
||||||
);
|
);
|
||||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||||
if (this.displayNoServiceAlert) {
|
if (this.displayNoServiceAlert) {
|
||||||
return this.$refs.navbar.removeLoader();
|
return this.$refs.navbar.removeLoader();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,9 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
html .has-error.list-card:hover {
|
||||||
|
border-radius: 50rem;
|
||||||
|
}
|
||||||
.vehicle-parts {
|
.vehicle-parts {
|
||||||
label {
|
label {
|
||||||
&.list-card {
|
&.list-card {
|
||||||
|
|
|
||||||
|
|
@ -496,7 +496,10 @@ export default {
|
||||||
{ zip: this.serviceZipCode, carId: this.carId },
|
{ zip: this.serviceZipCode, carId: this.carId },
|
||||||
"vehicle"
|
"vehicle"
|
||||||
);
|
);
|
||||||
if (!closestShops?.data?.providers || closestShops.data.providers.length === 0) {
|
if (
|
||||||
|
!closestShops?.data?.inShopProviders ||
|
||||||
|
closestShops.data.inShopProviders.length === 0
|
||||||
|
) {
|
||||||
this.displayNoServiceAlert = true;
|
this.displayNoServiceAlert = true;
|
||||||
if (store.getters.externalParameterState?.isExternalParameter) {
|
if (store.getters.externalParameterState?.isExternalParameter) {
|
||||||
return baseMixin.methods.ResetExternalParamsAndHideModal();
|
return baseMixin.methods.ResetExternalParamsAndHideModal();
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,7 @@ export default {
|
||||||
this.serviceZipCode,
|
this.serviceZipCode,
|
||||||
this.carId
|
this.carId
|
||||||
);
|
);
|
||||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||||
if (this.displayNoServiceAlert) {
|
if (this.displayNoServiceAlert) {
|
||||||
return this.$refs.navbar.removeLoader();
|
return this.$refs.navbar.removeLoader();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -695,6 +695,7 @@ export const mutations = {
|
||||||
vehicleSubType: sessionInformation.order.vehicle?.vehicleSubType,
|
vehicleSubType: sessionInformation.order.vehicle?.vehicleSubType,
|
||||||
vehicleSpecialClass: sessionInformation.order.vehicle?.vehicleSpecialClass,
|
vehicleSpecialClass: sessionInformation.order.vehicle?.vehicleSpecialClass,
|
||||||
isBigTruck: sessionInformation.order.vehicle?.isBigTruck,
|
isBigTruck: sessionInformation.order.vehicle?.isBigTruck,
|
||||||
|
canSafeliteService: sessionInformation.order.vehicle?.canSafeliteService,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.order.damage.glassToReplace = sessionInformation.order.damage.glassToReplace;
|
state.order.damage.glassToReplace = sessionInformation.order.damage.glassToReplace;
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,6 @@ export default {
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.btn {
|
.btn {
|
||||||
&:first-child:active {
|
|
||||||
background-color: $red;
|
|
||||||
}
|
|
||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $red;
|
background: $red;
|
||||||
|
|
@ -117,8 +114,8 @@ export default {
|
||||||
&.btn-secondary {
|
&.btn-secondary {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid $red;
|
border: 1px solid $blue;
|
||||||
color: $red;
|
color: $blue;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 150ms linear;
|
transition: all 150ms linear;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
|
@ -131,8 +128,11 @@ export default {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 3px $white,
|
0 0 0 3px $white,
|
||||||
0 0 0 5.5px $red;
|
0 0 0 5.5px $blue;
|
||||||
color: $white;
|
color: $blue;
|
||||||
|
&:hover {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export default {
|
||||||
color: $black;
|
color: $black;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: $blue-100;
|
background: $blue-100;
|
||||||
box-shadow: 0 0 0 1px $blue;
|
border: 1px solid $blue;
|
||||||
}
|
}
|
||||||
&:checked:focus + .list-button-content {
|
&:checked:focus + .list-button-content {
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,23 @@ parameters:
|
||||||
- name: imageTag
|
- name: imageTag
|
||||||
type: string
|
type: string
|
||||||
default: '$(Build.BuildId)'
|
default: '$(Build.BuildId)'
|
||||||
|
- name: dockerBuildArgs
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
- name: envFileName
|
- name: envFileName
|
||||||
type: string
|
type: string
|
||||||
default: '.env.ci'
|
default: '.env.ci'
|
||||||
|
- name: applicationType
|
||||||
|
type: string
|
||||||
|
default: 'vue'
|
||||||
|
values: ['vue', 'dotnet']
|
||||||
|
- name: readinessEndpoint
|
||||||
|
type: string
|
||||||
|
default: '/healthcheck'
|
||||||
|
- name: serverMaxAttempts
|
||||||
|
type: number
|
||||||
|
default: 60
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: playwright_tests
|
- job: playwright_tests
|
||||||
continueOnError: true
|
continueOnError: true
|
||||||
|
|
@ -103,6 +115,14 @@ jobs:
|
||||||
echo "Docker cleanup completed!"
|
echo "Docker cleanup completed!"
|
||||||
displayName: "Docker Cleanup"
|
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
|
- task: Docker@2
|
||||||
displayName: 'Build Docker Image'
|
displayName: 'Build Docker Image'
|
||||||
inputs:
|
inputs:
|
||||||
|
|
@ -110,20 +130,23 @@ jobs:
|
||||||
dockerfile: ${{ parameters.dockerFileName }}
|
dockerfile: ${{ parameters.dockerFileName }}
|
||||||
repository: ${{ parameters.dockerImageName }}
|
repository: ${{ parameters.dockerImageName }}
|
||||||
tags: ${{ parameters.imageTag }}
|
tags: ${{ parameters.imageTag }}
|
||||||
arguments: '--no-cache --pull'
|
arguments: |
|
||||||
|
--no-cache --pull ${{ parameters.dockerBuildArgs }}
|
||||||
|
|
||||||
- script: |
|
- script: |
|
||||||
printenv > "${{ parameters.envFileName }}"
|
# Determine JIRA card number and filters
|
||||||
|
|
||||||
JIRA_CARD_NUMBER=""
|
JIRA_CARD_NUMBER=""
|
||||||
IS_REGRESSION="${{ parameters.isRegression }}"
|
IS_REGRESSION="${{ parameters.isRegression }}"
|
||||||
echo "isRegression: $IS_REGRESSION";
|
echo "isRegression: $IS_REGRESSION"
|
||||||
|
|
||||||
if [[ "${{ parameters.isRegression }}" == "False" ]]; then
|
if [[ "${{ parameters.isRegression }}" == "False" ]]; then
|
||||||
branch_name=$(System.PullRequest.SourceBranch)
|
branch_name="$(System.PullRequest.SourceBranch)"
|
||||||
echo "Retrieved branch name: '$branch_name'"
|
echo "Retrieved branch name: '$branch_name'"
|
||||||
JIRA_CARD_NUMBER="${branch_name##*/}"
|
JIRA_CARD_NUMBER="${branch_name##*/}"
|
||||||
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
|
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Build test filter
|
||||||
FILTER=""
|
FILTER=""
|
||||||
if [[ -n "${{ parameters.filterTags }}" && -n "$JIRA_CARD_NUMBER" ]]; then
|
if [[ -n "${{ parameters.filterTags }}" && -n "$JIRA_CARD_NUMBER" ]]; then
|
||||||
echo "Filtering by filterTags and Jira Card Number..."
|
echo "Filtering by filterTags and Jira Card Number..."
|
||||||
|
|
@ -140,57 +163,213 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Filter value: $FILTER"
|
echo "Filter value: $FILTER"
|
||||||
TARGET_URL=${{ parameters.targetUrl }}
|
TARGET_URL="${{ parameters.targetUrl }}"
|
||||||
echo "BASE_URL value: $BASE_URL"
|
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
|
if [[ "$TARGET_URL" == *localhost* ]]; then
|
||||||
echo 'Npx version:'
|
echo "Localhost URL detected, extracting port..."
|
||||||
npx --version
|
port=$(echo "$TARGET_URL" | sed -E 's/.*:([0-9]+).*/\1/')
|
||||||
port="$TARGET_URL"
|
echo "Port extracted: $port"
|
||||||
echo "URL: '$port'"
|
|
||||||
echo "URL '$port' contains 'localhost'. Extracting port... "
|
# Create readiness check URL based on application type
|
||||||
port=$(echo "$port" | sed -E 's/.*:([0-9]+).*/\1/')
|
if [[ "$APPLICATION_TYPE" == "dotnet" ]]; then
|
||||||
echo "Port is '$port'"
|
# For .NET APIs, append the readiness endpoint to the target URL
|
||||||
TARGET_URL="$TARGET_URL"
|
READINESS_URL="${TARGET_URL}${{ parameters.readinessEndpoint }}"
|
||||||
JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
|
echo "Using .NET readiness check URL: $READINESS_URL"
|
||||||
FILTER="$FILTER"
|
else
|
||||||
PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
|
# For Vue apps, use the target URL as-is
|
||||||
SERVE_PATH="${{ parameters.npmServePath }}"
|
READINESS_URL="$TARGET_URL"
|
||||||
SHARD_NUMBER=$(shardNumber)
|
echo "Using Vue app URL for readiness check: $READINESS_URL"
|
||||||
TOTAL_SHARDS=${{ parameters.totalShards }}
|
fi
|
||||||
|
|
||||||
|
echo "Starting tests for shard $SHARD_NUMBER of $TOTAL_SHARDS..."
|
||||||
|
|
||||||
container_id=$(docker create \
|
container_id=$(docker create \
|
||||||
--ipc=host \
|
--ipc=host \
|
||||||
--env CI=true \
|
--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_MAX_ATTEMPTS="${{ parameters.serverMaxAttempts }}" \
|
||||||
|
--env ASPNETCORE_URLS="https://localhost:$port" \
|
||||||
|
--env ENV_FILE="$ENV_FILE" \
|
||||||
--env-file "${{ parameters.envFileName }}" \
|
--env-file "${{ parameters.envFileName }}" \
|
||||||
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
||||||
npx concurrently -k -n 'server,playwright' \
|
bash -c '
|
||||||
"npm --prefix $SERVE_PATH run serve -- --port=$port" \
|
echo "=== Starting Application Server ==="
|
||||||
"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" \
|
echo "Application Type: $APPLICATION_TYPE"
|
||||||
|
echo "Target URL: $TARGET_URL"
|
||||||
|
echo "Readiness Check URL: $READINESS_URL"
|
||||||
|
echo "Server Max Attempts: $SERVER_MAX_ATTEMPTS seconds"
|
||||||
|
|
||||||
|
# Function to wait for server readiness
|
||||||
|
wait_for_server() {
|
||||||
|
local url=$1
|
||||||
|
local max_attempts=$SERVER_MAX_ATTEMPTS
|
||||||
|
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
|
else
|
||||||
export TARGET_URL="$TARGET_URL"
|
# Remote URL testing
|
||||||
export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
|
echo "Remote URL testing mode"
|
||||||
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 }}"
|
|
||||||
container_id=$(docker create \
|
container_id=$(docker create \
|
||||||
--ipc=host \
|
--ipc=host \
|
||||||
--env CI=true \
|
--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 }}" \
|
--env-file "${{ parameters.envFileName }}" \
|
||||||
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
||||||
bash -c "
|
bash -c "
|
||||||
set -a
|
echo '=== Running Tests Against Remote URL ==='
|
||||||
source $ENV_FILE
|
echo 'Target URL: $TARGET_URL'
|
||||||
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
|
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
|
fi
|
||||||
|
|
||||||
# Start container and stream logs
|
# Start container and stream logs
|
||||||
echo "Starting tests for shard $(shardNumber)..."
|
echo "Starting tests for shard $(shardNumber)..."
|
||||||
docker start -a $container_id
|
docker start -a $container_id
|
||||||
|
|
@ -206,12 +385,6 @@ jobs:
|
||||||
# Remove container
|
# Remove container
|
||||||
echo "Cleaning up container..."
|
echo "Cleaning up container..."
|
||||||
docker rm $container_id
|
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)'
|
displayName: 'Run Playwright Tests - Shard $(shardNumber)'
|
||||||
env: ${{ parameters.secrets }}
|
env: ${{ parameters.secrets }}
|
||||||
|
|
||||||
|
|
@ -227,6 +400,7 @@ jobs:
|
||||||
docker rmi ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} -f
|
docker rmi ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} -f
|
||||||
displayName: 'Cleanup Docker Image'
|
displayName: 'Cleanup Docker Image'
|
||||||
condition: always()
|
condition: always()
|
||||||
|
|
||||||
- job: download_and_merge_reports
|
- job: download_and_merge_reports
|
||||||
dependsOn: playwright_tests
|
dependsOn: playwright_tests
|
||||||
timeoutInMinutes: 8
|
timeoutInMinutes: 8
|
||||||
|
|
@ -238,6 +412,10 @@ jobs:
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
inputs:
|
inputs:
|
||||||
targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports'
|
targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports'
|
||||||
|
- bash: |
|
||||||
|
printenv > "${{ parameters.envFileName }}"
|
||||||
|
env: ${{ parameters.secrets }}
|
||||||
|
displayName: "Make Azure Pipeline Variables Available to Docker"
|
||||||
- task: Docker@2
|
- task: Docker@2
|
||||||
displayName: 'Build Docker Image'
|
displayName: 'Build Docker Image'
|
||||||
inputs:
|
inputs:
|
||||||
|
|
@ -245,16 +423,15 @@ jobs:
|
||||||
dockerfile: ${{ parameters.dockerFileName }}
|
dockerfile: ${{ parameters.dockerFileName }}
|
||||||
repository: ${{ parameters.dockerImageName }}
|
repository: ${{ parameters.dockerImageName }}
|
||||||
tags: ${{ parameters.imageTag }}
|
tags: ${{ parameters.imageTag }}
|
||||||
arguments: '--no-cache --pull'
|
arguments: |
|
||||||
|
--no-cache --pull ${{ parameters.dockerBuildArgs }}
|
||||||
- bash: |
|
- bash: |
|
||||||
printenv > "${{ parameters.envFileName }}"
|
|
||||||
branch_name=$(System.PullRequest.SourceBranch)
|
branch_name=$(System.PullRequest.SourceBranch)
|
||||||
echo "Retrieved branch name: '$branch_name'"
|
echo "Retrieved branch name: '$branch_name'"
|
||||||
JIRA_CARD_NUMBER="${branch_name##*/}"
|
JIRA_CARD_NUMBER="${branch_name##*/}"
|
||||||
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
|
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
|
||||||
export PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
|
export PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
|
||||||
|
|
||||||
|
|
||||||
export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
|
export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
|
||||||
export ENV_FILE="${{ parameters.envFileName }}"
|
export ENV_FILE="${{ parameters.envFileName }}"
|
||||||
container_id=$(docker create \
|
container_id=$(docker create \
|
||||||
|
|
@ -263,14 +440,12 @@ jobs:
|
||||||
--env-file "${{ parameters.envFileName }}" \
|
--env-file "${{ parameters.envFileName }}" \
|
||||||
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
||||||
bash -c "
|
bash -c "
|
||||||
set -a
|
|
||||||
source $ENV_FILE
|
|
||||||
set +a
|
|
||||||
echo 'Moving Playwright reports out of subfolders...'
|
echo 'Moving Playwright reports out of subfolders...'
|
||||||
find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \;
|
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 'Value of JIRA_CARD_NUMBER in bash -c command: $JIRA_CARD_NUMBER'
|
||||||
echo 'Merging reports...'
|
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
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue