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
|
||||
- template: temp/playwright-test.yml
|
||||
parameters:
|
||||
applicationType: 'vue'
|
||||
totalShards: ${{ variables.totalShards }}
|
||||
targetUrl: $(BASE_URL)
|
||||
dockerFileName: 'Dockerfile.playwright'
|
||||
|
|
|
|||
|
|
@ -51,19 +51,11 @@ stages:
|
|||
displayName: Run Prettier check
|
||||
|
||||
- stage: TestPr
|
||||
displayName: Run Unit 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
|
||||
displayName: Run Tests For PullRequest
|
||||
jobs:
|
||||
- template: temp/playwright-test.yml
|
||||
parameters:
|
||||
applicationType: 'vue'
|
||||
totalShards: 2
|
||||
targetUrl: $(BASE_URL)
|
||||
dockerFileName: 'Dockerfile.playwright'
|
||||
|
|
@ -75,6 +67,12 @@ stages:
|
|||
secrets:
|
||||
CCIS_API_AUTH: $(CCIS_API_AUTH)
|
||||
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 }}:
|
||||
# Dev Build/Deploy
|
||||
- stage: Dev
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Environment configuration
|
||||
|
||||
# Environment type
|
||||
NODE_ENV="qa"
|
||||
PLAYWRIGHT_ENV="qa"
|
||||
|
||||
# Skip content site
|
||||
SKIP_CONTENT_SITE=false
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Environment configuration
|
||||
|
||||
# Environment type
|
||||
NODE_ENV="qa"
|
||||
PLAYWRIGHT_ENV="qa"
|
||||
|
||||
# Skip content site
|
||||
SKIP_CONTENT_SITE=false
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Environment configuration
|
||||
|
||||
# Environment type
|
||||
NODE_ENV="qa"
|
||||
PLAYWRIGHT_ENV="qa"
|
||||
|
||||
# Skip content site
|
||||
SKIP_CONTENT_SITE=false
|
||||
|
|
|
|||
|
|
@ -260,7 +260,6 @@ ADMIN_SERVICE_API_URL= <br>
|
|||
SAUCE_USERNAME= <br>
|
||||
SAUCE_ACCESS_KEY= <br>
|
||||
|
||||
|
||||
## CI/CD Integration
|
||||
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 {
|
||||
VehicleSelectionPage = '4%',
|
||||
VehicleDamagePage = '16%',
|
||||
EstimatePage = '28%',
|
||||
ServiceZipPage = '32%',
|
||||
VehicleLookupAddressPage = '32%',
|
||||
VehicleLookupLicensePage = '32%',
|
||||
VinLookupPage = '32%',
|
||||
PartQuestionsPage = '40%',
|
||||
MoldingQuestionsPage = '40%',
|
||||
CapabilityQuestionsPage = '40%',
|
||||
VehiclePartsPage = '40%',
|
||||
ServicePackagePage = '48%',
|
||||
VehicleSelectionPage = '8%',
|
||||
VehicleDamagePage = '11%',
|
||||
EstimatePage = '18%',
|
||||
ServiceZipPage = '22%',
|
||||
VehicleLookupAddressPage = '22%',
|
||||
VehicleLookupLicensePage = '22%',
|
||||
VinLookupPage = '22%',
|
||||
PartQuestionsPage = '30%',
|
||||
MoldingQuestionsPage = '30%',
|
||||
CapabilityQuestionsPage = '30%',
|
||||
VehiclePartsPage = '30%',
|
||||
ServicePackagePage = '62%',
|
||||
InsuranceCompanyPage = '52%',
|
||||
ServiceLocationPage = '60%',
|
||||
SchedulePage = '64%',
|
||||
ServiceLocationPage = '76%',
|
||||
SchedulePage = '76%',
|
||||
MobileDetailsPage = '76%',
|
||||
ContactDetailsPage = '84%',
|
||||
PaymentMethodPage = '92%',
|
||||
PaymentMethodPage = '97%',
|
||||
OrderConfirmationPage = '100%'
|
||||
}
|
||||
8
playwright-tests/package-lock.json
generated
8
playwright-tests/package-lock.json
generated
|
|
@ -22,7 +22,7 @@
|
|||
"eslint": "^9.28.0",
|
||||
"luxon": "^3.6.1",
|
||||
"ortoni-report": "^3.0.2",
|
||||
"playwright-jira-reporter": "^1.0.4",
|
||||
"playwright-jira-reporter": "^1.0.9",
|
||||
"safelite-playwright-core": "^1.0.26",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
|
@ -3006,9 +3006,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-jira-reporter": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/playwright-jira-reporter/-/playwright-jira-reporter-1.0.4.tgz",
|
||||
"integrity": "sha1-uwnLFtj3Pnq5XXMFotlfHItucAw=",
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/playwright-jira-reporter/-/playwright-jira-reporter-1.0.9.tgz",
|
||||
"integrity": "sha1-o6+EHQlQ/jY4BtWKA6gSJ5mwK6A=",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"eslint": "^9.28.0",
|
||||
"luxon": "^3.6.1",
|
||||
"ortoni-report": "^3.0.2",
|
||||
"playwright-jira-reporter": "^1.0.4",
|
||||
"playwright-jira-reporter": "^1.0.9",
|
||||
"safelite-playwright-core": "^1.0.26",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export class AfterpayPage extends BasePage {
|
|||
readonly passwordTextBox: Locator;
|
||||
|
||||
// Card details
|
||||
readonly paymentOptionsButton: Locator;
|
||||
readonly continueButtonAfterpay: Locator;
|
||||
readonly cardholderNameTextBox: Locator;
|
||||
readonly cardNumberTextBox: Locator;
|
||||
readonly expirationDateTextBox: Locator;
|
||||
|
|
@ -23,6 +25,8 @@ export class AfterpayPage extends BasePage {
|
|||
this.passwordTextBox = page.getByTestId('login-password-input');
|
||||
this.submitButton = page.getByRole('button', { name: 'Continue' });
|
||||
|
||||
this.paymentOptionsButton = page.locator('div:has(>input[id*=\'payment-types\']) label').nth(1);
|
||||
this.continueButtonAfterpay = page.getByRole('button').filter({hasText: 'Continue'});
|
||||
this.cardholderNameTextBox = page.getByTestId('payment-method-cardHolderName-input');
|
||||
this.cardNumberTextBox = page.getByTestId('payment-method-cardNumber-input');
|
||||
this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input');
|
||||
|
|
@ -46,7 +50,12 @@ export class AfterpayPage extends BasePage {
|
|||
|
||||
async executeAfterpayPayment(paymentDetails: IPaymentDetails) {
|
||||
await this.login(paymentDetails.password!);
|
||||
|
||||
await this.page.locator('div[data-testid=\'loading-icon-svg\']').filter({ visible: true}).first().waitFor({ state: 'hidden' });
|
||||
if (await this.paymentOptionsButton.isVisible())
|
||||
{
|
||||
await this.paymentOptionsButton.click();
|
||||
await this.continueButtonAfterpay.click();
|
||||
}
|
||||
await this.confirmButton.click();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export class BasePage {
|
|||
this.pageSpinner = page.getByRole('status');
|
||||
this.buttonLoadSpin = page.getByRole('alert');
|
||||
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() {
|
||||
|
|
@ -104,7 +104,7 @@ export class BasePage {
|
|||
|
||||
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) => {
|
||||
const currentDate = new Date().toISOString().split('T')[0]; // e.g., "2025-07-23"
|
||||
if(route.request().postDataJSON().startDate === currentDate) {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class PaymentMethodPage extends BasePage {
|
|||
this.payNowButton = this.page.locator('[buttonlabel="Pay now"]');
|
||||
this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]');
|
||||
this.submitButton = this.page.locator('[data-test-id="nav-bar-main-button"]');
|
||||
this.recalibrationCheckbox = this.page.getByLabel('I understand after windshield');
|
||||
this.recalibrationCheckbox = this.page.locator('label:has(>input[name=\'recalAckOptIn\'])');
|
||||
// this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1);
|
||||
this.paymentPage = new PaymentPage(page);
|
||||
this.paypalPage = new PaypalPage(page);
|
||||
|
|
@ -101,7 +101,7 @@ export class PaymentMethodPage extends BasePage {
|
|||
let expectedAppointmentDetails = new Map<string, string[]>();
|
||||
expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails);
|
||||
expectedAppointmentDetails = await this.getExpectedVehicleDamage(testData, expectedAppointmentDetails);
|
||||
expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails);
|
||||
// expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails);
|
||||
expectedAppointmentDetails = await this.getExpectedServiceLocation(testData, expectedAppointmentDetails);
|
||||
expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails);
|
||||
expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails);
|
||||
|
|
@ -403,7 +403,7 @@ l
|
|||
const { appointmentDetails } = testData;
|
||||
let serviceLocationTitle = appointmentDetails?.serviceLocation == ServiceLocation.Mobile
|
||||
? "We're coming to you"
|
||||
: "You're going to a Safelite shop";
|
||||
: "You're coming to us";
|
||||
let serviceLocation: string[] = [];
|
||||
|
||||
serviceLocation.push(
|
||||
|
|
@ -443,7 +443,7 @@ l
|
|||
customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : ""
|
||||
);
|
||||
|
||||
expectedServicePackageDetails["Appointment date + time"] = appointmentDateText;
|
||||
expectedServicePackageDetails["Appointment Date + Time"] = appointmentDateText;
|
||||
return expectedServicePackageDetails;
|
||||
}
|
||||
|
||||
|
|
@ -452,7 +452,7 @@ l
|
|||
let customerDetailsText: string[] = [];
|
||||
|
||||
customerDetailsText.push(
|
||||
customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(),
|
||||
// customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(),
|
||||
customerDetails?.email ? customerDetails?.email.toUpperCase() : "",
|
||||
customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "",
|
||||
"Opted out of text message updates"
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ export class PaypalPage extends BasePage {
|
|||
this.passwordTextBox = page.getByPlaceholder('Password');
|
||||
this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true });
|
||||
this.completePurchaseButton = page.getByTestId('submit-button-initial')
|
||||
this.payWithRadioButton = page.locator('.py-4').first();
|
||||
this.payButton = page.getByRole('button', { name: 'Pay $' });
|
||||
this.payWithRadioButton = page.getByRole('button').filter({ hasText: 'Pay with' });
|
||||
this.payButton = page.locator('#one-time-cta');
|
||||
}
|
||||
|
||||
async completePaypalPurchase(paymentDetails: IPaymentDetails){
|
||||
|
|
@ -45,7 +45,8 @@ export class PaypalPage extends BasePage {
|
|||
await this.passwordTextBox.fill(paymentDetails.password!);
|
||||
await this.paypalLoginButton.click();
|
||||
await this.payWithRadioButton.click();
|
||||
await this.payButton.click();
|
||||
await this.page.waitForTimeout(2000); // wait for 2 seconds to ensure the Pay button is clickable
|
||||
await this.payButton.dblclick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@ export class SchedulePage extends BasePage {
|
|||
readonly militaryWarningMessage: Locator;
|
||||
readonly changeZipButton: Locator;
|
||||
readonly updateZipTextBox: Locator;
|
||||
readonly safeliteShopsList: Locator;
|
||||
readonly searchButton: Locator;
|
||||
readonly storeAddressText: Locator;
|
||||
readonly saveLocationButton: Locator;
|
||||
readonly changeShopLocationLink: Locator;
|
||||
readonly moreLocationsButton: Locator;
|
||||
readonly selectAShopOptions: Locator;
|
||||
readonly yourSafeliteShop: Locator;
|
||||
|
||||
|
|
@ -44,12 +47,15 @@ export class SchedulePage extends BasePage {
|
|||
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.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.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.yourSafeliteShop = this.page.locator("fieldset:has(#chooseShop) label");
|
||||
|
||||
this.allDayDropOffButton = this.page.locator("label[buttonlabel='Drop off all day']");
|
||||
this.allDayDropOffButton = this.page.locator("label[buttonlabel*='Drop']");
|
||||
this.pickATimeButton = this.page.locator("label[buttonlabel='Pick a time']");
|
||||
this.firstAvailableDate = this.page.locator('.selectable-day').filter({ visible: true}).locator('nth=0');
|
||||
this.firstAvailableTime = this.page.locator('label').filter({ hasText: /AM|PM/ }).locator('div').locator('nth=0');
|
||||
|
|
@ -58,7 +64,7 @@ export class SchedulePage extends BasePage {
|
|||
this.dateText = this.page.locator('label.modal-title');
|
||||
this.viewMoreDatesLink = this.page.getByText(/View more dates/).first();
|
||||
this.appointmentDuration = this.page.locator('.duration-text-block');
|
||||
this.timeSlots = this.page.locator('fieldset[aria-labelledby=\'chooseTimeSlot\'] label');
|
||||
this.timeSlots = this.page.locator('fieldset:has(>legend#chooseTimeSlot) label').filter({ visible: true});
|
||||
}
|
||||
|
||||
async selectLocation(testData: Partial<ITestData>) {
|
||||
|
|
@ -76,25 +82,36 @@ export class SchedulePage extends BasePage {
|
|||
}
|
||||
|
||||
async scheduleInShop(appointmentDetails?: IAppointmentDetails) {
|
||||
await this.inShopButton.click();
|
||||
|
||||
await this.inShopButton.isVisible() ? await this.inShopButton.click() : null;
|
||||
if (appointmentDetails && appointmentDetails.shopAddress) {
|
||||
const zipCodeMatch = appointmentDetails.shopAddress.match(/\b\d{5}$/);
|
||||
if (zipCodeMatch) {
|
||||
const zipCode = zipCodeMatch[0];
|
||||
// Enter the ZIP code into the updateZipTextBox
|
||||
await this.changeShopLocationLink.click();
|
||||
await this.moreLocationsButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
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
|
||||
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());
|
||||
}
|
||||
|
||||
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();
|
||||
for (const inshopAvailableDate of await inshopAvailableDates) {
|
||||
await inshopAvailableDate.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
if (await this.allDayDropOffButton.isVisible()) {
|
||||
customerDetails!.apptDate = `${await inshopAvailableDate.getAttribute("id")}`;
|
||||
break;
|
||||
|
|
@ -186,13 +204,13 @@ export class SchedulePage extends BasePage {
|
|||
}
|
||||
|
||||
// appointmentmentDetails.serviceLocation === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click();
|
||||
customerDetails!.apptDuration = (await this.appointmentDuration.innerText()).replace("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();
|
||||
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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class ServicePackagesPage extends BasePage {
|
|||
this.iHavePromoCodeButton = this.page.getByRole('link', { name: 'I have a promo code' });
|
||||
this.skipQuoteEmailButton = this.page.getByRole('button', { name: 'Skip' });
|
||||
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.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
|
||||
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ export class VehicleDamagePage extends BasePage {
|
|||
super(page);
|
||||
this.page = page;
|
||||
this.windshieldChkBox = this.page.locator('[buttonlabel="Windshield"]');
|
||||
this.crackButton = this.page.locator('[buttonlabel="Crack"]');
|
||||
this.chipButton = this.page.locator('[buttonlabel="Chip(s)"]');
|
||||
this.sideDoorButton = this.page.locator('[buttonlabel="Side door"]');
|
||||
this.crackButton = this.page.locator('[buttonlabelsubcopy="Replace my windshield"]');
|
||||
this.chipButton = this.page.locator('[buttonlabelsubcopy="Repair my windshield"]');
|
||||
this.sideDoorButton = this.page.locator('[buttonlabel="Side window"]');
|
||||
this.driverSideButton = this.page.locator('[buttonlabel="Driver side"]');
|
||||
this.passengerSideButton = this.page.locator('[buttonlabel="Passenger side"]');
|
||||
this.driverQuarterPanelChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Quarter panel"]');
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ if (!process.env.CI) {
|
|||
// 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
|
||||
|
||||
if (process.env.NODE_ENV == 'undefined' || process.env.NODE_ENV == null) {
|
||||
if (process.env.PLAYWRIGHT_ENV == 'undefined' || process.env.PLAYWRIGHT_ENV == null) {
|
||||
dotenv.config({
|
||||
path: path.join(basePath, '.env.dev'),
|
||||
example: path.join(basePath, '.env.example')
|
||||
|
|
@ -17,7 +17,7 @@ if (!process.env.CI) {
|
|||
}
|
||||
else {
|
||||
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')
|
||||
});
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ const ortoniReportConfig: OrtoniReportConfig = {
|
|||
filename: `ortoni_report_${formatDateForFilename(new Date())}.html`,
|
||||
showProject: false,
|
||||
projectName: "FMG-Nextgen-Playwright-Report",
|
||||
testType: `E2E- Environment: ${process.env.NODE_ENV} `,
|
||||
testType: `E2E- Environment: ${process.env.PLAYWRIGHT_ENV} `,
|
||||
preferredTheme: "light",
|
||||
base64Image: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,7 +291,6 @@ async function runWorkflow(page: Page, testCase: TestCase) {
|
|||
// let serviceLocationPage = testCase.pages.serviceLocationPage;
|
||||
// await serviceLocationPage.handleServiceLocationPage(testCase.testData);
|
||||
|
||||
|
||||
// Schedule appointment
|
||||
let schedulePage = testCase.pages.schedulePage;
|
||||
await schedulePage.handleSchedulePage(testCase.testData);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ const cashRepairMobileCCTests: ITestCase[] = [];
|
|||
|
||||
const tc = {
|
||||
name: `CashRepairMobileCreditCard`,
|
||||
tags: ['@E2E','@CashRepairMobileCreditCard', '@test_report', '@CASH'],
|
||||
tags: ['@E2E','@CashRepairMobileCreditCard', '@test_report', '@CASH', '@CASH-848'],
|
||||
testData: cashRepairMobileCCData
|
||||
};
|
||||
cashRepairMobileCCTests.push(tc);
|
||||
|
|
|
|||
|
|
@ -42,14 +42,6 @@ const cashReplaceGlassAddressLookupInshopAfterPayData: Partial<ITestData> = {
|
|||
vehicleLookupType: VehicleLookupType.Address
|
||||
},
|
||||
|
||||
// No need to override vehicleDamage as it already defaults to WindshieldCrack
|
||||
|
||||
// Override appointment details
|
||||
appointmentDetails: {
|
||||
...getDefaultTestData().appointmentDetails!,
|
||||
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
|
||||
},
|
||||
|
||||
// Override payment details
|
||||
paymentDetails: ClientData.getDefaultAfterpayDetails()
|
||||
}
|
||||
|
|
@ -58,7 +50,7 @@ const cashReplaceGlassAddressLookupInshopAfterPayTests: ITestCase[] = [];
|
|||
|
||||
const tc = {
|
||||
name: `CashReplaceGlassAddressLookupInshopAfterPay`,
|
||||
tags: ['@E2E','@CashReplaceGlassAddressLookupInshopAfterPay', '@test_report', '@CASH'],
|
||||
tags: ['@E2E','@CashReplaceGlassAddressLookupInshopAfterPay', '@test_report', '@CASH', '@CASH-848'],
|
||||
testData: cashReplaceGlassAddressLookupInshopAfterPayData
|
||||
};
|
||||
cashReplaceGlassAddressLookupInshopAfterPayTests.push(tc);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
|
|||
appointmentDetails: {
|
||||
serviceLocation: ServiceLocation.DropOff,
|
||||
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
|
||||
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
|
||||
shopAddress: "6826 sawmill rd, Columbus, OH 43235"
|
||||
},
|
||||
|
||||
// Override vehicle details
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const CashReplaceSplitWindshieldTests: ITestCase[] = [];
|
|||
|
||||
const tc = {
|
||||
name: `CashReplaceSplitWindshield`,
|
||||
tags: ['@E2E','@CashReplaceSplitWindshield', '@test_report', '@CASH'],
|
||||
tags: ['@E2E','@CashReplaceSplitWindshield', '@test_report', '@CASH', '@CASH-848'],
|
||||
testData: CashReplaceSplitWindshieldData
|
||||
};
|
||||
CashReplaceSplitWindshieldTests.push(tc);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const cashReplaceWiperDropoffData: Partial<ITestData> = {
|
|||
servicePackage: ServicePackage.Standard,
|
||||
|
||||
// Special flags
|
||||
isSkipEstimatePage: true,
|
||||
isSkipEstimatePage: false,
|
||||
isRecalVehicle: true,
|
||||
|
||||
// Override customer postal code
|
||||
|
|
@ -28,7 +28,7 @@ const cashReplaceWiperDropoffData: Partial<ITestData> = {
|
|||
...getDefaultTestData().customerDetails!,
|
||||
address: {
|
||||
...getDefaultTestData().customerDetails!.address,
|
||||
postalCode: '43085'
|
||||
postalCode: '43235'
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const insuranceAcuityPaypalTests: ITestCase[] = [];
|
|||
|
||||
const tc = {
|
||||
name: `InsuranceAcuityPaypal`,
|
||||
tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187'],
|
||||
tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187', '@CASH-848'],
|
||||
testData: insuranceAcuityPaypalData
|
||||
};
|
||||
insuranceAcuityPaypalTests.push(tc);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ body select {
|
|||
background-color: #fff;
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
body input:focus, body input:focus-visible,
|
||||
body select:focus,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ body {
|
|||
background-color: #fff;
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2.5px #1574a1;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ const damageLocationsSelected = {
|
|||
PASSENGERSIDE: "PassengerSide",
|
||||
STATIONARY: "Stationary",
|
||||
SLIDER: "Slider",
|
||||
DOOR: "Door",
|
||||
GLASS: "Glass",
|
||||
};
|
||||
|
||||
export { damageLocationsSelected };
|
||||
|
|
|
|||
|
|
@ -2,16 +2,6 @@
|
|||
<div
|
||||
class="date-picker text-center"
|
||||
: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">
|
||||
<legend class="sr-only">Select a day and time</legend>
|
||||
<div
|
||||
|
|
@ -139,36 +129,11 @@
|
|||
@click="showAnotherMonth">
|
||||
View more dates
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
import store from "@/store";
|
||||
|
|
@ -178,12 +143,6 @@ import {
|
|||
MONTHS_OF_YEAR,
|
||||
DAYS_OF_WEEK,
|
||||
} from "@/digital-components/date-picker/mixins/constants";
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
RouteCodeFlags,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
cmsWidgetFieldMappings,
|
||||
} from "@/constants/schedule-constants";
|
||||
import {
|
||||
selectableDaysOptions,
|
||||
requiredParameter,
|
||||
|
|
@ -195,7 +154,6 @@ import {
|
|||
import { useField, ErrorMessage } from "vee-validate";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
||||
|
||||
export default {
|
||||
name: "datePicker",
|
||||
|
|
@ -205,10 +163,8 @@ export default {
|
|||
months: null,
|
||||
disableViewMoreDatesButton: false,
|
||||
hideSomeDaysForInitialView: null,
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based)
|
||||
selectableDatesMobile: [],
|
||||
durationTextBlockCopyForInshopOrDropoff: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
|
@ -242,12 +198,6 @@ export default {
|
|||
pricingByDayBasePrice: Number,
|
||||
pricingByDayUpcharge: Number,
|
||||
isPricingByDayExperiment: Boolean,
|
||||
timeSlotsForSelectedDate: Object,
|
||||
appointmentType: String,
|
||||
premiumAppointmentFee: Object,
|
||||
estimatedServiceMinutesMinimum: Number,
|
||||
estimatedServiceMinutesMaximum: Number,
|
||||
displayWaitList: Boolean,
|
||||
isMobileSelected: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
|
|
@ -309,80 +259,10 @@ export default {
|
|||
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: {
|
||||
async initializeComponent(initialData) {
|
||||
await this.setCalendarData(initialData);
|
||||
this.$refs.timeSlotModalQuestion.initializeComponent();
|
||||
},
|
||||
fireDateSelectedEvent(event, date) {
|
||||
// Ignore if arrow key selected radioButton
|
||||
|
|
@ -949,42 +829,9 @@ export default {
|
|||
};
|
||||
window.requestAnimationFrame(step);
|
||||
},
|
||||
getSelectedTimeSlotInfo() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
|
||||
var isPremiumAppointment = false;
|
||||
if (supportingItems) {
|
||||
isPremiumAppointment =
|
||||
!!supportingItems.filter(
|
||||
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
|
||||
).length > 0;
|
||||
}
|
||||
|
||||
const selectedTimeSlotInfo = {
|
||||
timeSlot: store.getters.order.schedule,
|
||||
isPremiumAppointment: isPremiumAppointment,
|
||||
};
|
||||
|
||||
return selectedTimeSlotInfo;
|
||||
},
|
||||
getSupportingItems() {
|
||||
return store.getters.lineItems.supportingItems;
|
||||
},
|
||||
handleWaitListRequested(value) {
|
||||
this.$emit("waitListRequested", value);
|
||||
},
|
||||
getDurationTextBlockCopyForInshopOrDropoff(selectedRouteCode) {
|
||||
if (selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropoffDurationText;
|
||||
} else if (selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
if (this.isSameDay) {
|
||||
return this.sameDayDropoffDurationText;
|
||||
} else {
|
||||
return this.dropOffDurationText;
|
||||
}
|
||||
}
|
||||
return this.inshopDurationText;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(newValue) {
|
||||
|
|
@ -998,19 +845,10 @@ export default {
|
|||
this.scrollToElement("date-of-month-error");
|
||||
}
|
||||
},
|
||||
selectedTimeSlotInfo(newValue) {
|
||||
const routeCode = newValue?.timeSlot?.routeCode;
|
||||
this.durationTextBlockCopyForInshopOrDropoff =
|
||||
this.getDurationTextBlockCopyForInshopOrDropoff(routeCode);
|
||||
this.$emit("TimeSlotSelected", newValue); // needed to update footer button text on Schedule page and to save to Store correctly
|
||||
},
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
ErrorMessage,
|
||||
timeSlotQuestion,
|
||||
textBlock,
|
||||
funnelSubHeader,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1028,14 +866,6 @@ export default {
|
|||
align-items: flex-start;
|
||||
text-align: left;
|
||||
|
||||
.duration-text-block {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.date-picker-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<!-- Modal -->
|
||||
<div
|
||||
class="modal fade modal-component"
|
||||
:class="modalId"
|
||||
:class="[modalId, { 'recall-show': isRecal }]"
|
||||
v-on="{ 'hidden.bs.modal': onModalClosed, 'shown.bs.modal': onModalOpened }"
|
||||
:data-bs-backdrop="backdropSetting"
|
||||
:id="modalId"
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
@keypress.enter="onEnter"
|
||||
aria-hidden="true">
|
||||
<!--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-header mb-0 mt-6">
|
||||
<label
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
<div class="modal-footer">
|
||||
<slot name="modal-footer-slot"></slot>
|
||||
<modalButtonMain
|
||||
isPrimary
|
||||
:isPrimary="isFooterButtonPrimary"
|
||||
class="w-100 modal-footer-button"
|
||||
:id="modalId + '-modalbtn'"
|
||||
ref="modalButtonMain"
|
||||
|
|
@ -65,12 +65,17 @@ export default {
|
|||
suppressPageScroll: Boolean,
|
||||
staticBackdrop: Boolean,
|
||||
footerButtonDisabled: Boolean,
|
||||
isFooterButtonPrimary: Boolean,
|
||||
onModalOpenedCallback: {
|
||||
type: Function,
|
||||
},
|
||||
onModalClosedCallback: {
|
||||
type: Function,
|
||||
},
|
||||
isRecal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -195,6 +200,19 @@ export default {
|
|||
background-color: $white;
|
||||
}
|
||||
&.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 {
|
||||
max-width: 767px;
|
||||
margin: 0 auto;
|
||||
|
|
@ -250,6 +268,9 @@ export default {
|
|||
button {
|
||||
margin: 0;
|
||||
border-radius: 50rem;
|
||||
&.btn-secondary {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,14 +115,16 @@ export default {
|
|||
&.btn-secondary {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 1px solid $red;
|
||||
border: 1px solid $blue;
|
||||
color: $blue;
|
||||
font-weight: 500;
|
||||
transition: all 150ms linear;
|
||||
height: 3rem;
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:active {
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
background: $blue;
|
||||
border: none;
|
||||
}
|
||||
&:focus, // Mouse, touch, stylus focus
|
||||
&:focus-visible {
|
||||
|
|
@ -130,16 +132,18 @@ export default {
|
|||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 3px $white,
|
||||
0 0 0 5.5px $red;
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
0 0 0 5.5px $blue;
|
||||
color: $blue;
|
||||
&:active {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
color: $gray-550 !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: 1px solid $red;
|
||||
border: 1px solid $blue;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
:max-length="12"
|
||||
v-model="selectedValue"
|
||||
:mask="mask"
|
||||
placeholderText="###-###-####"
|
||||
:isRequired="isRequired"
|
||||
:validationRules="validationRulesForTextBoxQuestion"
|
||||
:cmsWidgetName="cmsWidgetName" />
|
||||
|
|
|
|||
|
|
@ -214,6 +214,47 @@ export default {
|
|||
|
||||
&.is-insurance {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
<div class="px-0" v-if="isCartReadyToLoad">
|
||||
<div
|
||||
class="row vin-toggle flex align-items-center pt-4"
|
||||
:class="[isExpanded ? 'expanded' : '']"
|
||||
:class="[
|
||||
isCollapsible ? (isExpanded ? 'expanded' : '') : 'expanded non-collapsible',
|
||||
]"
|
||||
@click="toggleIsExpanded()">
|
||||
<a
|
||||
aria-label="expand cart"
|
||||
href="javascript:void(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">
|
||||
{{ getLineItemAmount(amountDue, showCoverageAsPending) }}
|
||||
</span>
|
||||
|
|
@ -208,6 +210,7 @@ export default {
|
|||
isItac: Boolean,
|
||||
isNoComp: Boolean,
|
||||
isExpandedOnLoad: Boolean,
|
||||
isCollapsible: { type: Boolean, default: true },
|
||||
isMSRFeeApplicable: Boolean,
|
||||
donationCartItem: Object,
|
||||
},
|
||||
|
|
@ -1094,9 +1097,19 @@ export default {
|
|||
amountDueText() {
|
||||
return this.getCmsContent("AmountDueTextWidget", "Text");
|
||||
},
|
||||
OrderDetailsText() {
|
||||
orderDetailsText() {
|
||||
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() {
|
||||
return this.getCmsContent("AmountPaidTextWidget", "Text");
|
||||
},
|
||||
|
|
@ -1182,6 +1195,11 @@ export default {
|
|||
font-weight: 500;
|
||||
line-height: 1.625;
|
||||
}
|
||||
.non-collapsible {
|
||||
.amount-due {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.amount-due {
|
||||
color: $green;
|
||||
|
||||
|
|
@ -1283,23 +1301,26 @@ export default {
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
.vin-toggle {
|
||||
&: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 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-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
right: 0.75rem;
|
||||
margin: 0.5rem 0 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.expanded:after {
|
||||
transform: rotate(180deg);
|
||||
&:not(.non-collapsible) {
|
||||
&: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 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-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
right: 0.75rem;
|
||||
margin: 0.5rem 0 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.expanded:after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded + .cart-panel {
|
||||
max-height: 650px;
|
||||
transition: all 150ms ease-in;
|
||||
|
|
@ -1307,6 +1328,13 @@ export default {
|
|||
padding: 1rem 0 0.5rem;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.non-collapsible {
|
||||
& > a {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-family: UrbanistSemibold;
|
||||
text-decoration: none;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
:ref="ModalName"
|
||||
:modalId="ModalName"
|
||||
:footerButtonText="ModalCloseButtonText"
|
||||
@footer-button-event="footerButtonClick">
|
||||
@footer-button-event="footerButtonClick"
|
||||
:isRecal="isRecal">
|
||||
<div :class="[isRecal ? 'recal-modal' : '']">
|
||||
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
|
||||
<h5 class="mb-4" v-html="ModalHeadline"></h5>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="funnel-header" v-if="imageSrc">
|
||||
<div class="funnel-header" :class="{ scrolled: isScrolled }" v-if="imageSrc">
|
||||
<div class="d-flex w-100">
|
||||
<progress-bar :page="$route.name" />
|
||||
</div>
|
||||
|
|
@ -61,6 +61,7 @@ export default {
|
|||
sierraChatOpen: false,
|
||||
salesforceChatOpen: false,
|
||||
isSalesforceTransferInProgress: false,
|
||||
isScrolled: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
|
@ -108,6 +109,15 @@ export default {
|
|||
}
|
||||
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) {
|
||||
event.preventDefault();
|
||||
this.launchWebchat();
|
||||
|
|
@ -185,6 +195,12 @@ export default {
|
|||
}
|
||||
};
|
||||
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() {
|
||||
window.removeEventListener("sierra-chat-closed", this._handleSierraChatClosed);
|
||||
|
|
@ -195,6 +211,7 @@ export default {
|
|||
);
|
||||
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
|
||||
window.removeEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
|
||||
window.removeEventListener("scroll", this._handleScroll);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -209,9 +226,18 @@ export default {
|
|||
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;
|
||||
|
||||
&.scrolled {
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.button-container {
|
||||
&.webchat {
|
||||
margin-right: 0.875rem;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
:deep(.bolded-words) {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
color: $black;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
h5 {
|
||||
line-height: 32px;
|
||||
|
|
@ -87,7 +90,7 @@ h5 {
|
|||
}
|
||||
|
||||
p.small {
|
||||
color: $gray-550;
|
||||
color: $gray-600;
|
||||
}
|
||||
.sub-text {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
suppressPageScroll
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalButtonText"
|
||||
:isFooterButtonPrimary="true"
|
||||
@footer-button-event="saveProgress">
|
||||
<p class="modal-body-inner" v-html="modalBodyText"></p>
|
||||
<saveProgressQuestion
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
suppressPageScroll
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalButtonText"
|
||||
:isFooterButtonPrimary="true"
|
||||
@footer-button-event="saveProgress">
|
||||
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
||||
<saveProgressQuestion
|
||||
|
|
|
|||
|
|
@ -82,3 +82,56 @@ export async function isGlassAvailableForCarId(carId, pageNameToLog) {
|
|||
|
||||
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();
|
||||
const closestShops = {
|
||||
data: {
|
||||
providers: [
|
||||
inShopProviders: [
|
||||
{ id: 1, name: "Shop 1" },
|
||||
{ id: 2, name: "Shop 2" },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ export default {
|
|||
this.serviceZipCode,
|
||||
this.carId
|
||||
);
|
||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
||||
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||
if (this.displayNoServiceAlert) {
|
||||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default {
|
|||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border-radius: $border-radius-pill;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
|
|
|||
|
|
@ -61,14 +61,15 @@
|
|||
:donationCartItem="donationLineItem"
|
||||
:showAsPaid="isPia"
|
||||
servicePackageOptionsCmsName="ServicePackageTitle"
|
||||
recyclingModalCmsWidgetName="RecycleModal"
|
||||
:isInsurance="isInsurance"
|
||||
:insuranceDeductible="currentDeductible"
|
||||
:insuranceCompanyName="insuranceCompanyName"
|
||||
:showInsuranceCoverageAs="showInsuranceCoverageAs"
|
||||
:shouldHideRecalibration="shouldHideRecalibration"
|
||||
:isExpandedOnLoad="false"
|
||||
:isItac="isItac"
|
||||
:isNoComp="isNoComp"
|
||||
:isExpandedOnLoad="false"
|
||||
:isMSRFeeApplicable="isMSRFeeApplicable" />
|
||||
|
||||
<hr class="mb-5" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
<div class="container page-container-grouped-styles">
|
||||
<div class="row">
|
||||
<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
|
||||
isRequired
|
||||
class="mb-4"
|
||||
|
|
@ -215,3 +218,8 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.form-check {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
@back-clicked="backButtonAction"
|
||||
@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>
|
||||
|
|
@ -268,6 +268,15 @@ export default {
|
|||
.header-sub-copy {
|
||||
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 {
|
||||
font-family: UrbanistSemibold;
|
||||
|
|
@ -277,5 +286,6 @@ export default {
|
|||
.select-option {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<textboxQuestion
|
||||
v-model="selectedAccountName"
|
||||
ref="insuranceCoQuestion"
|
||||
customInputId="autocomplete"
|
||||
class="mb-4 mt-5"
|
||||
cmsWidgetName="InsuranceCoQuestionWidget"
|
||||
includeSearchIcon
|
||||
:validationRules="validationRules" />
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="container page-container-grouped-styles">
|
||||
<div class="row">
|
||||
<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">
|
||||
<insurance-company-question
|
||||
|
|
|
|||
|
|
@ -722,7 +722,7 @@ function setupMocks({
|
|||
wrapper.vm.$refs.navbar.removeLoader = jest.fn();
|
||||
const closestShops = {
|
||||
data: {
|
||||
providers: [
|
||||
inShopProviders: [
|
||||
{ id: 1, name: "Shop 1" },
|
||||
{ id: 2, name: "Shop 2" },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export default {
|
|||
this.serviceZipCode,
|
||||
this.carId
|
||||
);
|
||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
||||
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||
if (this.displayNoServiceAlert) {
|
||||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@
|
|||
</div>
|
||||
</transition>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="row mb-4" aria-live="polite">
|
||||
<div class="col-8">
|
||||
<div class="mb-4" aria-live="polite">
|
||||
<div class="col mb-4">
|
||||
<dropdownQuestion
|
||||
customDropdownId="state"
|
||||
cmsWidgetName="StateQuestionWidget"
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
:labelBold="labelBold"
|
||||
:isDisabled="this.isStateDisabled" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
customInputId="zipCode"
|
||||
cmsWidgetName="ZipQuestionWidget"
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ beforeEach(() => {
|
|||
describe("mobile-details.vue", () => {
|
||||
test("if the continue button is clicked, navigate forward", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks(mobileDetails, {
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
|
@ -91,7 +91,7 @@ describe("mobile-details.vue", () => {
|
|||
});
|
||||
test("arePagePrerequisitesValid should be true ", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks(mobileDetails, {
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
|
@ -105,7 +105,7 @@ describe("mobile-details.vue", () => {
|
|||
|
||||
test("if the back button is clicked, navigate back", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks(mobileDetails, {
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,11 +7,26 @@
|
|||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<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" />
|
||||
<hr class="my-3" />
|
||||
<div class="mobile-location-questions">
|
||||
<div class="address-questions-container">
|
||||
<textBlock
|
||||
cmsWidgetName="PleaseProvideAddressWidget"
|
||||
class="provide-address-header" />
|
||||
<textBlock
|
||||
cmsWidgetName="VehicleKeysMessageWidget"
|
||||
class="keys-message" />
|
||||
<mobileAddressQuestions
|
||||
ref="addressQuestions"
|
||||
v-model="this.addressQuestions"
|
||||
|
|
@ -56,6 +71,8 @@ import store from "@/store";
|
|||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
export default {
|
||||
name: "MobileDetails",
|
||||
data() {
|
||||
|
|
@ -142,6 +159,17 @@ export default {
|
|||
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() {
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_BACK,
|
||||
|
|
@ -156,6 +184,9 @@ export default {
|
|||
AppointmentTimeOfService() {
|
||||
return this.getCmsContent("AppointmentDateAndTime", "Text");
|
||||
},
|
||||
editLink() {
|
||||
return this.getCmsContent("EditLinkWidget", "Text");
|
||||
},
|
||||
},
|
||||
components: {
|
||||
mobileAddressQuestions,
|
||||
|
|
@ -166,6 +197,7 @@ export default {
|
|||
funnelSubHeader,
|
||||
Form,
|
||||
loadingModal,
|
||||
textLink,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -175,5 +207,40 @@ export default {
|
|||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export default {
|
|||
.question-text span {
|
||||
text-align: left;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@
|
|||
<contentGroupModal
|
||||
ref="RecalModal"
|
||||
cmsWidgetName="RecalModal"
|
||||
class="recal-modal" />
|
||||
class="recal-modal"
|
||||
isRecal />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -222,7 +223,8 @@ export default {
|
|||
"payment-method"
|
||||
);
|
||||
|
||||
const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
|
||||
// const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
|
||||
// (removed temporarily for Heritage parity effort)
|
||||
|
||||
const promiseResultMap = [
|
||||
{
|
||||
|
|
@ -237,10 +239,11 @@ export default {
|
|||
resultKey: "rainDefense",
|
||||
promise: rainDefensePromise,
|
||||
},
|
||||
{
|
||||
resultKey: "reviewDropdownData",
|
||||
promise: reviewDropdownPromise,
|
||||
},
|
||||
// {
|
||||
// resultKey: "reviewDropdownData",
|
||||
// promise: reviewDropdownPromise,
|
||||
// },
|
||||
// (removed temporarily for Heritage parity effort)
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
|
@ -345,7 +348,8 @@ export default {
|
|||
);
|
||||
vm.updateFooterButtonText(vm.customCtaCopy);
|
||||
|
||||
vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
|
||||
// vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
|
||||
// (removed temporarily for Heritage parity effort)
|
||||
|
||||
if (revalidatePromoResponse) {
|
||||
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||
|
|
@ -751,15 +755,15 @@ export default {
|
|||
customCtaCopy() {
|
||||
switch (this.paymentMethod) {
|
||||
case paymentMethods.PAY_NOW:
|
||||
return "Continue to checkout";
|
||||
return "Continue"; // Before Heritage Parity it was "Continue to checkout"
|
||||
case paymentMethods.INSURANCE:
|
||||
return "Continue to insurance";
|
||||
return "Continue"; // Before Heritage Parity it was "Continue to insurance"
|
||||
case paymentMethods.CREDIT_CARD:
|
||||
return "Continue to checkout";
|
||||
return "Continue"; // Before Heritage Parity it was "Continue to checkout"
|
||||
case paymentMethods.PAYPAL:
|
||||
return "Continue to Paypal";
|
||||
return "Continue"; // Before Heritage Parity it was "Continue to Paypal"
|
||||
case paymentMethods.AFTERPAY:
|
||||
return "Continue to Afterpay";
|
||||
return "Continue"; // Before Heritage Parity it was "Continue to Afterpay"
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -846,6 +850,18 @@ export default {
|
|||
(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);
|
||||
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
||||
} else if (
|
||||
|
|
@ -891,6 +907,9 @@ export default {
|
|||
text-transform: lowercase;
|
||||
}
|
||||
}
|
||||
:deep(.review-table p) {
|
||||
margin: 0.25rem 0 0 0;
|
||||
}
|
||||
}
|
||||
.checkbox-group {
|
||||
align-items: start;
|
||||
|
|
@ -932,17 +951,31 @@ export default {
|
|||
:deep(.modal-body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: visible !important;
|
||||
|
||||
p.subheader-text {
|
||||
color: $red;
|
||||
order: 1 !important;
|
||||
margin-top: -2.25rem;
|
||||
}
|
||||
h5 {
|
||||
text-align: center;
|
||||
order: 1;
|
||||
text-align: left !important;
|
||||
order: 2 !important;
|
||||
}
|
||||
p {
|
||||
order: 3;
|
||||
order: 4;
|
||||
}
|
||||
img {
|
||||
order: 2;
|
||||
order: 3 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(span.label) {
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(div.service-questions span) {
|
||||
color: $black;
|
||||
font-family: UrbanistSemibold;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,28 @@
|
|||
<div v-html="apptWordingText" v-show="!isExpanded"></div>
|
||||
|
||||
<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" />
|
||||
|
||||
<hr class="my-0" />
|
||||
|
|
@ -48,6 +70,7 @@
|
|||
<hr class="my-0" />
|
||||
|
||||
<customerReview cmsWidgetName="CustomerReviewWidget" :customer="customerInfo" />
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -56,8 +79,11 @@
|
|||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
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 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 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 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";
|
||||
|
|
@ -69,6 +95,7 @@ export default {
|
|||
props: {
|
||||
apptWordingText: String,
|
||||
serviceLocationFullAddress: String,
|
||||
damageInfoHeader: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -76,21 +103,22 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
async loadInitialData() {
|
||||
const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData();
|
||||
// the following was used prior to Heritage parity
|
||||
// async loadInitialData() {
|
||||
// const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData();
|
||||
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "servicePackageData",
|
||||
promise: servicePackageInitialDataPromise,
|
||||
},
|
||||
];
|
||||
// const promiseResultMap = [
|
||||
// {
|
||||
// resultKey: "servicePackageData",
|
||||
// promise: servicePackageInitialDataPromise,
|
||||
// },
|
||||
// ];
|
||||
|
||||
return settleAllPromises(promiseResultMap);
|
||||
},
|
||||
initializeComponent(apiResponses) {
|
||||
this.$refs.servicePackageReview.initializeComponent(apiResponses.servicePackageData);
|
||||
},
|
||||
// return settleAllPromises(promiseResultMap);
|
||||
// },
|
||||
// initializeComponent(apiResponses) {
|
||||
// this.$refs.servicePackageReview.initializeComponent(apiResponses.servicePackageData);
|
||||
// },
|
||||
toggleIsExpanded() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
},
|
||||
|
|
@ -117,11 +145,9 @@ export default {
|
|||
},
|
||||
components: {
|
||||
customerReview,
|
||||
damageReview,
|
||||
scheduleReview,
|
||||
serviceLocationReview,
|
||||
servicePackageReview,
|
||||
vehicleReview,
|
||||
optInReview,
|
||||
appointmentReview,
|
||||
parityDamageReview,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -138,6 +164,10 @@ export default {
|
|||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
|
||||
& > :first-child {
|
||||
margin-top: -0.75rem;
|
||||
}
|
||||
|
||||
:deep(.service-location) {
|
||||
.review-block-content {
|
||||
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: {
|
||||
firstName: "First",
|
||||
lastName: "Last",
|
||||
// firstName: "First",
|
||||
// lastName: "Last",
|
||||
phoneNumber: "111-111-1111",
|
||||
emailAddress: "builddigitaltest@safelite.com",
|
||||
isSmsOptIn: false,
|
||||
// isSmsOptIn: false,
|
||||
},
|
||||
displayContent: {
|
||||
fullName: "First Last",
|
||||
// fullName: "First Last",
|
||||
phoneNumber: "111-111-1111",
|
||||
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);
|
||||
});
|
||||
|
||||
test("Should display sms text from cms", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
// test("Should display sms text from cms", async () => {
|
||||
// // Arrange
|
||||
// let props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
// const { wrapper } = setupMocks({
|
||||
// propsData: props,
|
||||
// });
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$nextTick();
|
||||
// // Act
|
||||
// await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
|
||||
});
|
||||
// // Assert
|
||||
// expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
|
||||
// });
|
||||
|
||||
test("Should render correct display content", async () => {
|
||||
// Arrange
|
||||
|
|
@ -82,10 +82,9 @@ describe("Customer Review Block", () => {
|
|||
|
||||
// Assert
|
||||
expect(wrapper.vm.displayContent).toEqual([
|
||||
testConstants.displayContent.fullName,
|
||||
testConstants.displayContent.emailAddress,
|
||||
testConstants.displayContent.phoneNumber,
|
||||
testConstants.displayContent.smsOptIn,
|
||||
// testConstants.displayContent.smsOptIn,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -94,11 +93,11 @@ function generateDefaultProps() {
|
|||
return {
|
||||
cmsWidgetName: "CustomerWidget",
|
||||
customer: {
|
||||
firstName: testConstants.customer.firstName,
|
||||
lastName: testConstants.customer.lastName,
|
||||
// firstName: testConstants.customer.firstName,
|
||||
// lastName: testConstants.customer.lastName,
|
||||
phoneNumber: testConstants.customer.phoneNumber,
|
||||
emailAddress: testConstants.customer.emailAddress,
|
||||
isSmsOptIn: testConstants.customer.isSmsOptIn,
|
||||
// isSmsOptIn: testConstants.customer.isSmsOptIn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
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() {
|
||||
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"
|
||||
:isItac="isItac"
|
||||
:isNoComp="isNoComp"
|
||||
:isExpandedOnLoad="true" />
|
||||
:isCollapsible="false" />
|
||||
|
||||
<button
|
||||
v-if="showSwitchPaymentMethod"
|
||||
|
|
@ -919,10 +919,6 @@ export default {
|
|||
width: 100%;
|
||||
min-height: 650px;
|
||||
border: 0 none;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
min-height: 1688px;
|
||||
}
|
||||
}
|
||||
#pageLoadingModal {
|
||||
background-color: red;
|
||||
|
|
@ -936,16 +932,18 @@ export default {
|
|||
}
|
||||
& > #cart-container {
|
||||
flex-basis: 36%;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
flex-direction: column;
|
||||
& > iframe {
|
||||
flex-basis: 100%;
|
||||
flex-basis: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
& > #cart-container {
|
||||
flex-basis: 100%;
|
||||
flex-basis: auto;
|
||||
margin: 0 0.5rem;
|
||||
|
||||
& > button#submit-payment {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
<span v-else>{{ getInlineAltText(token) }}</span>
|
||||
</span>
|
||||
<span v-else v-html="token"></span>
|
||||
<span class="nbsp"> </span>
|
||||
</span>
|
||||
|
||||
<a
|
||||
id="afterpay-learnmore"
|
||||
href="#"
|
||||
|
|
@ -139,7 +139,7 @@ export default {
|
|||
line-height: 2rem;
|
||||
@include media-breakpoint-down(md) {
|
||||
border-bottom: 1px solid;
|
||||
padding-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
||||
|
|
@ -170,4 +170,8 @@ export default {
|
|||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
#afterpay-learnmore {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
alertClass="alert-info" />
|
||||
|
||||
<promoModalQuestion
|
||||
class="mt-6 d-flex"
|
||||
class="mt-4 d-flex"
|
||||
v-model="lineItems"
|
||||
:addableVaps="addableVaps"
|
||||
pageName="quote"
|
||||
|
|
@ -67,11 +67,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<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="FrontWiperModal" cmsWidgetName="FrontWiperModal" />
|
||||
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
|
||||
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" isRecal />
|
||||
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
|
||||
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
|
|
@ -902,21 +902,19 @@ export default {
|
|||
.quote {
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.nav-container {
|
||||
/* Create correct positioning point for .nav-bar a.navigation-link */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.nav-bar) {
|
||||
position: initial;
|
||||
button.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
||||
@media only screen and (min-width: 340px) {
|
||||
.col button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
a.navigation-link {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
bottom: -1rem;
|
||||
bottom: 1rem;
|
||||
}
|
||||
.col-auto {
|
||||
width: -webkit-fill-available;
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
.start-over {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
a {
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
|
|
@ -114,7 +115,7 @@ export default {
|
|||
}
|
||||
.return-user {
|
||||
.return-user-spacing {
|
||||
margin-top: 14rem;
|
||||
margin-top: 8rem;
|
||||
}
|
||||
}
|
||||
: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="row">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<alert
|
||||
ref="alertMobileFeeFree"
|
||||
class="mb-5"
|
||||
cmsWidgetName="AlertMobileFeeFreeWidget"
|
||||
v-if="showMobileFreeAlert"
|
||||
alertClass="alert-success" />
|
||||
<mobileFeeWaiverAlert cmsWidgetName="MobileFeeFreeDropdownWidget" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row appointment-type">
|
||||
<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
|
||||
v-model="appointmentTypeFromAppointmentTypeQuestion"
|
||||
v-show="isAppointmentTypeDisplayed"
|
||||
|
|
@ -118,6 +115,15 @@
|
|||
<div class="row date-picker-wrapper">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<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
|
||||
:currentZip="zipCode"
|
||||
:currentProviderNumber="selectedProvider?.providerNumber"
|
||||
|
|
@ -134,14 +140,25 @@
|
|||
:pricingByDayBasePrice="pricingByDayBasePrice"
|
||||
:pricingByDayUpcharge="pricingByDayUpcharge"
|
||||
:showPricingByDay="showPricingByDay"
|
||||
:isPricingByDayExperiment="isPricingByDayExperiment"
|
||||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
||||
:isPricingByDayExperiment="isPricingByDayExperiment" />
|
||||
<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"
|
||||
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
||||
:estimatedServiceMinutesMinimum="getServiceMinutesMin"
|
||||
:estimatedServiceMinutesMaximum="getServiceMinutesMax"
|
||||
@timeSlotSelected="updateTimeSlot"
|
||||
:displayWaitList="displayWaitList"
|
||||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
||||
:isSameDay="isSameDay"
|
||||
:selectedRouteCodeData="selectedRouteCodeData"
|
||||
@waitListRequested="handleWaitListRequested" />
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
|
|
@ -160,9 +177,10 @@
|
|||
import alert from "@/ux-components/alert/alert";
|
||||
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 shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
|
||||
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 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 contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import mobileFeeWaiverAlert from "./mobile-fee-waiver-alert/mobile-fee-waiver-alert.vue";
|
||||
|
||||
// Supporting files
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
|
@ -626,6 +645,9 @@ export default {
|
|||
return this.isGlassServiceableMobile;
|
||||
}
|
||||
},
|
||||
showTimeSlotQuestion() {
|
||||
return this.selectedDate && this.appointmentTypeFromAppointmentTypeQuestion;
|
||||
},
|
||||
isMobileStaticRecalibrationApplicable() {
|
||||
return (
|
||||
this.displayMSR &&
|
||||
|
|
@ -785,6 +807,21 @@ export default {
|
|||
}
|
||||
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() {
|
||||
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
|
||||
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
|
||||
},
|
||||
getServiceMinutesMin() {
|
||||
estimatedServiceMinutesMinimum() {
|
||||
return this.isMobileSelected
|
||||
? this.selectableDatesMobile.estimatedServiceMinutesMinimum
|
||||
: this.selectableDatesInshop.estimatedServiceMinutesMinimum;
|
||||
},
|
||||
getServiceMinutesMax() {
|
||||
estimatedServiceMinutesMaximum() {
|
||||
return this.isMobileSelected
|
||||
? this.selectableDatesMobile.estimatedServiceMinutesMaximum
|
||||
: this.selectableDatesInshop.estimatedServiceMinutesMaximum;
|
||||
|
|
@ -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: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
|
|
@ -1238,7 +1287,9 @@ export default {
|
|||
return store.getters.lineItems.supportingItems;
|
||||
},
|
||||
updateFooterButtonText(timeSlotInfo) {
|
||||
let navbarButtonText;
|
||||
let navbarButtonText = "Continue";
|
||||
// Archiving the following in case we revert back from Heritage parity
|
||||
/*
|
||||
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
|
||||
navbarButtonText = "Continue";
|
||||
} else {
|
||||
|
|
@ -1273,6 +1324,7 @@ export default {
|
|||
)}`;
|
||||
}
|
||||
}
|
||||
*/
|
||||
this.$refs.navbar.updateButtonText(navbarButtonText);
|
||||
},
|
||||
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
||||
|
|
@ -1555,22 +1607,46 @@ export default {
|
|||
}
|
||||
},
|
||||
updateTimeSlot(timeSlotObj) {
|
||||
if (this.preSelectedDate && !timeSlotObj?.timeSlot?.date) return;
|
||||
this.selectedTimeSlotInfo = timeSlotObj;
|
||||
if (!timeSlotObj?.routeCode) {
|
||||
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 (
|
||||
this.appointmentType !== AppointmentTypeStrings.MOBILE &&
|
||||
this.appointmentType !== null
|
||||
) {
|
||||
// update apptType based on routeCode to determine if it should be dropoff or inshop
|
||||
debugLog(`Updating appointment type based on route code.`);
|
||||
debugLog(`Route code =`, timeSlotObj.timeSlot.routeCode);
|
||||
debugLog(
|
||||
`Parsed type = `,
|
||||
this.getInShopOrDropOffApptType(timeSlotObj.timeSlot.routeCode)
|
||||
);
|
||||
this.appointmentType = this.getInShopOrDropOffApptType(
|
||||
timeSlotObj.timeSlot.routeCode
|
||||
);
|
||||
debugLog(`Route code =`, timeSlotObj.routeCode);
|
||||
debugLog(`Parsed type = `, this.getInShopOrDropOffApptType(timeSlotObj.routeCode));
|
||||
this.appointmentType = this.getInShopOrDropOffApptType(timeSlotObj.routeCode);
|
||||
}
|
||||
},
|
||||
getInShopOrDropOffApptType(routeCode) {
|
||||
|
|
@ -1618,7 +1694,7 @@ export default {
|
|||
},
|
||||
};
|
||||
} else if (
|
||||
!this.selectedProvider?.address?.streetAddress &&
|
||||
(!this.selectedProvider?.address?.streetAddress || this.isVehicleHeavyTruck) &&
|
||||
this.shopProviderData?.shopProviders?.length
|
||||
) {
|
||||
this.selectedProvider = this.shopProviderData.shopProviders[0];
|
||||
|
|
@ -1737,12 +1813,15 @@ export default {
|
|||
datePicker,
|
||||
locationAlerts,
|
||||
textBlock,
|
||||
timeSlotQuestion,
|
||||
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
appointmentTypeQuestion,
|
||||
contentGroupModal,
|
||||
durationTextBlock,
|
||||
shopLocation,
|
||||
mobileFeeWaiverAlert,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1777,7 +1856,7 @@ export default {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.funnel-sub-header:not(.mb-5)) {
|
||||
:deep(.funnel-sub-header:not(#schedule-your-service)) {
|
||||
h5.dark-header {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
|
|
@ -1791,7 +1870,7 @@ export default {
|
|||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.text-block.duration-text-block b {
|
||||
:deep(.text-block.duration-text-block b) {
|
||||
font-family: UrbanistSemibold;
|
||||
}
|
||||
:deep(.alert-warning.widget-name-AlertRecalNoMobileWidget .alert-heading),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<buttonQuestion
|
||||
v-if="isDropOffAppointmentAvailable"
|
||||
v-model="selectedAnswerForDropOffOrInshop"
|
||||
@update:modelValue="dropOffSelectionChanged"
|
||||
:questionText="dropOffOrPickATimeQuestionLabelText"
|
||||
:questionTextDescription="dropOffOrPickATimeQuestionDescription"
|
||||
:answers="answersForDropOffQuestion"
|
||||
|
|
@ -15,7 +16,7 @@
|
|||
<alert
|
||||
v-if="shouldDisplayDropOffAlert"
|
||||
ref="alertDropoffInformation"
|
||||
class="mb-4 drop-off-alert"
|
||||
class="mt-4 mb-4 drop-off-alert"
|
||||
:cmsWidgetName="dropOffTimeSlotAlertCMSWidgetName"
|
||||
alertClass="alert-info"
|
||||
v-bind:isDismissible="false" />
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
v-if="shouldDisplayTimeSlotQuestion"
|
||||
@update:modelValue="timeSlotSelectionChanged"
|
||||
buttonTypeString="timeSlotModalListButton"
|
||||
:buttonTypeObject="timeSlotModalListButton"
|
||||
class="mt-4 timeslots"
|
||||
|
|
@ -44,7 +46,7 @@
|
|||
v-if="supplementalInformationBlock"
|
||||
v-html="supplementalInformationBlock" />
|
||||
<div
|
||||
v-if="hasWaitListExperiment && displayWaitListFeature && selectedDate"
|
||||
v-if="hasWaitListExperiment && displayWaitListFeature"
|
||||
class="mt-5 bg-light rounded waitlist">
|
||||
<textBlock
|
||||
cmsWidgetName="WaitListLabelWidget"
|
||||
|
|
@ -59,7 +61,7 @@
|
|||
@click="waitListChecked" />
|
||||
</div>
|
||||
<div
|
||||
v-if="waitListRequested && displayWaitList && selectedDate"
|
||||
v-if="waitListRequested && displayWaitList"
|
||||
class="rounded waitlist-success"
|
||||
ref="waitlistSuccessMessage">
|
||||
<img :src="waitListSuccessImage" class="success-image" />
|
||||
|
|
@ -92,7 +94,6 @@ import {
|
|||
isDropOffRouteCode,
|
||||
militaryToTwelveHourTime,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
||||
|
||||
// Validation
|
||||
import { defineRule, useField } from "vee-validate";
|
||||
|
|
@ -116,23 +117,8 @@ defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
|||
|
||||
export default {
|
||||
name: "time-slot-question",
|
||||
emits: ["update:modelValue", "TimeSlotSelected", "click-event"],
|
||||
emits: ["update:modelValue", "time-slot-selection-changed"],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
timeSlot: {
|
||||
routeCode: null,
|
||||
date: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null,
|
||||
},
|
||||
isPremiumAppointment: null,
|
||||
}),
|
||||
},
|
||||
cmsWidgetName: String,
|
||||
mobileCmsWidgetName: String,
|
||||
mobilePremiumCmsWidgetName: String,
|
||||
dropoffCmsWidgetName: String,
|
||||
|
|
@ -142,21 +128,23 @@ export default {
|
|||
appointmentType: String,
|
||||
timeSlotsForSelectedDate: Object,
|
||||
premiumAppointmentFee: Object,
|
||||
estimatedServiceMinutesMinimum: Number,
|
||||
estimatedServiceMinutesMaximum: Number,
|
||||
validationRules: String,
|
||||
customComponentId: String,
|
||||
selectedDate: String,
|
||||
isSameDay: Boolean,
|
||||
displayWaitList: Boolean,
|
||||
selectedRouteCodeData: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedRouteCode: null,
|
||||
timeSlotModalListButton: timeSlotModalListButton,
|
||||
selectedAnswerForDropOffOrInshop: null,
|
||||
selectedAnswerForTimeSlots: null,
|
||||
selectedAnswerForDropOffOrInshop: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(this.selectedRouteCodeData);
|
||||
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
const componentId = !props.customComponentId
|
||||
|
|
@ -186,20 +174,33 @@ export default {
|
|||
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: {
|
||||
supplementalInformationBlock() {
|
||||
let appointmentTypeCmsWidgetName;
|
||||
|
||||
if (
|
||||
this.selectedDate == null ||
|
||||
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
|
||||
this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
|
||||
this.appointmentType === AppointmentTypeStrings.DROP_OFF ||
|
||||
!this.availableTimeSlots
|
||||
) {
|
||||
if (!this.selectedAnswerForTimeSlots && !this.selectedAnswerForDropOffOrInshop) {
|
||||
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
||||
}
|
||||
return null;
|
||||
// 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
|
||||
|
|
@ -214,11 +215,7 @@ export default {
|
|||
// );
|
||||
// }
|
||||
} else {
|
||||
if (!this.selectedAnswerForTimeSlots && this.selectedRouteCode) {
|
||||
// Clearing selectedRouteCode if no time slot is selected
|
||||
this.resetSelectedTimeSlot();
|
||||
}
|
||||
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(
|
||||
appointmentTypeCmsWidgetName = this.selectedRouteCodeData?.routeCode?.includes(
|
||||
PREMIUM_TIME_SLOT_ID_FLAG
|
||||
)
|
||||
? this.mobilePremiumCmsWidgetName
|
||||
|
|
@ -299,13 +296,6 @@ export default {
|
|||
// return null;
|
||||
// }
|
||||
// },
|
||||
isSameDay() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return false;
|
||||
}
|
||||
const todaysDate = new Date().toISOString().split("T")[0];
|
||||
return this.selectedDate === todaysDate;
|
||||
},
|
||||
availableTimeSlots() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return null;
|
||||
|
|
@ -407,29 +397,60 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
initializeComponent() {
|
||||
this.setSelectedRouteCodeFromParent();
|
||||
},
|
||||
async setSelectedTimeSlot() {
|
||||
this.selectedRouteCode =
|
||||
this.selectedAnswerForTimeSlots || this.selectedAnswerForDropOffOrInshop;
|
||||
this.$emit(
|
||||
"update:modelValue",
|
||||
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode)
|
||||
);
|
||||
},
|
||||
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;
|
||||
updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(selectedRouteCodeData) {
|
||||
if (selectedRouteCodeData?.routeCode) {
|
||||
let routeCode = selectedRouteCodeData.routeCode;
|
||||
if (isDropOffRouteCode(routeCode)) {
|
||||
this.selectedAnswerForDropOffOrInshop = routeCode;
|
||||
} else {
|
||||
this.selectedAnswerForDropOffOrInshop = PICK_A_TIME_BUTTON_VALUE;
|
||||
if (this.selectedRouteCodeData.isPremiumAppointment) {
|
||||
routeCode = this.addPremiumFlagToInput(routeCode);
|
||||
}
|
||||
this.selectedAnswerForTimeSlots = routeCode;
|
||||
}
|
||||
} else {
|
||||
this.selectedAnswerForDropOffOrInshop = null;
|
||||
this.selectedAnswerForTimeSlots = null;
|
||||
}
|
||||
},
|
||||
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) {
|
||||
return timeSlotsForSelectedDate
|
||||
.filter((timeSlot) => {
|
||||
|
|
@ -463,8 +484,6 @@ export default {
|
|||
);
|
||||
}
|
||||
|
||||
this.resetSelectedTimeSlotIfDateChange(timeSlotsForSelectedDate);
|
||||
|
||||
return availableTimeSlots;
|
||||
},
|
||||
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() {
|
||||
return store.getters.order.customer?.waitListRequested;
|
||||
},
|
||||
autoSelectTimeSlotIfOnlyOneIsAvailable() {
|
||||
const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length;
|
||||
if (this.appointmentType && numberOfOptions === 1) {
|
||||
if (numberOfOptions === 1) {
|
||||
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;
|
||||
} else {
|
||||
this.selectedAnswerForDropOffOrInshop = this.answersForDropOffQuestion[0].value;
|
||||
}
|
||||
this.setSelectedTimeSlot();
|
||||
}
|
||||
},
|
||||
addPremiumFlagToInput(routeCode) {
|
||||
|
|
@ -517,41 +520,6 @@ export default {
|
|||
removePremiumFlagFromInput(routeCode) {
|
||||
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) {
|
||||
this.$emit("waitListRequested", event.target.checked);
|
||||
},
|
||||
|
|
@ -561,65 +529,6 @@ export default {
|
|||
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: {
|
||||
textBlock,
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export default {
|
|||
"service-location"
|
||||
);
|
||||
|
||||
if (!closestShops || closestShops.providers?.length === 0) {
|
||||
if (!closestShops || closestShops.inShopProviders?.length === 0) {
|
||||
this.displayNoServiceAlert = true;
|
||||
this.focusOnZipInput();
|
||||
this.resetModalButtonStyle();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
</transition>
|
||||
<modal
|
||||
ref="shopQuestionModal"
|
||||
class="shop-question-popup"
|
||||
:headerText="modalHeaderText"
|
||||
:onModalOpenedCallback="updateSelectedAnswer"
|
||||
:onModalClosedCallback="resetZipCodeData"
|
||||
|
|
@ -290,7 +291,7 @@ export default {
|
|||
this.$store.getters.order.vehicle.carId,
|
||||
"service-location"
|
||||
);
|
||||
if (!closestShops || closestShops.providers?.length === 0) {
|
||||
if (!closestShops || closestShops.inShopProviders?.length === 0) {
|
||||
this.isLoading = false;
|
||||
this.displayNoServiceAlert = true;
|
||||
return null;
|
||||
|
|
@ -392,6 +393,25 @@ export default {
|
|||
.modal.modal-component .modal-dialog .modal-content .modal-body .textbox-question {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ export default {
|
|||
this.serviceZipCode,
|
||||
this.carId
|
||||
);
|
||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
||||
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||
if (this.displayNoServiceAlert) {
|
||||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,6 +238,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
html .has-error.list-card:hover {
|
||||
border-radius: 50rem;
|
||||
}
|
||||
.vehicle-parts {
|
||||
label {
|
||||
&.list-card {
|
||||
|
|
|
|||
|
|
@ -496,7 +496,10 @@ export default {
|
|||
{ zip: this.serviceZipCode, carId: this.carId },
|
||||
"vehicle"
|
||||
);
|
||||
if (!closestShops?.data?.providers || closestShops.data.providers.length === 0) {
|
||||
if (
|
||||
!closestShops?.data?.inShopProviders ||
|
||||
closestShops.data.inShopProviders.length === 0
|
||||
) {
|
||||
this.displayNoServiceAlert = true;
|
||||
if (store.getters.externalParameterState?.isExternalParameter) {
|
||||
return baseMixin.methods.ResetExternalParamsAndHideModal();
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ export default {
|
|||
this.serviceZipCode,
|
||||
this.carId
|
||||
);
|
||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
||||
this.displayNoServiceAlert = !closestShops?.data?.inShopProviders?.length;
|
||||
if (this.displayNoServiceAlert) {
|
||||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -695,6 +695,7 @@ export const mutations = {
|
|||
vehicleSubType: sessionInformation.order.vehicle?.vehicleSubType,
|
||||
vehicleSpecialClass: sessionInformation.order.vehicle?.vehicleSpecialClass,
|
||||
isBigTruck: sessionInformation.order.vehicle?.isBigTruck,
|
||||
canSafeliteService: sessionInformation.order.vehicle?.canSafeliteService,
|
||||
});
|
||||
|
||||
state.order.damage.glassToReplace = sessionInformation.order.damage.glassToReplace;
|
||||
|
|
|
|||
|
|
@ -63,9 +63,6 @@ export default {
|
|||
|
||||
<style lang="scss">
|
||||
.btn {
|
||||
&:first-child:active {
|
||||
background-color: $red;
|
||||
}
|
||||
&.btn-primary {
|
||||
position: relative;
|
||||
background: $red;
|
||||
|
|
@ -117,8 +114,8 @@ export default {
|
|||
&.btn-secondary {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 1px solid $red;
|
||||
color: $red;
|
||||
border: 1px solid $blue;
|
||||
color: $blue;
|
||||
font-weight: 500;
|
||||
transition: all 150ms linear;
|
||||
height: 3rem;
|
||||
|
|
@ -131,8 +128,11 @@ export default {
|
|||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 3px $white,
|
||||
0 0 0 5.5px $red;
|
||||
color: $white;
|
||||
0 0 0 5.5px $blue;
|
||||
color: $blue;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export default {
|
|||
color: $black;
|
||||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
border: 1px solid $blue;
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
|
|
|
|||
|
|
@ -32,11 +32,23 @@ parameters:
|
|||
- name: imageTag
|
||||
type: string
|
||||
default: '$(Build.BuildId)'
|
||||
- name: dockerBuildArgs
|
||||
type: string
|
||||
default: ''
|
||||
- name: envFileName
|
||||
type: string
|
||||
default: '.env.ci'
|
||||
- name: applicationType
|
||||
type: string
|
||||
default: 'vue'
|
||||
values: ['vue', 'dotnet']
|
||||
- name: readinessEndpoint
|
||||
type: string
|
||||
default: '/healthcheck'
|
||||
- name: serverMaxAttempts
|
||||
type: number
|
||||
default: 60
|
||||
|
||||
|
||||
jobs:
|
||||
- job: playwright_tests
|
||||
continueOnError: true
|
||||
|
|
@ -103,6 +115,14 @@ jobs:
|
|||
echo "Docker cleanup completed!"
|
||||
displayName: "Docker Cleanup"
|
||||
|
||||
- bash: |
|
||||
printenv > "${{ parameters.envFileName }}"
|
||||
if [ -z "$(GITHUB_TOKEN)" ]; then
|
||||
echo "Printed env, but GITHUB_TOKEN was undefined"
|
||||
fi
|
||||
env: ${{ parameters.secrets }}
|
||||
displayName: "Make Azure Pipeline Variables Available to Docker"
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Build Docker Image'
|
||||
inputs:
|
||||
|
|
@ -110,20 +130,23 @@ jobs:
|
|||
dockerfile: ${{ parameters.dockerFileName }}
|
||||
repository: ${{ parameters.dockerImageName }}
|
||||
tags: ${{ parameters.imageTag }}
|
||||
arguments: '--no-cache --pull'
|
||||
arguments: |
|
||||
--no-cache --pull ${{ parameters.dockerBuildArgs }}
|
||||
|
||||
- script: |
|
||||
printenv > "${{ parameters.envFileName }}"
|
||||
|
||||
# Determine JIRA card number and filters
|
||||
JIRA_CARD_NUMBER=""
|
||||
IS_REGRESSION="${{ parameters.isRegression }}"
|
||||
echo "isRegression: $IS_REGRESSION";
|
||||
echo "isRegression: $IS_REGRESSION"
|
||||
|
||||
if [[ "${{ parameters.isRegression }}" == "False" ]]; then
|
||||
branch_name=$(System.PullRequest.SourceBranch)
|
||||
branch_name="$(System.PullRequest.SourceBranch)"
|
||||
echo "Retrieved branch name: '$branch_name'"
|
||||
JIRA_CARD_NUMBER="${branch_name##*/}"
|
||||
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
|
||||
fi
|
||||
|
||||
# Build test filter
|
||||
FILTER=""
|
||||
if [[ -n "${{ parameters.filterTags }}" && -n "$JIRA_CARD_NUMBER" ]]; then
|
||||
echo "Filtering by filterTags and Jira Card Number..."
|
||||
|
|
@ -140,57 +163,213 @@ jobs:
|
|||
fi
|
||||
|
||||
echo "Filter value: $FILTER"
|
||||
TARGET_URL=${{ parameters.targetUrl }}
|
||||
echo "BASE_URL value: $BASE_URL"
|
||||
TARGET_URL="${{ parameters.targetUrl }}"
|
||||
echo "Target URL: $TARGET_URL"
|
||||
echo "Application Type: ${{ parameters.applicationType }}"
|
||||
|
||||
# Create container and run tests
|
||||
# Set common variables
|
||||
PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
|
||||
SERVE_PATH="${{ parameters.npmServePath }}"
|
||||
SHARD_NUMBER=$(shardNumber)
|
||||
TOTAL_SHARDS=${{ parameters.totalShards }}
|
||||
APPLICATION_TYPE="${{ parameters.applicationType }}"
|
||||
ENV_FILE="${{ parameters.envFileName }}"
|
||||
|
||||
# Handle localhost URLs
|
||||
if [[ "$TARGET_URL" == *localhost* ]]; then
|
||||
echo 'Npx version:'
|
||||
npx --version
|
||||
port="$TARGET_URL"
|
||||
echo "URL: '$port'"
|
||||
echo "URL '$port' contains 'localhost'. Extracting port... "
|
||||
port=$(echo "$port" | sed -E 's/.*:([0-9]+).*/\1/')
|
||||
echo "Port is '$port'"
|
||||
TARGET_URL="$TARGET_URL"
|
||||
JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
|
||||
FILTER="$FILTER"
|
||||
PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
|
||||
SERVE_PATH="${{ parameters.npmServePath }}"
|
||||
SHARD_NUMBER=$(shardNumber)
|
||||
TOTAL_SHARDS=${{ parameters.totalShards }}
|
||||
echo "Localhost URL detected, extracting port..."
|
||||
port=$(echo "$TARGET_URL" | sed -E 's/.*:([0-9]+).*/\1/')
|
||||
echo "Port extracted: $port"
|
||||
|
||||
# Create readiness check URL based on application type
|
||||
if [[ "$APPLICATION_TYPE" == "dotnet" ]]; then
|
||||
# For .NET APIs, append the readiness endpoint to the target URL
|
||||
READINESS_URL="${TARGET_URL}${{ parameters.readinessEndpoint }}"
|
||||
echo "Using .NET readiness check URL: $READINESS_URL"
|
||||
else
|
||||
# For Vue apps, use the target URL as-is
|
||||
READINESS_URL="$TARGET_URL"
|
||||
echo "Using Vue app URL for readiness check: $READINESS_URL"
|
||||
fi
|
||||
|
||||
echo "Starting tests for shard $SHARD_NUMBER of $TOTAL_SHARDS..."
|
||||
|
||||
container_id=$(docker create \
|
||||
--ipc=host \
|
||||
--env CI=true \
|
||||
--env TARGET_URL="$TARGET_URL" \
|
||||
--env READINESS_URL="$READINESS_URL" \
|
||||
--env PLAYWRIGHT_PATH="$PLAYWRIGHT_PATH" \
|
||||
--env SERVE_PATH="$SERVE_PATH" \
|
||||
--env SHARD_NUMBER="$SHARD_NUMBER" \
|
||||
--env TOTAL_SHARDS="$TOTAL_SHARDS" \
|
||||
--env FILTER="$FILTER" \
|
||||
--env APPLICATION_TYPE="$APPLICATION_TYPE" \
|
||||
--env SERVER_MAX_ATTEMPTS="${{ parameters.serverMaxAttempts }}" \
|
||||
--env ASPNETCORE_URLS="https://localhost:$port" \
|
||||
--env ENV_FILE="$ENV_FILE" \
|
||||
--env-file "${{ parameters.envFileName }}" \
|
||||
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
||||
npx concurrently -k -n 'server,playwright' \
|
||||
"npm --prefix $SERVE_PATH run serve -- --port=$port" \
|
||||
"npx --prefix $PLAYWRIGHT_PATH wait-on $TARGET_URL && PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx --prefix $PLAYWRIGHT_PATH playwright test $PLAYWRIGHT_PATH --config=$PLAYWRIGHT_PATH/playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob $FILTER" \
|
||||
bash -c '
|
||||
echo "=== Starting Application Server ==="
|
||||
echo "Application Type: $APPLICATION_TYPE"
|
||||
echo "Target URL: $TARGET_URL"
|
||||
echo "Readiness Check URL: $READINESS_URL"
|
||||
echo "Server 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
|
||||
export TARGET_URL="$TARGET_URL"
|
||||
export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
|
||||
export FILTER="$FILTER"
|
||||
export PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
|
||||
export SERVE_PATH="${{ parameters.npmServePath }}"
|
||||
export SHARD_NUMBER=$(shardNumber)
|
||||
export TOTAL_SHARDS=${{ parameters.totalShards }}
|
||||
export ENV_FILE="${{ parameters.envFileName }}"
|
||||
# Remote URL testing
|
||||
echo "Remote URL testing mode"
|
||||
container_id=$(docker create \
|
||||
--ipc=host \
|
||||
--env CI=true \
|
||||
--env TARGET_URL="$TARGET_URL" \
|
||||
--env PLAYWRIGHT_PATH="$PLAYWRIGHT_PATH" \
|
||||
--env SHARD_NUMBER="$SHARD_NUMBER" \
|
||||
--env TOTAL_SHARDS="$TOTAL_SHARDS" \
|
||||
--env FILTER="$FILTER" \
|
||||
--env ENV_FILE="$ENV_FILE" \
|
||||
--env-file "${{ parameters.envFileName }}" \
|
||||
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
||||
bash -c "
|
||||
set -a
|
||||
source $ENV_FILE
|
||||
set +a
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx --prefix $PLAYWRIGHT_PATH playwright test $PLAYWRIGHT_PATH --config=$PLAYWRIGHT_PATH/playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob $FILTER
|
||||
" \
|
||||
echo '=== Running Tests Against Remote URL ==='
|
||||
echo 'Target URL: $TARGET_URL'
|
||||
|
||||
mkdir -p /app/blob-report
|
||||
cd \$PLAYWRIGHT_PATH
|
||||
|
||||
if [ -n \"\$FILTER\" ]; then
|
||||
echo 'Running tests with filter: \$FILTER'
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx dotenv-cli -e \"../\$ENV_FILE\" -- playwright test \
|
||||
--config=./playwright.config.ts \
|
||||
--shard=\$SHARD_NUMBER/\$TOTAL_SHARDS \
|
||||
--reporter=list,blob \
|
||||
\$FILTER || true
|
||||
else
|
||||
echo 'Running all tests'
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx dotenv-cli -e \"../\$ENV_FILE\" -- playwright test \
|
||||
--config=./playwright.config.ts \
|
||||
--shard=\$SHARD_NUMBER/\$TOTAL_SHARDS \
|
||||
--reporter=list,blob || true
|
||||
fi
|
||||
"
|
||||
)
|
||||
fi
|
||||
|
||||
# Start container and stream logs
|
||||
echo "Starting tests for shard $(shardNumber)..."
|
||||
docker start -a $container_id
|
||||
|
|
@ -206,12 +385,6 @@ jobs:
|
|||
# Remove container
|
||||
echo "Cleaning up container..."
|
||||
docker rm $container_id
|
||||
|
||||
# Check if tests failed
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Tests failed in shard $(shardNumber) or tests don't exist for this shard number!"
|
||||
exit 0 # Suppress error. It will be visible in report.
|
||||
fi
|
||||
displayName: 'Run Playwright Tests - Shard $(shardNumber)'
|
||||
env: ${{ parameters.secrets }}
|
||||
|
||||
|
|
@ -227,6 +400,7 @@ jobs:
|
|||
docker rmi ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} -f
|
||||
displayName: 'Cleanup Docker Image'
|
||||
condition: always()
|
||||
|
||||
- job: download_and_merge_reports
|
||||
dependsOn: playwright_tests
|
||||
timeoutInMinutes: 8
|
||||
|
|
@ -238,6 +412,10 @@ jobs:
|
|||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports'
|
||||
- bash: |
|
||||
printenv > "${{ parameters.envFileName }}"
|
||||
env: ${{ parameters.secrets }}
|
||||
displayName: "Make Azure Pipeline Variables Available to Docker"
|
||||
- task: Docker@2
|
||||
displayName: 'Build Docker Image'
|
||||
inputs:
|
||||
|
|
@ -245,16 +423,15 @@ jobs:
|
|||
dockerfile: ${{ parameters.dockerFileName }}
|
||||
repository: ${{ parameters.dockerImageName }}
|
||||
tags: ${{ parameters.imageTag }}
|
||||
arguments: '--no-cache --pull'
|
||||
arguments: |
|
||||
--no-cache --pull ${{ parameters.dockerBuildArgs }}
|
||||
- bash: |
|
||||
printenv > "${{ parameters.envFileName }}"
|
||||
branch_name=$(System.PullRequest.SourceBranch)
|
||||
echo "Retrieved branch name: '$branch_name'"
|
||||
JIRA_CARD_NUMBER="${branch_name##*/}"
|
||||
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
|
||||
export PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
|
||||
|
||||
|
||||
export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
|
||||
export ENV_FILE="${{ parameters.envFileName }}"
|
||||
container_id=$(docker create \
|
||||
|
|
@ -263,14 +440,12 @@ jobs:
|
|||
--env-file "${{ parameters.envFileName }}" \
|
||||
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
|
||||
bash -c "
|
||||
set -a
|
||||
source $ENV_FILE
|
||||
set +a
|
||||
echo 'Moving Playwright reports out of subfolders...'
|
||||
find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \;
|
||||
echo 'Value of JIRA_CARD_NUMBER in bash -c command: $JIRA_CARD_NUMBER'
|
||||
echo 'Merging reports...'
|
||||
JIRA_CARD_NUMBER='$JIRA_CARD_NUMBER' PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx --prefix $PLAYWRIGHT_PATH playwright merge-reports --config=$PLAYWRIGHT_PATH/playwright.config.ts ./playwright-reports
|
||||
cd $PLAYWRIGHT_PATH
|
||||
JIRA_CARD_NUMBER='$JIRA_CARD_NUMBER' PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx dotenv-cli -e ../$ENV_FILE -- playwright merge-reports --config=./playwright.config.ts ../playwright-reports
|
||||
"
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue