From 0b742ab84827c9db6b1d1343fc91e7dc2b7735b3 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Tue, 8 Apr 2025 16:17:05 -0400 Subject: [PATCH 01/50] Updates scheduled pipeline branch Updates the scheduled pipeline to run against the release branch. This ensures that the automated tests are executed against the stable release branch. --- playwright-tests/azure-pipelines-automated-testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index b4d7da831..ccb5634c4 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -4,7 +4,7 @@ schedules: displayName: Daily Test Automation Run for FMG-NextGen branches: include: - - main + - release/2025.04.24 pool: 'AmazonLinuxPool' From 84fa84fdb3930d41830dd1fdc8bacd6eade23209 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Tue, 8 Apr 2025 16:18:09 -0400 Subject: [PATCH 02/50] Update azure-pipelines-automated-testing.yml for Azure Pipelines From 219762ec30af2e87d848c0b4fc9cccc34f33ffa2 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Wed, 9 Apr 2025 14:50:30 -0400 Subject: [PATCH 03/50] Adds Jira integration for test results Adds a Jira writeback reporter to automatically create bug and test case subtasks in Jira based on playwright test results. Upgrades axios and adds axios-retry and axios-mock-adapter for more robust API calls. --- package-lock.json | 135 ++++++- package.json | 4 +- .../business-logic/types/JiraApi.ts | 111 ++++++ playwright-tests/impl/API/JiraApiUtil.ts | 143 ++++++++ .../impl/reporter/JiraWritebackReporter.ts | 339 ++++++++++++++++++ 5 files changed, 726 insertions(+), 6 deletions(-) create mode 100644 playwright-tests/business-logic/types/JiraApi.ts create mode 100644 playwright-tests/impl/API/JiraApiUtil.ts create mode 100644 playwright-tests/impl/reporter/JiraWritebackReporter.ts diff --git a/package-lock.json b/package-lock.json index 00fc7673a..2b541b385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "@iframe-resizer/child": "^5.3.3", - "axios": "^0.23.0", "bootstrap": "^5.3.3", "core-js": "^3.38.1", "http-status-codes": "^2.3.0", @@ -38,6 +37,9 @@ "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "^27.0.0", + "axios": "^1.8.4", + "axios-mock-adapter": "^2.1.0", + "axios-retry": "^4.5.0", "dotenv-safe": "^9.1.0", "eslint": "8.57", "eslint-plugin-prettier": "^5.2.1", @@ -5412,12 +5414,82 @@ } }, "node_modules/axios": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz", - "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-mock-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", + "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/axios-mock-adapter/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/babel-jest": { @@ -8005,6 +8077,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -9227,6 +9315,7 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, "funding": [ { "type": "individual", @@ -9694,6 +9783,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -10509,6 +10614,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -16247,6 +16365,13 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", diff --git a/package.json b/package.json index c63d8b5e8..923fec124 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ }, "dependencies": { "@iframe-resizer/child": "^5.3.3", - "axios": "^0.23.0", "bootstrap": "^5.3.3", "core-js": "^3.38.1", "http-status-codes": "^2.3.0", @@ -42,6 +41,9 @@ "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "^27.0.0", + "axios": "^1.8.4", + "axios-mock-adapter": "^2.1.0", + "axios-retry": "^4.5.0", "dotenv-safe": "^9.1.0", "eslint": "8.57", "eslint-plugin-prettier": "^5.2.1", diff --git a/playwright-tests/business-logic/types/JiraApi.ts b/playwright-tests/business-logic/types/JiraApi.ts new file mode 100644 index 000000000..d5b34b081 --- /dev/null +++ b/playwright-tests/business-logic/types/JiraApi.ts @@ -0,0 +1,111 @@ +export interface PutEditIssueRequestBody { + fields: Partial +} + +export interface PostTransitionIssueRequestBody { + transition: { id: string } +} + +export interface PostCreateIssueRequestBody extends JiraIssue {} + +export interface PostCreateIssueResponse { + id: string, + key: string +} + +export interface PostBulkCreateIssueRequestBody { + issueUpdates: PostCreateIssueRequestBody[] +} + +export interface BulkTransitionInput { + selectedIssueIdsOrKeys: string[], + transitionId: string +} + +export interface PostBulkTransitionIssuesRequestBody { + bulkTransitionInputs: BulkTransitionInput[], + sendBulkNotification: false +} + +export interface PostBulkCreateIssueResponse { + issues: PostCreateIssueResponse[] +} + +export interface PostAddCommentResponse { + body: JiraContent +} + +export interface JiraIssueFields { + summary: string, + description: JiraContent, + project?: JiraProject, + issuetype?: { id: string }, + parent?: JiraParent, + fixVersions?: JiraVersion[], + subtasks?: JiraSubTask[], + customfield_14857?: JiraContent // Test Steps field +} + +export interface JiraSubTask { + id: string, + key: string +} + +export interface JiraProject { + key: string +} + +export interface JiraParent { + key: string +} + +export interface JiraContent { + type?: string, + text?: string, + version?: number + content?: JiraContent[] +} + +export interface JiraVersion { + id: string +} + +export interface JiraTransition { + id: string, + name: string +} + +export interface JiraIssueType { + id: string, + self: string, + description: string, + iconUrl: string, + name: string, + untranslatedName: string, + subtask: boolean, +} + +export interface GetIssueTransitionsResponse { + transitions: JiraTransition[] +} + +export interface GetIssueTypesResponse { + issueTypes: JiraIssueType[] +} + +export interface PostBulkFetchIssuesRequestBody { + issueIdsOrKeys: string[] +} + +export interface PostBulkFetchIssuesResponse { + issues: GetIssueResponse[]; +} + +export interface JiraIssue { + key?: string, + id?: string, + transition?: { id: string }, + fields: JiraIssueFields +} + +export interface GetIssueResponse extends JiraIssue {} \ No newline at end of file diff --git a/playwright-tests/impl/API/JiraApiUtil.ts b/playwright-tests/impl/API/JiraApiUtil.ts new file mode 100644 index 000000000..fdb970d1e --- /dev/null +++ b/playwright-tests/impl/API/JiraApiUtil.ts @@ -0,0 +1,143 @@ +import { PostBulkFetchIssuesRequestBody, PostBulkFetchIssuesResponse, GetIssueResponse, GetIssueTransitionsResponse, GetIssueTypesResponse, PostAddCommentResponse, PostBulkCreateIssueRequestBody, PostBulkCreateIssueResponse, PostCreateIssueRequestBody, PostCreateIssueResponse, PostTransitionIssueRequestBody, PutEditIssueRequestBody, PostBulkTransitionIssuesRequestBody } from "@business-logic/types/JiraApi"; +import axios, { request } from "axios"; +import axiosRetry from "axios-retry"; + +const jiraUrl = process.env.JIRA_SERVER!; +const jiraUsername = process.env.JIRA_USERNAME!; +const jiraApiKey = process.env.JIRA_API_KEY!; +const encodedAuthKey = Buffer.from(`${jiraUsername}:${jiraApiKey}`).toString('base64'); + +export default class JiraApiUtil { + readonly baseUrl: string; + readonly issueUrl: string; + readonly bulkIssueCreateUrl: string; + readonly bulkIssueFetchUrl: string; + readonly bulkTransitionIssuesUrl: string; + readonly axiosClient: axios.AxiosInstance; + + constructor() { + this.baseUrl = jiraUrl; + this.issueUrl = `${this.baseUrl}/rest/api/3/issue`; + this.bulkIssueCreateUrl = `${this.issueUrl}/bulk`; + this.bulkIssueFetchUrl = `${this.issueUrl}/bulkfetch`; + this.bulkTransitionIssuesUrl = `${this.baseUrl}/rest/api/3/bulk/issues/transition` + this.axiosClient = axios.create(); + // interceptor to log error message from api + this.axiosClient.interceptors.response.use( + response => response, + error => { + console.error('Axios Error:', error?.response?.data || error.message); + return Promise.reject(error); + } + ); + // Set up retries if requests are made too quickly + axiosRetry(this.axiosClient, { + retries: 4, + retryDelay: (retryCount) => { return Math.pow(2, retryCount) * 1000; }, // Exponential backoff + retryCondition: (error) => { return error.response?.status === 429 } // If rate-limit error + }); + } + + getIssue(issueKey: string) { + const url = `${this.issueUrl}/${issueKey}`; + return this.axiosClient.get(url, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + Accept: 'application/json' + } + }); + } + + postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) { + return this.axiosClient.post(this.bulkIssueFetchUrl, requestBody, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }); + } + + getIssueTransitions(issueKey: string) { + const url = `${this.issueUrl}/${issueKey}/transitions` + return this.axiosClient.get(url, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + Accept: 'application/json' + } + }); + } + + getIssueTypes(projectKey: string) { + const url = `${this.baseUrl}/rest/api/3/issue/createmeta/${projectKey}/issuetypes`; + return this.axiosClient.get(url, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + Accept: 'application/json' + } + }); + } + + postAddComment(issueKey: string, commentBody: PostAddCommentResponse) { + const url = `${this.issueUrl}/${issueKey}/comment`; + return this.axiosClient.post(url, commentBody, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }); + } + + postTransitionIssue(issueKey: string, requestBody: PostTransitionIssueRequestBody) { + const transitionUrl = `${this.issueUrl}/${issueKey}/transitions` + return this.axiosClient.post(transitionUrl, requestBody, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }); + } + + putEditIssue(issueKey: string, requestBody: PutEditIssueRequestBody) { + const editIssueUrl = `${this.issueUrl}/${issueKey}` + return this.axiosClient.put(editIssueUrl, requestBody, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }); + } + + postBulkTransitionIssues(requestBody: PostBulkTransitionIssuesRequestBody) { + return this.axiosClient.post(this.bulkTransitionIssuesUrl, requestBody, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }); + } + + postCreateIssue(requestBody: PostCreateIssueRequestBody) { + return this.axiosClient.post(this.issueUrl, requestBody, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }); + } + + postBulkCreateIssue(requestBody: PostBulkCreateIssueRequestBody) { + return this.axiosClient.post(this.bulkIssueCreateUrl, requestBody, { + headers: { + Authorization: `Basic ${encodedAuthKey}`, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }); + } +} \ No newline at end of file diff --git a/playwright-tests/impl/reporter/JiraWritebackReporter.ts b/playwright-tests/impl/reporter/JiraWritebackReporter.ts new file mode 100644 index 000000000..c1db33244 --- /dev/null +++ b/playwright-tests/impl/reporter/JiraWritebackReporter.ts @@ -0,0 +1,339 @@ +import JiraApiUtil from "@impl/API/JiraApiUtil"; +import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter"; +import { formatDate } from "@impl/utils/DateUtils"; +import { GetIssueResponse, JiraIssue, JiraIssueFields } from "@business-logic/types/JiraApi"; + +const jiraCardNumber = process.env.JIRA_CARD_NUMBER!; +const jiraProjectKey = 'INSR'; +const passTransitionId = '111'; +const failTransitionId = '101'; + +export default class JiraWritebackReporter implements Reporter { + readonly jiraApiUtil: JiraApiUtil = new JiraApiUtil(); + readonly loadIssueCalls: (() => void)[] = []; // Calls to loadIssue must be deferred because they depend on information from the API. + readonly issuesToPass: string[] = []; // Issues to transition to "Pass". + readonly issuesToFail: string[] = []; // Issues to transition to "Fail". + readonly issuesToCreate: JiraIssue[] = []; // Issues to be batch created. + existingSubtasks: JiraIssue[] = []; // Array to hold existing subtasks of the dev card. + testCaseTypeId: string|undefined = undefined; // ID of the test case subtask type in Jira. Will be filled by API call. + bugTypeId:string|undefined = undefined; // ID of the bug subtask type. + parentCard: GetIssueResponse | undefined = undefined; // Variable to hold the parent card. Will be filled by API call. + + // Will need this if we want to move ortoni report upload into this reporter. + // readonly ortoniReport = new OrtoniReport(reportConfig); + + /** + * This function loads the IDs for the Jira Issue Types we use. + * + */ + async loadJiraIssueTypes() { + console.log(`JiraWritebackReporter >> Loading Jira Issue Types for project '${jiraProjectKey}'...`); + const issueTypesRes = await this.jiraApiUtil.getIssueTypes(jiraProjectKey); + const issueTypes = issueTypesRes.data; + this.testCaseTypeId = issueTypes.issueTypes.find(issueType => { + return issueType.subtask === true && issueType.name === 'Test Case Sub-task' + })?.id; + this.bugTypeId = issueTypes.issueTypes.find(issueType => { + return issueType.subtask === true && issueType.name === 'Bug Sub-task' + })?.id; + console.log('JiraWritebackReporter >> Loaded issue types.'); + } + + /** + * This function generates a test subtask based on the test results we pass in. + * @param test TestCase from onTestEnd() + * @param result TestResult from onTestEnd() + * @returns Jira Test Subtask based on test result. + */ + getCreateTestSubtask(test: TestCase, result: TestResult) { + if (result.status === 'skipped') { + return undefined; + } + const testStepTitles = result.steps.map(step => { + return `-\t${step.title}`; + }).join('\n'); + const allErrors = result.errors.map(value => { + return value.message + }).join('\n'); + + const description = `Most recent test status: ${result.status}.\nDuration: ${result.duration/1000} seconds.\nErrors:\n${allErrors}` + const testSubtaskBody: JiraIssueFields = { + summary: test.title, + description: { + content: [ + { + "content": [ + { + "text": description, + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 + }, + project: { + key: jiraProjectKey + }, + issuetype: { + id: this.testCaseTypeId! + }, + parent: { + key: jiraCardNumber + }, + customfield_14857: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: testStepTitles + } + ] + } + ] + } + }; + return testSubtaskBody; + } + + /** + * This function generates a bug subtask based on the test results we pass in. + * @param test TestCase from onTestEnd() + * @param result TestResult from onTestEnd() + * @returns Jira Bug Subtask based on test result. + */ + getCreateBug(test: TestCase, result: TestResult) { + if (result.status === 'passed' || result.status === 'skipped') { + return undefined; + } + const allErrors = result.errors.map(value => { + return value.message + }).join('\n'); + + const bug: JiraIssueFields = { + summary: `TEST FAILED ${formatDate(new Date())}: ${test.title}`, + description: { + content: [ + { + "content": [ + { + "text": allErrors, + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 + }, + project: { + key: jiraProjectKey + }, + issuetype: { + id: this.bugTypeId! + }, + parent: { + key: jiraCardNumber + } + }; + return bug; + } + + /** + * This function gives us the correct transition for a test subtask based on this test result. + * @param result TestResult from onTestEnd() + * @returns Correct transition to pass to the Jira API. + */ + getSubtaskTransition(result: TestResult): { id: string } | undefined { + if (result.status === 'skipped') { + return undefined; + } + + if (result.status === 'passed') { + return { + id: passTransitionId + }; + } else { + return { + id: failTransitionId + }; + } + } + + /** + * Grabs parent card and existing subtasks. + */ + async loadReporterData() { + await this.loadJiraIssueTypes(); + + console.log('JiraWritebackReporter >> Searching for parent card...'); + const parentRes = await this.jiraApiUtil.getIssue(jiraCardNumber); + const parentCard = parentRes.data; + console.log(`JiraWritebackReporter >> Retrieved parent card: ${parentCard.key}: ${parentCard.fields.summary}`); + + console.log(`JiraWritebackReporter >> Searching for all subtasks of parent card '${jiraCardNumber}'...`); + const subtaskIds = parentCard.fields.subtasks?.map(subtask => { + return subtask.id; + }); + + if (subtaskIds && subtaskIds.length > 0) { + for (let i = 0; i < subtaskIds.length; i+=50) { + const batch = subtaskIds.slice(i, i+50); + const subTasksRes = await this.jiraApiUtil.postBulkFetchIssues({ issueIdsOrKeys: batch }); + this.existingSubtasks.push(...subTasksRes.data.issues); + } + console.log('JiraWritebackReporter >> Subtasks retrieved.'); + } else { + console.log('JiraWritebackReporter >> No subtasks were found.'); + } + + } + + /** + * Bulk creates test cases and bugs in batches of up to 50. Bulk transitions existing test case subtasks. + */ + async writeResults() { + for (let i = 0; i < this.issuesToCreate.length; i+=50) { + const batch = this.issuesToCreate.slice(i, i + 50); + console.log('JiraWritebackReporter >> Creating issue batch...'); + // fire request + await this.jiraApiUtil.postBulkCreateIssue({ issueUpdates: batch }); + console.log('JiraWritebackReporter >> Issue batch created.'); + } + + if (this.issuesToPass.length > 0) { + console.log('JiraWritebackReporter >> Transitioning passed test cases to "Pass"...') + await this.jiraApiUtil.postBulkTransitionIssues( { + bulkTransitionInputs: [{ + selectedIssueIdsOrKeys: this.issuesToPass, + transitionId: passTransitionId + }], + sendBulkNotification: false + }); + console.log('JiraWritebackReporter >> Transition success.'); + } + + if (this.issuesToFail.length > 0) { + console.log('JiraWritebackReporter >> Transitioning failed test cases to "Fail"...') + await this.jiraApiUtil.postBulkTransitionIssues( { + bulkTransitionInputs: [{ + selectedIssueIdsOrKeys: this.issuesToFail, + transitionId: failTransitionId + }], + sendBulkNotification: false + }); + console.log('JiraWritebackReporter >> Transition success.') + } + } + + /** + * Builds and loads appropriate response based on test result. This can be a test subtask/bug subtask or a call to transition a test subtask. + * @param test TestCase from onTestEnd() + * @param result TestResult from onTestEnd() + * @returns Void + */ + loadIssue(test: TestCase, result: TestResult) { + if (result.status === 'skipped') return; + if (result.status !== 'passed' && result.retry < test.retries) return; // Skip if test case failed and this isn't the last retry. + + let existingTestSubtask: JiraIssue | undefined = undefined; + let existingBug: JiraIssue | undefined = undefined; + const subtaskTransition = this.getSubtaskTransition(result)!; // Get either a Pass or Fail transition depending on test results. + + console.log('JiraWritebackReporter >> Checking for existing subtasks for this test...'); + for (const card of this.existingSubtasks) { + if (card.fields.issuetype?.id === this.testCaseTypeId) { + if (card.fields.summary === test.title) { + existingTestSubtask = card; + console.log('JiraWritebackReporter >> Found existing test subtask.'); + } + } else if (card.fields.issuetype?.id === this.bugTypeId) { + if (new RegExp(`^TEST FAILED [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]: ${test.title}$`).test(card.fields.summary)) { + existingBug = card; + console.log('JiraWritebackReporter >> Found existing bug.'); + // TODO: Handle multiple matches for bug/test case. Currently picks one in that case. + } + } + } + if (!(existingTestSubtask || existingBug)) { + console.log('JiraWritebackReporter >> No existing test subtask or bug was found.') + } + + const subTask = this.getCreateTestSubtask(test, result); // Will return undefined if test status is 'skipped' + const bug = this.getCreateBug(test, result); // Will return undefined if we don't need one + if (subTask) { + // Create or Edit Subtask + if (existingTestSubtask) { + if (result.status === 'passed') { + this.issuesToPass.push(existingTestSubtask.key!); + } else { + this.issuesToFail.push(existingTestSubtask.key!); + } + } else { + this.issuesToCreate.push({ + fields: subTask, + transition: subtaskTransition + }); + } + } + + if (bug) { + // Create or Edit Bug + if (existingBug) { + // Leave it alone. This reporter should not modify existing bugs. + } else { + this.issuesToCreate.push({ + fields: bug + }); + } + } + } + + /** + * This function loads calls to loadIssue() in an array to be executed when the necessary information from the Jira API is available. + * @param test + * @param result + */ + onTestEnd(test: TestCase, result: TestResult) { + // this.ortoniReport.onTestEnd(test, result); + this.loadIssueCalls.push(() => { this.loadIssue(test, result); }); + } + + /** + * Makes calls to load and execute API calls to Jira. + * @param result + * @returns Promise to write results to Jira. + */ + onEnd(result: FullResult): Promise<{ status?: FullResult["status"]; } | undefined | void>|void { + // this.ortoniReport.onEnd(result); + return this.loadReporterData().then(async () => { + this.loadIssueCalls.map(fn => fn()); + await this.writeResults(); + console.log(`JiraWritebackReporter >> Posted necessary changes for '${jiraCardNumber}'`); + }); + } + + // These overloads will become necessary if we move the ortoni report upload into this reporter + // onBegin(config: FullConfig, suite: Suite): void { + // this.ortoniReport.onBegin(config, suite); + // } + + // onError(error: TestError): void { + // this.ortoniReport.onError(error); + // } + + // onExit(): Promise { + // return this.ortoniReport.onExit(); + // } + + // onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void { + // this.ortoniReport.onStdOut(chunk, test, result); + // } +} \ No newline at end of file From 918853670880620a129025f00a3c9dab676ac29d Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Wed, 9 Apr 2025 15:09:45 -0400 Subject: [PATCH 04/50] Adding pr gate pipeline functionality to pipeline file --- azure-pipelines.yml | 168 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2cb18429c..8c5e61265 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,6 +34,12 @@ resources: variables: - group: Digital-Infrastructure - group: FixMyGlass-BuildBranches + - name: dockerImageName + value: 'playwright-tests' + - name: imageTag + value: '$(Build.BuildId)' + - name: totalShards + value: 2 stages: # PR's @@ -57,6 +63,168 @@ stages: npmLocation: $(Build.SourcesDirectory) testResultsFile: junit.xml summaryFileLocation: $(Build.SourcesDirectory)/coverage/cobertura-coverage.xml + - job: playwright_tests + continueOnError: true + strategy: + matrix: + shard1: + shardNumber: 1 + shard2: + shardNumber: 2 + + steps: + - task: Docker@2 + displayName: 'Build Docker Image' + inputs: + command: build + dockerfile: Dockerfile.playwright + repository: $(dockerImageName) + tags: $(imageTag) + arguments: '--no-cache --pull' + + - script: | + branch_name=$(System.PullRequest.SourceBranch) + echo "Retrieved branch name: '$branch_name'" + JIRA_CARD_NUMBER="${branch_name##*/}" + echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'" + # Create container and run tests + container_id=$(docker create \ + --ipc=host \ + -e CCIS_API_AUTH=$(CCIS_API_AUTH) \ + -e BASE_URL=$(BASE_URL) \ + -e CCIS_API_URL=$(CCIS_API_URL) \ + -e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \ + -e SHARD=$(shardNumber) \ + -e CI=true \ + -e ENABLE_MOCK_TESTING=true \ + -e ENABLE_ACCESSIBILITY_TESTING=true \ + -e NODE_ENV=$(NODE_ENV) \ + $(dockerImageName):$(imageTag) \ + npx concurrently -k -n "server,playwright"\ + "sed -i \"s|^process\.env\.VUE_APP_CONSUMER_CF_DISTRO = .*|process\.env\.VUE_APP_CONSUMER_CF_DISTRO='https://digitalapi.test.safelite.io'|\" \"./vue.config.js\" && echo \"Updated config file to use TEST APIs\" && npm run serve -- --port=8080"\ + "npx wait-on http://localhost:8080 && npm run test:playwright -- --shard=$(shardNumber)/$(totalShards) --reporter=list,blob --grep \"@$JIRA_CARD_NUMBER\"") + + # Start container and stream logs + echo "Starting tests for shard $(shardNumber)..." + docker start -a $container_id + + # Create directory for test results + echo "Creating test results directory..." + mkdir -p $(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber) + + # Copy test results from container + echo "Copying test results..." + docker cp $container_id:/app/blob-report/. $(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber)/ + + # 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)' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Test Reports - Shard $(shardNumber)' + condition: always() + inputs: + targetPath: '$(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber)' + artifact: 'playwright-report-shard-$(shardNumber)' + publishLocation: 'pipeline' + + - script: | + docker rmi $(dockerImageName):$(imageTag) -f + displayName: 'Cleanup Docker Image' + condition: always() + + # Add job to merge test reports and update Jira + - job: download_and_merge_reports + dependsOn: playwright_tests + timeoutInMinutes: 10 + cancelTimeoutInMinutes: 10 + steps: + - task: DownloadPipelineArtifact@2 + inputs: + targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports' + - task: Docker@2 + displayName: 'Build Docker Image' + inputs: + command: build + dockerfile: Dockerfile.playwright + repository: $(dockerImageName) + tags: $(imageTag) + arguments: '--no-cache --pull' + - bash: | + branch_name=$(System.PullRequest.SourceBranch) + echo "Retrieved branch name: '$branch_name'" + JIRA_CARD_NUMBER="${branch_name##*/}" + echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'" + # Create container for jira writeback + container_id=$(docker create \ + --ipc=host \ + -e JIRA_SERVER=$(JIRA_SERVER) \ + -e JIRA_USERNAME=$(JIRA_USERNAME) \ + -e JIRA_API_KEY=$(JIRA_API_KEY) \ + -e JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER" \ + $(dockerImageName):$(imageTag) \ + bash -c "echo \"Moving Playwright reports out of subfolders...\" && + find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \; && + echo \"Merging reports...\" && + export NODE_OPTIONS=--max_old_space_size=4096 + PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx playwright merge-reports --reporter=ortoni-report,junit,\"/app/playwright-tests/impl/reporter/JiraWritebackReporter.ts\" ./playwright-reports + echo \"Contents of ortoni-report:\" && ls ./ortoni-report && + echo 'Current dir: ' && pwd + echo 'Contents of current dir: ' && ls + echo 'Contents of /app/test-results' && ls /app/test-results + echo \"Writing report to Jira card '$JIRA_CARD_NUMBER'...\" && + /app/devops/scripts/jira_writeback.sh add_comment \"$JIRA_CARD_NUMBER\" /app/ortoni-report/ortoni-report.html \"AUTOMATED TEST RUN: $(date)\" ") + + # Start container and stream logs + echo "Starting merge" + docker start -a $container_id + + # Create directory for test results + echo "Creating test results directory..." + mkdir -p $(System.DefaultWorkingDirectory)/ortoni-report + mkdir -p $(System.DefaultWorkingDirectory)/test-results + + # Copy test results from container + echo "Copying test results..." + docker cp $container_id:/app/ortoni-report/. $(System.DefaultWorkingDirectory)/ortoni-report + docker cp $container_id:/app/test-results/junit_results.xml $(System.DefaultWorkingDirectory)/test-results + + # Remove container + echo "Cleaning up container..." + docker rm $container_id + env: + JIRA_API_KEY: $(JIRA_API_KEY) + displayName: merge_and_publish_results_to_jira + + - task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + searchFolder: 'test-results' + testResultsFormat: 'JUnit' + testResultsFiles: 'junit_results.xml' + mergeTestResults: true + failTaskOnFailedTests: false + testRunTitle: 'Playwright Tests' + condition: succeededOrFailed() + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Merged Report' + condition: always() + inputs: + targetPath: '$(System.DefaultWorkingDirectory)/ortoni-report' + artifact: 'playwright-merged-report' + publishLocation: 'pipeline' + - script: | + docker rmi $(dockerImageName):$(imageTag) -f + displayName: 'Cleanup Docker Image' + condition: always() - ${{ else }}: # Dev Build/Deploy - stage: Dev From 4e82930ea358a7a4ba724224284c38b268e35df7 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 10 Apr 2025 09:24:37 -0400 Subject: [PATCH 05/50] Removed Cron Job scheduling from automated testing pipeline Now setting it up through Azure --- playwright-tests/azure-pipelines-automated-testing.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index ccb5634c4..db2a902da 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -1,11 +1,3 @@ -schedules: -- cron: 0 9 * * MON-FRI - always: true - displayName: Daily Test Automation Run for FMG-NextGen - branches: - include: - - release/2025.04.24 - pool: 'AmazonLinuxPool' variables: From 49ea0fa13ec559c02d2cb4e71f1b68de7ce1aaaa Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 10 Apr 2025 09:27:14 -0400 Subject: [PATCH 06/50] Adding Environment to Jira comment --- playwright-tests/azure-pipelines-automated-testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index db2a902da..50466bbd2 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -143,7 +143,7 @@ stages: fi echo \"Writing report to Jira card '$jira_card_number'...\" - /app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE})\" ") + /app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE}) - Environment: \${NODE_ENV}\" ") # Start container and stream logs echo "Starting merge" From d918d85ee48186fe371703f103b271c5273880bd Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 10 Apr 2025 09:48:01 -0400 Subject: [PATCH 07/50] Pulls NODE_ENV variable from env and uses this in the comment --- playwright-tests/azure-pipelines-automated-testing.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index 50466bbd2..5634b4bd6 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -123,6 +123,7 @@ stages: -e REPORT_NAME="$report_name" \ -e CURRENT_DAY="$current_day" \ -e NUMERIC_DATE="$numeric_date" \ + -e NODE_ENV=$(NODE_ENV) \ $(dockerImageName):$(imageTag) \ bash -c "chmod +x devops/scripts/jira_writeback.sh echo \"Moving Playwright reports out of subfolders...\" From d44367274529557dbefaee9cae0af8e217475bb9 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 10 Apr 2025 11:43:49 -0400 Subject: [PATCH 08/50] Updates default postal code for customer details Updates the default postal code in the customer details data. This ensures that the default data reflects a more accurate or valid postal code for testing purposes. --- playwright-tests/business-logic/constants/DefaultTestData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/business-logic/constants/DefaultTestData.ts b/playwright-tests/business-logic/constants/DefaultTestData.ts index 6efb2c1db..197297b4b 100644 --- a/playwright-tests/business-logic/constants/DefaultTestData.ts +++ b/playwright-tests/business-logic/constants/DefaultTestData.ts @@ -43,7 +43,7 @@ export function getCustomerDetails(): ICustomerDetails { street: faker.location.streetAddress(), city: 'Columbus', state: 'Ohio', - postalCode: '43215', + postalCode: '21237', country: 'United States' } }; From d26885d0f0d344a861c52b9bbd82a264e0c28b0f Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 10 Apr 2025 13:37:41 -0400 Subject: [PATCH 09/50] Improves handling of repeated clicks modal Updates the selector for the repeated clicks modal close button to be more specific, and adds a short wait after closing the modal before proceeding. This improves reliability when the modal appears and interferes with subsequent actions. Removes redundant check for modal visibility in the service location page as it is handled on the service packages page. --- playwright-tests/pages/ServiceLocationPage.ts | 3 --- playwright-tests/pages/ServicePackagesPage.ts | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index b8c8649b6..c9cb6c98f 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -114,9 +114,6 @@ export class ServiceLocationPage extends BasePage { await this.mobileButton.click(); await this.enterServiceAddressButton.click(); await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! }); - if (await this.repeatedClicksModalCloseButton.isVisible()) { - await this.repeatedClicksModalCloseButton.click(); - } if (faker.datatype.boolean()) { await this.vehicleProtectedYesButton.check(); } else { diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 9a3b87ae3..8b1597598 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -41,7 +41,7 @@ export class ServicePackagesPage extends BasePage { 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' }); - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('#app').getByRole('dialog').locator('button').filter({ hasText: 'Close' }); // this.validateURL(this.url); } @@ -76,6 +76,7 @@ export class ServicePackagesPage extends BasePage { if (await this.repeatedClicksModalCloseButton.isVisible()) { await this.repeatedClicksModalCloseButton.click(); } + await this.page.waitForTimeout(3000); await this.closeButton.click(); } else { From 2510f3c9debb9b2acecc99b30701cd0467ef047d Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 10 Apr 2025 13:51:18 -0400 Subject: [PATCH 10/50] Adds skip content site functionality. Allows skipping the content site (home page) based on an environment variable. This provides flexibility in test execution, enabling tests to bypass the home page when it's not relevant to the specific scenario being tested. The SKIP_CONTENT_SITE environment variable controls whether to skip the content site. --- playwright-tests/.env.dev | 4 ++++ playwright-tests/tests/0000__M.test.ts | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev index 47ec8336d..8c064ba62 100644 --- a/playwright-tests/.env.dev +++ b/playwright-tests/.env.dev @@ -4,9 +4,13 @@ # Environment type NODE_ENV="qa" +SKIP_CONTENT_SITE=false + # Base URLs by environment (uncomment the one you need) # qa BASE_URL="https://www-qa2.safelite.com/" +# qa with skipToInsurance Turned Off +# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true" # sys # BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle" # dev diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 6bef5a2f3..656e078c2 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -192,8 +192,9 @@ async function runWorkflow(page: Page, testCase: TestCase) { //============================= TEST WORKFLOW STEPS ============================= - // Execute home page for qa and dev environments (skip for sys) - if (process.env.NODE_ENV !== 'sys') { + // Use Environment Variable to decide whether or not we want to skip content site aka home page + + if (process.env.SKIP_CONTENT_SITE == "false") { await test.step('HomePage >> Lets Get Started', async () => { let homePage = testCase.pages.homePage; console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`); From 7d20b0074ccd5409d0d79918d08cabf8b710e031 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 10 Apr 2025 14:03:06 -0400 Subject: [PATCH 11/50] Removes PR gate pipeline config Removes the Playwright tests and related jobs from the Azure Pipelines configuration file. These jobs were unintentionally added to the branch and are no longer needed. --- azure-pipelines.yml | 168 --------- .../business-logic/types/JiraApi.ts | 111 ------ playwright-tests/impl/API/JiraApiUtil.ts | 143 -------- .../impl/reporter/JiraWritebackReporter.ts | 339 ------------------ 4 files changed, 761 deletions(-) delete mode 100644 playwright-tests/business-logic/types/JiraApi.ts delete mode 100644 playwright-tests/impl/API/JiraApiUtil.ts delete mode 100644 playwright-tests/impl/reporter/JiraWritebackReporter.ts diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8c5e61265..2cb18429c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,12 +34,6 @@ resources: variables: - group: Digital-Infrastructure - group: FixMyGlass-BuildBranches - - name: dockerImageName - value: 'playwright-tests' - - name: imageTag - value: '$(Build.BuildId)' - - name: totalShards - value: 2 stages: # PR's @@ -63,168 +57,6 @@ stages: npmLocation: $(Build.SourcesDirectory) testResultsFile: junit.xml summaryFileLocation: $(Build.SourcesDirectory)/coverage/cobertura-coverage.xml - - job: playwright_tests - continueOnError: true - strategy: - matrix: - shard1: - shardNumber: 1 - shard2: - shardNumber: 2 - - steps: - - task: Docker@2 - displayName: 'Build Docker Image' - inputs: - command: build - dockerfile: Dockerfile.playwright - repository: $(dockerImageName) - tags: $(imageTag) - arguments: '--no-cache --pull' - - - script: | - branch_name=$(System.PullRequest.SourceBranch) - echo "Retrieved branch name: '$branch_name'" - JIRA_CARD_NUMBER="${branch_name##*/}" - echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'" - # Create container and run tests - container_id=$(docker create \ - --ipc=host \ - -e CCIS_API_AUTH=$(CCIS_API_AUTH) \ - -e BASE_URL=$(BASE_URL) \ - -e CCIS_API_URL=$(CCIS_API_URL) \ - -e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \ - -e SHARD=$(shardNumber) \ - -e CI=true \ - -e ENABLE_MOCK_TESTING=true \ - -e ENABLE_ACCESSIBILITY_TESTING=true \ - -e NODE_ENV=$(NODE_ENV) \ - $(dockerImageName):$(imageTag) \ - npx concurrently -k -n "server,playwright"\ - "sed -i \"s|^process\.env\.VUE_APP_CONSUMER_CF_DISTRO = .*|process\.env\.VUE_APP_CONSUMER_CF_DISTRO='https://digitalapi.test.safelite.io'|\" \"./vue.config.js\" && echo \"Updated config file to use TEST APIs\" && npm run serve -- --port=8080"\ - "npx wait-on http://localhost:8080 && npm run test:playwright -- --shard=$(shardNumber)/$(totalShards) --reporter=list,blob --grep \"@$JIRA_CARD_NUMBER\"") - - # Start container and stream logs - echo "Starting tests for shard $(shardNumber)..." - docker start -a $container_id - - # Create directory for test results - echo "Creating test results directory..." - mkdir -p $(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber) - - # Copy test results from container - echo "Copying test results..." - docker cp $container_id:/app/blob-report/. $(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber)/ - - # 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)' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish Test Reports - Shard $(shardNumber)' - condition: always() - inputs: - targetPath: '$(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber)' - artifact: 'playwright-report-shard-$(shardNumber)' - publishLocation: 'pipeline' - - - script: | - docker rmi $(dockerImageName):$(imageTag) -f - displayName: 'Cleanup Docker Image' - condition: always() - - # Add job to merge test reports and update Jira - - job: download_and_merge_reports - dependsOn: playwright_tests - timeoutInMinutes: 10 - cancelTimeoutInMinutes: 10 - steps: - - task: DownloadPipelineArtifact@2 - inputs: - targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports' - - task: Docker@2 - displayName: 'Build Docker Image' - inputs: - command: build - dockerfile: Dockerfile.playwright - repository: $(dockerImageName) - tags: $(imageTag) - arguments: '--no-cache --pull' - - bash: | - branch_name=$(System.PullRequest.SourceBranch) - echo "Retrieved branch name: '$branch_name'" - JIRA_CARD_NUMBER="${branch_name##*/}" - echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'" - # Create container for jira writeback - container_id=$(docker create \ - --ipc=host \ - -e JIRA_SERVER=$(JIRA_SERVER) \ - -e JIRA_USERNAME=$(JIRA_USERNAME) \ - -e JIRA_API_KEY=$(JIRA_API_KEY) \ - -e JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER" \ - $(dockerImageName):$(imageTag) \ - bash -c "echo \"Moving Playwright reports out of subfolders...\" && - find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \; && - echo \"Merging reports...\" && - export NODE_OPTIONS=--max_old_space_size=4096 - PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx playwright merge-reports --reporter=ortoni-report,junit,\"/app/playwright-tests/impl/reporter/JiraWritebackReporter.ts\" ./playwright-reports - echo \"Contents of ortoni-report:\" && ls ./ortoni-report && - echo 'Current dir: ' && pwd - echo 'Contents of current dir: ' && ls - echo 'Contents of /app/test-results' && ls /app/test-results - echo \"Writing report to Jira card '$JIRA_CARD_NUMBER'...\" && - /app/devops/scripts/jira_writeback.sh add_comment \"$JIRA_CARD_NUMBER\" /app/ortoni-report/ortoni-report.html \"AUTOMATED TEST RUN: $(date)\" ") - - # Start container and stream logs - echo "Starting merge" - docker start -a $container_id - - # Create directory for test results - echo "Creating test results directory..." - mkdir -p $(System.DefaultWorkingDirectory)/ortoni-report - mkdir -p $(System.DefaultWorkingDirectory)/test-results - - # Copy test results from container - echo "Copying test results..." - docker cp $container_id:/app/ortoni-report/. $(System.DefaultWorkingDirectory)/ortoni-report - docker cp $container_id:/app/test-results/junit_results.xml $(System.DefaultWorkingDirectory)/test-results - - # Remove container - echo "Cleaning up container..." - docker rm $container_id - env: - JIRA_API_KEY: $(JIRA_API_KEY) - displayName: merge_and_publish_results_to_jira - - - task: PublishTestResults@2 - displayName: 'Publish test results' - inputs: - searchFolder: 'test-results' - testResultsFormat: 'JUnit' - testResultsFiles: 'junit_results.xml' - mergeTestResults: true - failTaskOnFailedTests: false - testRunTitle: 'Playwright Tests' - condition: succeededOrFailed() - - - task: PublishPipelineArtifact@1 - displayName: 'Publish Merged Report' - condition: always() - inputs: - targetPath: '$(System.DefaultWorkingDirectory)/ortoni-report' - artifact: 'playwright-merged-report' - publishLocation: 'pipeline' - - script: | - docker rmi $(dockerImageName):$(imageTag) -f - displayName: 'Cleanup Docker Image' - condition: always() - ${{ else }}: # Dev Build/Deploy - stage: Dev diff --git a/playwright-tests/business-logic/types/JiraApi.ts b/playwright-tests/business-logic/types/JiraApi.ts deleted file mode 100644 index d5b34b081..000000000 --- a/playwright-tests/business-logic/types/JiraApi.ts +++ /dev/null @@ -1,111 +0,0 @@ -export interface PutEditIssueRequestBody { - fields: Partial -} - -export interface PostTransitionIssueRequestBody { - transition: { id: string } -} - -export interface PostCreateIssueRequestBody extends JiraIssue {} - -export interface PostCreateIssueResponse { - id: string, - key: string -} - -export interface PostBulkCreateIssueRequestBody { - issueUpdates: PostCreateIssueRequestBody[] -} - -export interface BulkTransitionInput { - selectedIssueIdsOrKeys: string[], - transitionId: string -} - -export interface PostBulkTransitionIssuesRequestBody { - bulkTransitionInputs: BulkTransitionInput[], - sendBulkNotification: false -} - -export interface PostBulkCreateIssueResponse { - issues: PostCreateIssueResponse[] -} - -export interface PostAddCommentResponse { - body: JiraContent -} - -export interface JiraIssueFields { - summary: string, - description: JiraContent, - project?: JiraProject, - issuetype?: { id: string }, - parent?: JiraParent, - fixVersions?: JiraVersion[], - subtasks?: JiraSubTask[], - customfield_14857?: JiraContent // Test Steps field -} - -export interface JiraSubTask { - id: string, - key: string -} - -export interface JiraProject { - key: string -} - -export interface JiraParent { - key: string -} - -export interface JiraContent { - type?: string, - text?: string, - version?: number - content?: JiraContent[] -} - -export interface JiraVersion { - id: string -} - -export interface JiraTransition { - id: string, - name: string -} - -export interface JiraIssueType { - id: string, - self: string, - description: string, - iconUrl: string, - name: string, - untranslatedName: string, - subtask: boolean, -} - -export interface GetIssueTransitionsResponse { - transitions: JiraTransition[] -} - -export interface GetIssueTypesResponse { - issueTypes: JiraIssueType[] -} - -export interface PostBulkFetchIssuesRequestBody { - issueIdsOrKeys: string[] -} - -export interface PostBulkFetchIssuesResponse { - issues: GetIssueResponse[]; -} - -export interface JiraIssue { - key?: string, - id?: string, - transition?: { id: string }, - fields: JiraIssueFields -} - -export interface GetIssueResponse extends JiraIssue {} \ No newline at end of file diff --git a/playwright-tests/impl/API/JiraApiUtil.ts b/playwright-tests/impl/API/JiraApiUtil.ts deleted file mode 100644 index fdb970d1e..000000000 --- a/playwright-tests/impl/API/JiraApiUtil.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { PostBulkFetchIssuesRequestBody, PostBulkFetchIssuesResponse, GetIssueResponse, GetIssueTransitionsResponse, GetIssueTypesResponse, PostAddCommentResponse, PostBulkCreateIssueRequestBody, PostBulkCreateIssueResponse, PostCreateIssueRequestBody, PostCreateIssueResponse, PostTransitionIssueRequestBody, PutEditIssueRequestBody, PostBulkTransitionIssuesRequestBody } from "@business-logic/types/JiraApi"; -import axios, { request } from "axios"; -import axiosRetry from "axios-retry"; - -const jiraUrl = process.env.JIRA_SERVER!; -const jiraUsername = process.env.JIRA_USERNAME!; -const jiraApiKey = process.env.JIRA_API_KEY!; -const encodedAuthKey = Buffer.from(`${jiraUsername}:${jiraApiKey}`).toString('base64'); - -export default class JiraApiUtil { - readonly baseUrl: string; - readonly issueUrl: string; - readonly bulkIssueCreateUrl: string; - readonly bulkIssueFetchUrl: string; - readonly bulkTransitionIssuesUrl: string; - readonly axiosClient: axios.AxiosInstance; - - constructor() { - this.baseUrl = jiraUrl; - this.issueUrl = `${this.baseUrl}/rest/api/3/issue`; - this.bulkIssueCreateUrl = `${this.issueUrl}/bulk`; - this.bulkIssueFetchUrl = `${this.issueUrl}/bulkfetch`; - this.bulkTransitionIssuesUrl = `${this.baseUrl}/rest/api/3/bulk/issues/transition` - this.axiosClient = axios.create(); - // interceptor to log error message from api - this.axiosClient.interceptors.response.use( - response => response, - error => { - console.error('Axios Error:', error?.response?.data || error.message); - return Promise.reject(error); - } - ); - // Set up retries if requests are made too quickly - axiosRetry(this.axiosClient, { - retries: 4, - retryDelay: (retryCount) => { return Math.pow(2, retryCount) * 1000; }, // Exponential backoff - retryCondition: (error) => { return error.response?.status === 429 } // If rate-limit error - }); - } - - getIssue(issueKey: string) { - const url = `${this.issueUrl}/${issueKey}`; - return this.axiosClient.get(url, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - Accept: 'application/json' - } - }); - } - - postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) { - return this.axiosClient.post(this.bulkIssueFetchUrl, requestBody, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }); - } - - getIssueTransitions(issueKey: string) { - const url = `${this.issueUrl}/${issueKey}/transitions` - return this.axiosClient.get(url, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - Accept: 'application/json' - } - }); - } - - getIssueTypes(projectKey: string) { - const url = `${this.baseUrl}/rest/api/3/issue/createmeta/${projectKey}/issuetypes`; - return this.axiosClient.get(url, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - Accept: 'application/json' - } - }); - } - - postAddComment(issueKey: string, commentBody: PostAddCommentResponse) { - const url = `${this.issueUrl}/${issueKey}/comment`; - return this.axiosClient.post(url, commentBody, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }); - } - - postTransitionIssue(issueKey: string, requestBody: PostTransitionIssueRequestBody) { - const transitionUrl = `${this.issueUrl}/${issueKey}/transitions` - return this.axiosClient.post(transitionUrl, requestBody, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - }); - } - - putEditIssue(issueKey: string, requestBody: PutEditIssueRequestBody) { - const editIssueUrl = `${this.issueUrl}/${issueKey}` - return this.axiosClient.put(editIssueUrl, requestBody, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }); - } - - postBulkTransitionIssues(requestBody: PostBulkTransitionIssuesRequestBody) { - return this.axiosClient.post(this.bulkTransitionIssuesUrl, requestBody, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - }); - } - - postCreateIssue(requestBody: PostCreateIssueRequestBody) { - return this.axiosClient.post(this.issueUrl, requestBody, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - }); - } - - postBulkCreateIssue(requestBody: PostBulkCreateIssueRequestBody) { - return this.axiosClient.post(this.bulkIssueCreateUrl, requestBody, { - headers: { - Authorization: `Basic ${encodedAuthKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - }); - } -} \ No newline at end of file diff --git a/playwright-tests/impl/reporter/JiraWritebackReporter.ts b/playwright-tests/impl/reporter/JiraWritebackReporter.ts deleted file mode 100644 index c1db33244..000000000 --- a/playwright-tests/impl/reporter/JiraWritebackReporter.ts +++ /dev/null @@ -1,339 +0,0 @@ -import JiraApiUtil from "@impl/API/JiraApiUtil"; -import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter"; -import { formatDate } from "@impl/utils/DateUtils"; -import { GetIssueResponse, JiraIssue, JiraIssueFields } from "@business-logic/types/JiraApi"; - -const jiraCardNumber = process.env.JIRA_CARD_NUMBER!; -const jiraProjectKey = 'INSR'; -const passTransitionId = '111'; -const failTransitionId = '101'; - -export default class JiraWritebackReporter implements Reporter { - readonly jiraApiUtil: JiraApiUtil = new JiraApiUtil(); - readonly loadIssueCalls: (() => void)[] = []; // Calls to loadIssue must be deferred because they depend on information from the API. - readonly issuesToPass: string[] = []; // Issues to transition to "Pass". - readonly issuesToFail: string[] = []; // Issues to transition to "Fail". - readonly issuesToCreate: JiraIssue[] = []; // Issues to be batch created. - existingSubtasks: JiraIssue[] = []; // Array to hold existing subtasks of the dev card. - testCaseTypeId: string|undefined = undefined; // ID of the test case subtask type in Jira. Will be filled by API call. - bugTypeId:string|undefined = undefined; // ID of the bug subtask type. - parentCard: GetIssueResponse | undefined = undefined; // Variable to hold the parent card. Will be filled by API call. - - // Will need this if we want to move ortoni report upload into this reporter. - // readonly ortoniReport = new OrtoniReport(reportConfig); - - /** - * This function loads the IDs for the Jira Issue Types we use. - * - */ - async loadJiraIssueTypes() { - console.log(`JiraWritebackReporter >> Loading Jira Issue Types for project '${jiraProjectKey}'...`); - const issueTypesRes = await this.jiraApiUtil.getIssueTypes(jiraProjectKey); - const issueTypes = issueTypesRes.data; - this.testCaseTypeId = issueTypes.issueTypes.find(issueType => { - return issueType.subtask === true && issueType.name === 'Test Case Sub-task' - })?.id; - this.bugTypeId = issueTypes.issueTypes.find(issueType => { - return issueType.subtask === true && issueType.name === 'Bug Sub-task' - })?.id; - console.log('JiraWritebackReporter >> Loaded issue types.'); - } - - /** - * This function generates a test subtask based on the test results we pass in. - * @param test TestCase from onTestEnd() - * @param result TestResult from onTestEnd() - * @returns Jira Test Subtask based on test result. - */ - getCreateTestSubtask(test: TestCase, result: TestResult) { - if (result.status === 'skipped') { - return undefined; - } - const testStepTitles = result.steps.map(step => { - return `-\t${step.title}`; - }).join('\n'); - const allErrors = result.errors.map(value => { - return value.message - }).join('\n'); - - const description = `Most recent test status: ${result.status}.\nDuration: ${result.duration/1000} seconds.\nErrors:\n${allErrors}` - const testSubtaskBody: JiraIssueFields = { - summary: test.title, - description: { - content: [ - { - "content": [ - { - "text": description, - "type": "text" - } - ], - "type": "paragraph" - } - ], - "type": "doc", - "version": 1 - }, - project: { - key: jiraProjectKey - }, - issuetype: { - id: this.testCaseTypeId! - }, - parent: { - key: jiraCardNumber - }, - customfield_14857: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: testStepTitles - } - ] - } - ] - } - }; - return testSubtaskBody; - } - - /** - * This function generates a bug subtask based on the test results we pass in. - * @param test TestCase from onTestEnd() - * @param result TestResult from onTestEnd() - * @returns Jira Bug Subtask based on test result. - */ - getCreateBug(test: TestCase, result: TestResult) { - if (result.status === 'passed' || result.status === 'skipped') { - return undefined; - } - const allErrors = result.errors.map(value => { - return value.message - }).join('\n'); - - const bug: JiraIssueFields = { - summary: `TEST FAILED ${formatDate(new Date())}: ${test.title}`, - description: { - content: [ - { - "content": [ - { - "text": allErrors, - "type": "text" - } - ], - "type": "paragraph" - } - ], - "type": "doc", - "version": 1 - }, - project: { - key: jiraProjectKey - }, - issuetype: { - id: this.bugTypeId! - }, - parent: { - key: jiraCardNumber - } - }; - return bug; - } - - /** - * This function gives us the correct transition for a test subtask based on this test result. - * @param result TestResult from onTestEnd() - * @returns Correct transition to pass to the Jira API. - */ - getSubtaskTransition(result: TestResult): { id: string } | undefined { - if (result.status === 'skipped') { - return undefined; - } - - if (result.status === 'passed') { - return { - id: passTransitionId - }; - } else { - return { - id: failTransitionId - }; - } - } - - /** - * Grabs parent card and existing subtasks. - */ - async loadReporterData() { - await this.loadJiraIssueTypes(); - - console.log('JiraWritebackReporter >> Searching for parent card...'); - const parentRes = await this.jiraApiUtil.getIssue(jiraCardNumber); - const parentCard = parentRes.data; - console.log(`JiraWritebackReporter >> Retrieved parent card: ${parentCard.key}: ${parentCard.fields.summary}`); - - console.log(`JiraWritebackReporter >> Searching for all subtasks of parent card '${jiraCardNumber}'...`); - const subtaskIds = parentCard.fields.subtasks?.map(subtask => { - return subtask.id; - }); - - if (subtaskIds && subtaskIds.length > 0) { - for (let i = 0; i < subtaskIds.length; i+=50) { - const batch = subtaskIds.slice(i, i+50); - const subTasksRes = await this.jiraApiUtil.postBulkFetchIssues({ issueIdsOrKeys: batch }); - this.existingSubtasks.push(...subTasksRes.data.issues); - } - console.log('JiraWritebackReporter >> Subtasks retrieved.'); - } else { - console.log('JiraWritebackReporter >> No subtasks were found.'); - } - - } - - /** - * Bulk creates test cases and bugs in batches of up to 50. Bulk transitions existing test case subtasks. - */ - async writeResults() { - for (let i = 0; i < this.issuesToCreate.length; i+=50) { - const batch = this.issuesToCreate.slice(i, i + 50); - console.log('JiraWritebackReporter >> Creating issue batch...'); - // fire request - await this.jiraApiUtil.postBulkCreateIssue({ issueUpdates: batch }); - console.log('JiraWritebackReporter >> Issue batch created.'); - } - - if (this.issuesToPass.length > 0) { - console.log('JiraWritebackReporter >> Transitioning passed test cases to "Pass"...') - await this.jiraApiUtil.postBulkTransitionIssues( { - bulkTransitionInputs: [{ - selectedIssueIdsOrKeys: this.issuesToPass, - transitionId: passTransitionId - }], - sendBulkNotification: false - }); - console.log('JiraWritebackReporter >> Transition success.'); - } - - if (this.issuesToFail.length > 0) { - console.log('JiraWritebackReporter >> Transitioning failed test cases to "Fail"...') - await this.jiraApiUtil.postBulkTransitionIssues( { - bulkTransitionInputs: [{ - selectedIssueIdsOrKeys: this.issuesToFail, - transitionId: failTransitionId - }], - sendBulkNotification: false - }); - console.log('JiraWritebackReporter >> Transition success.') - } - } - - /** - * Builds and loads appropriate response based on test result. This can be a test subtask/bug subtask or a call to transition a test subtask. - * @param test TestCase from onTestEnd() - * @param result TestResult from onTestEnd() - * @returns Void - */ - loadIssue(test: TestCase, result: TestResult) { - if (result.status === 'skipped') return; - if (result.status !== 'passed' && result.retry < test.retries) return; // Skip if test case failed and this isn't the last retry. - - let existingTestSubtask: JiraIssue | undefined = undefined; - let existingBug: JiraIssue | undefined = undefined; - const subtaskTransition = this.getSubtaskTransition(result)!; // Get either a Pass or Fail transition depending on test results. - - console.log('JiraWritebackReporter >> Checking for existing subtasks for this test...'); - for (const card of this.existingSubtasks) { - if (card.fields.issuetype?.id === this.testCaseTypeId) { - if (card.fields.summary === test.title) { - existingTestSubtask = card; - console.log('JiraWritebackReporter >> Found existing test subtask.'); - } - } else if (card.fields.issuetype?.id === this.bugTypeId) { - if (new RegExp(`^TEST FAILED [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]: ${test.title}$`).test(card.fields.summary)) { - existingBug = card; - console.log('JiraWritebackReporter >> Found existing bug.'); - // TODO: Handle multiple matches for bug/test case. Currently picks one in that case. - } - } - } - if (!(existingTestSubtask || existingBug)) { - console.log('JiraWritebackReporter >> No existing test subtask or bug was found.') - } - - const subTask = this.getCreateTestSubtask(test, result); // Will return undefined if test status is 'skipped' - const bug = this.getCreateBug(test, result); // Will return undefined if we don't need one - if (subTask) { - // Create or Edit Subtask - if (existingTestSubtask) { - if (result.status === 'passed') { - this.issuesToPass.push(existingTestSubtask.key!); - } else { - this.issuesToFail.push(existingTestSubtask.key!); - } - } else { - this.issuesToCreate.push({ - fields: subTask, - transition: subtaskTransition - }); - } - } - - if (bug) { - // Create or Edit Bug - if (existingBug) { - // Leave it alone. This reporter should not modify existing bugs. - } else { - this.issuesToCreate.push({ - fields: bug - }); - } - } - } - - /** - * This function loads calls to loadIssue() in an array to be executed when the necessary information from the Jira API is available. - * @param test - * @param result - */ - onTestEnd(test: TestCase, result: TestResult) { - // this.ortoniReport.onTestEnd(test, result); - this.loadIssueCalls.push(() => { this.loadIssue(test, result); }); - } - - /** - * Makes calls to load and execute API calls to Jira. - * @param result - * @returns Promise to write results to Jira. - */ - onEnd(result: FullResult): Promise<{ status?: FullResult["status"]; } | undefined | void>|void { - // this.ortoniReport.onEnd(result); - return this.loadReporterData().then(async () => { - this.loadIssueCalls.map(fn => fn()); - await this.writeResults(); - console.log(`JiraWritebackReporter >> Posted necessary changes for '${jiraCardNumber}'`); - }); - } - - // These overloads will become necessary if we move the ortoni report upload into this reporter - // onBegin(config: FullConfig, suite: Suite): void { - // this.ortoniReport.onBegin(config, suite); - // } - - // onError(error: TestError): void { - // this.ortoniReport.onError(error); - // } - - // onExit(): Promise { - // return this.ortoniReport.onExit(); - // } - - // onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void { - // this.ortoniReport.onStdOut(chunk, test, result); - // } -} \ No newline at end of file From 27b6e968e2435adab9638be3f9e4f09ccd7c4000 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Fri, 11 Apr 2025 10:13:03 -0400 Subject: [PATCH 12/50] adding quotes when defining base url because of -e problem --- playwright-tests/azure-pipelines-automated-testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index 5634b4bd6..7ae582f8e 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -42,7 +42,7 @@ stages: container_id=$(docker create \ --ipc=host \ -e CCIS_API_AUTH=$(CCIS_API_AUTH) \ - -e BASE_URL=$(BASE_URL) \ + -e BASE_URL="$(BASE_URL)" \ -e CCIS_API_URL=$(CCIS_API_URL) \ -e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \ -e SHARD=$(shardNumber) \ From c26b214084a8e7234c1de9970b6d38e29512cc04 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Mon, 21 Apr 2025 11:51:23 -0400 Subject: [PATCH 13/50] added skip_Content_site variable --- playwright-tests/azure-pipelines-automated-testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index 7ae582f8e..ac332ef44 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -45,7 +45,7 @@ stages: -e BASE_URL="$(BASE_URL)" \ -e CCIS_API_URL=$(CCIS_API_URL) \ -e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \ - -e SHARD=$(shardNumber) \ + -e SKIP_CONTENT_SITE=$(SKIP_CONTENT_SITE) \ -e CI=true \ -e NODE_ENV=$(NODE_ENV) \ $(dockerImageName):$(imageTag) \ From 4344c19a30492c668ce54128baff8e804db8ad57 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:01:53 -0400 Subject: [PATCH 14/50] Payment method page validation changes --- .../business-logic/types/CustomerDetails.ts | 2 + playwright-tests/pages/PaymentMethodPage.ts | 329 ++++++++++++------ playwright-tests/pages/SchedulePage.ts | 51 ++- playwright-tests/pages/ServiceLocationPage.ts | 21 +- .../tests/CashRepairMobileCreditCard.ts | 2 +- .../tests/CashReplaceMultiGlassMobile.ts | 2 +- .../CashReplaceSafeliteCanNotRecalMobile.ts | 2 +- .../tests/CashReplaceVinMobile.ts | 2 +- 8 files changed, 287 insertions(+), 124 deletions(-) diff --git a/playwright-tests/business-logic/types/CustomerDetails.ts b/playwright-tests/business-logic/types/CustomerDetails.ts index bb1f08b96..0ea0e9605 100644 --- a/playwright-tests/business-logic/types/CustomerDetails.ts +++ b/playwright-tests/business-logic/types/CustomerDetails.ts @@ -9,6 +9,8 @@ export interface ICustomerDetails { notes: string, address: IAddress, apptDate?: string, + apptTime?: string, + apptDuration?: string, packagePrice?: string } diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index c859aa916..519cface5 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -1,7 +1,7 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IPaymentDetails } from '@business-logic/types/CustomerDetails'; -import { PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; +import { AppointmentType, PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; import { PaymentPage } from './PaymentPage'; import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; @@ -17,6 +17,7 @@ export class PaymentMethodPage extends BasePage { readonly payInFourButton: Locator; readonly amountDueTextField: Locator; readonly amountDueDropDown: Locator; + readonly appointmentDetailsSection: Locator; readonly appointmentDetailsDropdown: Locator; readonly subtotalAmountTextField: Locator; readonly submitButton: Locator; @@ -48,6 +49,7 @@ export class PaymentMethodPage extends BasePage { this.payAtServiceButton = this.page.locator('[buttonlabel="Pay at my appointment"]'); //this.page.getByText('Pay at time of service'); this.amountDueTextField = this.page.getByLabel('expand cart').locator('.amount-due'); this.amountDueDropDown = this.page.getByLabel('expand cart'); + this.appointmentDetailsSection = this.page.locator('div .appt-details-snapshot'); this.appointmentDetailsDropdown = this.page.getByLabel('expand appointment details'); this.payWithInsuranceButton = this.page.locator('div').filter({ hasText: /^Pay with insurance$/ }).nth(1); this.subtotalAmountTextField = this.page.locator('.sub-total span').nth(1); @@ -88,110 +90,27 @@ export class PaymentMethodPage extends BasePage { // Wait for review table to be visible to ensure page is loaded await this.appointmentDetailsDropdown.waitFor({state: "visible"}); - await this.appointmentDetailsDropdown.click(); // Expand cart to see all details + await this.appointmentDetailsDropdown.click(); + + let expectedAppointmentDetails = new Map(); + expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedVehicleDamage(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedServiceLocation(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails); + + let actualAppointmentDetails = await this.getActualAppointmentDetails(); + + for (const key in expectedAppointmentDetails) { + expect.soft(actualAppointmentDetails[key]?.map(item => item.toLowerCase())) + .toEqual(expectedAppointmentDetails[key].map(item => item.toLowerCase())); + } + + // Expand cart to see all details await this.amountDueDropDown.click(); await this.reviewTable.waitFor({ state: "visible" }); - // Helper method to get content lines from a section - const getSectionContent = async (section: Locator) => { - // First make sure section exists - if (await section.count() === 0) return []; - - // Find all content lines within this section - const contentLines = await section.locator('div.small.review-block-content').allInnerTexts(); - return contentLines; - }; - - // Validate vehicle information - const vehicleContent = await getSectionContent(this.vehicleSection); - if (vehicleContent.length > 0) { - const vehicleText = vehicleContent[0]; - expect.soft(vehicleText).toContain(`${vehicleDetails?.year} ${vehicleDetails?.make} ${vehicleDetails?.model}`); - } - - // Validate damage type - - // TODO: Work out logic on how to verify vehicle Damage in payment details with vehicleDamage - - // const damageContent = await getSectionContent(this.damageSection); - // if (vehicleDamage && damageContent.length > 0) { - // const damageText = damageContent[0]; - - // // For each damage type in the array, check if its display text is in the damage content - // for (const damage of vehicleDamage) { - // const expectedDamageText = this.getDamageDisplayText(damage); - - // // If this is a single damage item, it should match exactly - // if (vehicleDamage.length === 1) { - // expect.soft(damageText).toContain(expectedDamageText); - // } else { - // // For multiple damages, check if any of the damage content lines contain this damage type - // const damageFound = damageContent.some(content => - // content.includes(expectedDamageText) - // ); - // expect.soft(damageFound).toBeTruthy(); - // } - // } - // } - - // Validate service details based on package - const serviceContent = await getSectionContent(this.serviceDetailsSection); - if (servicePackage && serviceContent.length > 0) { - //TODO: Add service package validation - // move over logic from Kishan's code - - // Additional validations for Standard and Premium packages - if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) { - await expect.soft(this.wiperBladesText).toBeVisible(); - } - - if (servicePackage === ServicePackage.Premium) { - await expect.soft(this.rainDefenseText).toBeVisible(); - } - } - - // Validate service location - const locationContent = await getSectionContent(this.serviceLocationSection); - if (appointmentDetails?.serviceLocation && locationContent.length > 0) { - // Find the title element of the service location section - const serviceLocationTitle = this.serviceLocationSection.locator('span').first(); - const serviceLocationValue = await serviceLocationTitle.innerText(); - - // Check for mobile/inshop service wording - if (appointmentDetails.serviceLocation.toString().includes('Mobile')) { - expect.soft(serviceLocationValue).toContain("We're coming to you"); - } else if (appointmentDetails.serviceLocation.toString().includes('InShop')) { - expect.soft(serviceLocationValue).toContain("Bring to shop"); - } - - // Validate address if available - if (appointmentDetails.serviceAddress && locationContent.length > 0) { - const addressText = locationContent[0].toLowerCase(); - expect.soft(addressText).toContain(appointmentDetails.serviceAddress.street.toLowerCase()); - } - } - - // Validate appointment date/time - const appointmentContent = await getSectionContent(this.appointmentDateSection); - if (customerDetails?.apptDate && appointmentContent.length > 0) { - const appointmentText = appointmentContent[0]; - expect.soft(appointmentText).toContain(customerDetails.apptDate); - } - - // Validate contact details - const contactContent = await getSectionContent(this.contactDetailsSection); - if (customerDetails && contactContent.length > 0) { - const fullName = `${customerDetails.firstName} ${customerDetails.lastName}`; - const email = customerDetails.email; - const phone = customerDetails.phoneNumber; - - // Check if contact details are present - const contactTextJoined = contactContent.join(' '); - expect.soft(contactTextJoined).toContain(fullName); - expect.soft(contactTextJoined).toContain(email); - expect.soft(contactTextJoined).toContain(phone); - } - // Cart Validation // Get pricing information from cart panel if (await this.subtotalText.isVisible()) { @@ -339,4 +258,210 @@ l } } + async getActualAppointmentDetails(): Promise { + let actualAppointmentDetails = new Map(); + let appointmentDetailsSubSections = await this.appointmentDetailsSection.locator('div .py-3').all(); + + for (let element of appointmentDetailsSubSections) { + let label = await element.locator('div .text-block').innerText(); + //added to trim the text to remove any leading or trailing spaces (example: "expert installation " to "expert installation") + let value = (await element.locator('.review-block-content').allInnerTexts() as string[]).map(item => item.trim()); + actualAppointmentDetails[label] = value; + } + return actualAppointmentDetails; + } + + async getExpectedVehicleDetails(testdata: Partial, expectedServicePackageDetails: Map): Promise { + const { vehicleDetails } = testdata; + expectedServicePackageDetails["Vehicle"] = [vehicleDetails?.year + " " + vehicleDetails?.make + " " + vehicleDetails?.model]; + return expectedServicePackageDetails; + } + + async getExpectedVehicleDamage(testdata: Partial, expectedServicePackageDetails: Map): Promise { + + const {vehicleDamage} = testdata; + + let vehicleDamageText: string[] = []; + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldOneChip)) { + vehicleDamageText.push("Windshield repair - 1 chip"); + } + + else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldTwoChips)) { + vehicleDamageText.push("Windshield repair - 2 chips"); + } + + else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldThreeChips)) { + vehicleDamageText.push("Windshield repair - 3 chips"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldCrack)) { + vehicleDamageText.push("Windshield crack"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverVentGlass)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Driver side", "Vent glass"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverFrontDoor)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Driver side", "Front door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverRearDoor)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Driver side", "Back door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverQuarterPanel)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Driver side", "Quarter panel"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerVentGlass)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Passenger side", "Vent glass"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerFrontDoor)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Passenger side", "Front door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerRearDoor)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Passenger side", "Back door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerQuarterPanel)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Passenger side", "Quarter panel"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.RearWindow || item == VehicleDamage.RearSliding)) { + vehicleDamageText.push("Rear window"); + } + + expectedServicePackageDetails["Damage"] = vehicleDamageText; + return expectedServicePackageDetails; + } + + async expectedServicePackageDetails(testData: Partial, expectedServicePackageDetails: Map): Promise { + const {servicePackage} = testData; + + let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + let isRepair = localStorage.order.damage.isRepair as boolean; + let isInsurance = localStorage.order.payment.isInsurance as boolean; + let hasNonWindshieldGlass: boolean = false; + if (!isRepair) { + hasNonWindshieldGlass = localStorage.order.lineItems.glassParts.find((item: any) => item.partType !== "WINDSHIELD") ? true : false; + } + let isCaliforniaState = localStorage.order.serviceLocation.state as string == 'CA' ? true : false; + let recalRequired = !isRepair && (isInsurance || isCaliforniaState) && + localStorage.order.lineItems.glassParts.find((item: any) => item["requiresRecalibration"].value === true) ? true : false; + let stringForRepair: string[] = ["Expert windshield repair", "Exclusive resin sealant", "Nationwide lifetime guarantee"]; + let stringIfRecal = isRepair + ? "" + : recalRequired + ? " and recalibration" + : ""; + let stringForReplace: string[] = [hasNonWindshieldGlass ? "New replacement glass" : "New replacement windshield", "Expert installation" + `${stringIfRecal}`, "Nationwide lifetime warranty"]; + switch (servicePackage) + { + case ServicePackage.GlassOnly: + expectedServicePackageDetails["Glass service only"] = isRepair + ? stringForRepair + : stringForReplace; + break; + case ServicePackage.Standard: + stringForRepair.push("New wiper blades"); + stringForReplace.push("New wiper blades"); + expectedServicePackageDetails["Standard service"] = isRepair + ? stringForRepair + : stringForReplace; + break; + case ServicePackage.Premium: + stringForRepair.push("New wiper blades", "Rain Defense™"); + stringForReplace.push("New wiper blades", "Rain Defense™"); + expectedServicePackageDetails["Premium service"] = isRepair + ? stringForRepair + : stringForReplace; + break; + } + return expectedServicePackageDetails; + } + + async getExpectedServiceLocation(testData: Partial, expectedServicePackageDetails: Map): Promise { + const { appointmentDetails } = testData; + let serviceLocationTitle = appointmentDetails?.serviceLocation == AppointmentType.Mobile + ? "We're coming to you" + : "You're going to a Safelite shop"; + let serviceLocation: string[] = []; + + serviceLocation.push( + appointmentDetails?.serviceLocation == AppointmentType.Mobile + ? appointmentDetails?.serviceAddress + ? appointmentDetails.serviceAddress.street + ", " + appointmentDetails.serviceAddress.city + ", " + appointmentDetails.serviceAddress.state + " " + appointmentDetails.serviceAddress.postalCode + : "" + : appointmentDetails?.shopAddress + ? appointmentDetails.shopAddress + : "" + ); + + expectedServicePackageDetails[serviceLocationTitle] = serviceLocation; + return expectedServicePackageDetails; + } + + async getExpectedAppointmentDate(testData: Partial, expectedServicePackageDetails: Map): Promise { + const { customerDetails, appointmentDetails } = testData; + let appointmentDateText: string[] = []; + const isMobileAppointment = appointmentDetails?.serviceLocation == AppointmentType.Mobile + if (isMobileAppointment) { + let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + let jobMinMinutes = localStorage.order.schedule.jobMinMinutes as number; + let jobMaxMinutes = localStorage.order.schedule.jobMaxMinutes as number; + + if (jobMinMinutes < 60) { + customerDetails!.apptDuration = `${jobMinMinutes} - ${jobMaxMinutes} minutes`; + } else { + const minHours = Math.floor(jobMinMinutes / 60); + const maxHours = Math.floor(jobMaxMinutes / 60); + customerDetails!.apptDuration = `${minHours} - ${maxHours} hours`; + } + } + + appointmentDateText.push( + customerDetails?.apptDate ? await this.getFormattedAppointmentDate(customerDetails.apptDate) + " " + customerDetails?.apptTime?.replace("-", "—") : "", + customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : "" + ); + + expectedServicePackageDetails["Appointment date + time"] = appointmentDateText; + return expectedServicePackageDetails; + } + + async getExpectedCustomerDetails(testData: Partial, expectedServicePackageDetails: Map): Promise { + const { customerDetails } = testData; + let customerDetailsText: string[] = []; + + customerDetailsText.push( + customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(), + customerDetails?.email ? customerDetails?.email.toUpperCase() : "", + customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "", + "Opted out of text message updates" + ); + + expectedServicePackageDetails["Contact details"] = customerDetailsText; + return expectedServicePackageDetails; + } + + async getFormattedAppointmentDate(appointmentDate: string) { + const currentYear = new Date().getFullYear(); + + // Parse the original date + let parsedAppointmentDate = new Date(`${appointmentDate}, ${currentYear}`); + + let currentDate = new Date(); + currentDate.setDate(currentDate.getDate() - 2); + + // If the original date is not in the future, adjust the year + if (parsedAppointmentDate < currentDate) { + parsedAppointmentDate.setFullYear(parsedAppointmentDate.getFullYear() + 1); + } + + // Format the new date as a string + let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + return updatedAppointmentDate; + } } \ No newline at end of file diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index bea9e82bd..0bf2b1105 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -1,8 +1,9 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; -import { IAppointmentDetails } from '@business-logic/types/CustomerDetails'; +import { IAppointmentDetails, ICustomerDetails } from '@business-logic/types/CustomerDetails'; import { formatDate, formatTime } from '@impl/utils/DateUtils'; import { AppointmentType, ServiceLocation } from '@business-logic/types/Enums'; +import { time } from 'console'; export class SchedulePage extends BasePage { readonly page: Page; @@ -13,6 +14,8 @@ export class SchedulePage extends BasePage { readonly dropOffButton: Locator; readonly dateText: Locator; readonly viewMoreDatesLink: Locator; + readonly appointmentDuration: Locator; + readonly timeSlots: Locator; constructor(page: Page) { super(page); @@ -23,6 +26,8 @@ export class SchedulePage extends BasePage { this.dropOffButton = this.page.getByText('Drop off your vehicle', { exact: true }); this.dateText = this.page.locator('label.modal-title'); this.viewMoreDatesLink = this.page.getByText(/View more dates/).first(); + this.appointmentDuration = this.page.locator('.duration-text-block'); + this.timeSlots = this.page.locator('fieldset:has(legend[id *= \'chooseTimeSlot\']) label'); } async scheduleAppointment(appointmentDetails: IAppointmentDetails) { @@ -40,20 +45,40 @@ export class SchedulePage extends BasePage { await this.modalContinueButton.click(); } - async scheduleFirstAppointment(serviceLocation: AppointmentType) { - if (await this.firstAvailableDate.isVisible()) { - await this.firstAvailableDate.click(); - } else { + async scheduleFirstAppointment(customerDetails: ICustomerDetails) { + while (!(await this.firstAvailableDate.isVisible())) { await this.viewMoreDatesLink.click(); - while(await this.firstAvailableDate.isHidden()){ - await this.viewMoreDatesLink.click(); - } - await this.firstAvailableDate.click(); } + await this.firstAvailableDate.click(); - serviceLocation === AppointmentType.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); - const apptDate = `${await this.dateText.allInnerTexts()}` + // const timeSlots = this.timeSlots; + const timeSlotCount = await this.timeSlots.count(); + const randomIndex = Math.floor(Math.random() * timeSlotCount); + const timeSlot = this.timeSlots.nth(randomIndex); + await timeSlot.click().then(async () => { + customerDetails.apptTime = await this.getFormattedTimeSlot(timeSlot); + }); + // appointmentmentDetails.serviceLocation === AppointmentType.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); + customerDetails.apptDate = `${await this.dateText.allInnerTexts()}` + customerDetails.apptDuration = (await this.appointmentDuration.innerText()).replace("Duration: ", ""); await this.modalContinueButton.click(); - return (apptDate); } -} \ No newline at end of file + + async getFormattedTimeSlot(timeSlot: Locator) { + const selectedTimeSlot = await timeSlot.innerText(); + let formattedTimeSlot: string = ""; + if (selectedTimeSlot.toLowerCase().includes("drop off")) + { + 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"; + } + else if (selectedTimeSlot.includes("-") || selectedTimeSlot.toLowerCase().includes("Earlybird")) + { + formattedTimeSlot = selectedTimeSlot.includes("Earlybird") ? "arriving between 8:00 AM - 12:00 PM" : `arriving between ${selectedTimeSlot}`; + } + else + { + formattedTimeSlot = `at ${selectedTimeSlot}`; + } + return formattedTimeSlot; + } +} \ No newline at end of file diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index b8c8649b6..14830acf4 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -55,7 +55,7 @@ export class ServiceLocationPage extends BasePage { // For in-shop and drop off this.selectAShopOptions = this.page.locator('[class="shop-question"]'); - this.firstAppointmentButton = this.page.locator('div').filter({ hasText: /Appts/}).first(); + this.firstAppointmentButton = this.page.locator('fieldset:has(legend#chooseShop)').locator('label').first(); this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))'); this.updateZipTextBox = this.page.locator('#serviceZipCode'); this.saveZipButton = this.page.getByText('Save ZIP code', { exact: true }); @@ -103,9 +103,14 @@ export class ServiceLocationPage extends BasePage { await this.saveZipButton.click(); } await this.inShopButton.click(); - await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); + await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click()); } else { - await this.firstAppointmentButton.click(); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } @@ -115,7 +120,7 @@ export class ServiceLocationPage extends BasePage { await this.enterServiceAddressButton.click(); await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! }); if (await this.repeatedClicksModalCloseButton.isVisible()) { - await this.repeatedClicksModalCloseButton.click(); + await this.repeatedClicksModalCloseButton.click(); } if (faker.datatype.boolean()) { await this.vehicleProtectedYesButton.check(); @@ -134,7 +139,13 @@ export class ServiceLocationPage extends BasePage { await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); } else { // await this.firstAppointmentButton.click(); - await this.clickWithRetry(this.firstAppointmentButton, this.page); + // await this.clickWithRetry(this.firstAppointmentButton, this.page); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } diff --git a/playwright-tests/tests/CashRepairMobileCreditCard.ts b/playwright-tests/tests/CashRepairMobileCreditCard.ts index 3674159aa..892c8d71e 100644 --- a/playwright-tests/tests/CashRepairMobileCreditCard.ts +++ b/playwright-tests/tests/CashRepairMobileCreditCard.ts @@ -28,7 +28,7 @@ const cashRepairMobileCCData : Partial = { serviceAddress: { street: '13735 San Antonio Ave', city: 'Chino', - state: 'California', + state: 'CA', postalCode: '91710', country: 'United States' } diff --git a/playwright-tests/tests/CashReplaceMultiGlassMobile.ts b/playwright-tests/tests/CashReplaceMultiGlassMobile.ts index 54da96bb6..eac5e2263 100644 --- a/playwright-tests/tests/CashReplaceMultiGlassMobile.ts +++ b/playwright-tests/tests/CashReplaceMultiGlassMobile.ts @@ -35,7 +35,7 @@ const cashReplaceMultiGlassMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' }, diff --git a/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts b/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts index 9ad33f23d..f7d301da1 100644 --- a/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts +++ b/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts @@ -48,7 +48,7 @@ const cashReplaceSafeliteCanNotRecalMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' }, diff --git a/playwright-tests/tests/CashReplaceVinMobile.ts b/playwright-tests/tests/CashReplaceVinMobile.ts index 59930b1ac..37470200e 100644 --- a/playwright-tests/tests/CashReplaceVinMobile.ts +++ b/playwright-tests/tests/CashReplaceVinMobile.ts @@ -45,7 +45,7 @@ const cashReplaceVinMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' } From c74a6a7d10fa571b89cf855a4694738c110a0182 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:13:15 -0400 Subject: [PATCH 15/50] adding more changes --- playwright-tests/pages/ServiceLocationPage.ts | 2 +- playwright-tests/pages/ServicePackagesPage.ts | 2 +- playwright-tests/tests/0000__M.test.ts | 2 +- playwright-tests/tests/CashReplaceDynamicRecalMobile.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index 14830acf4..f405175e6 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -71,7 +71,7 @@ export class ServiceLocationPage extends BasePage { this.vehicleProtectedYesButton = this.page.locator('label').filter({ hasText: 'Yes' }).locator('div'); this.vehicleProtectedNoButton = this.page.locator('label').filter({ hasText: 'No' }).locator('div'); this.saveAddressButton = this.page.getByRole('button', { name: 'Continue' }) - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); } async selectLocation(appointmentDetails: IAppointmentDetails){ diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 9a3b87ae3..360b285c3 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -41,7 +41,7 @@ export class ServicePackagesPage extends BasePage { 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' }); - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); // this.validateURL(this.url); } diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 6bef5a2f3..6f84c9431 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -482,7 +482,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { // Schedule appointment await test.step('SchedulePage >> Select day and time', async () => { let schedulePage = testCase.pages.schedulePage; - customerDetails!.apptDate! = await schedulePage.scheduleFirstAppointment(appointmentDetails!.serviceLocation); + await schedulePage.scheduleFirstAppointment(customerDetails!); }); // Enter contact details diff --git a/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts b/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts index 50c1d72b5..25bb3bf50 100644 --- a/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts +++ b/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts @@ -39,7 +39,7 @@ const cashReplaceDynamicRecalMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' }, From abb78523c97f8b6c3e62d06d9095b3552a203cf7 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Wed, 23 Apr 2025 14:45:20 -0400 Subject: [PATCH 16/50] Fixes service location page selector issue Updates the selectors for the first appointment button and the repeated clicks modal close button on the service location page. Also, adjusts the logic to ensure the shop selection is visible and clickable, particularly when dealing with dynamic content loading or potential rendering issues. Additionally, shop address is saved in appointment details This resolves issues with element selection and interaction on the service location page, improving the reliability of the tests. --- playwright-tests/pages/ServiceLocationPage.ts | 22 ++++++++++++++----- playwright-tests/pages/ServicePackagesPage.ts | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index c9cb6c98f..609f2d1db 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -55,7 +55,7 @@ export class ServiceLocationPage extends BasePage { // For in-shop and drop off this.selectAShopOptions = this.page.locator('[class="shop-question"]'); - this.firstAppointmentButton = this.page.locator('div').filter({ hasText: /Appts/}).first(); + this.firstAppointmentButton = this.page.locator('fieldset:has(legend#chooseShop)').locator('label').first(); this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))'); this.updateZipTextBox = this.page.locator('#serviceZipCode'); this.saveZipButton = this.page.getByText('Save ZIP code', { exact: true }); @@ -71,7 +71,7 @@ export class ServiceLocationPage extends BasePage { this.vehicleProtectedYesButton = this.page.locator('label').filter({ hasText: 'Yes' }).locator('div'); this.vehicleProtectedNoButton = this.page.locator('label').filter({ hasText: 'No' }).locator('div'); this.saveAddressButton = this.page.getByRole('button', { name: 'Continue' }) - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); } async selectLocation(appointmentDetails: IAppointmentDetails){ @@ -103,9 +103,14 @@ export class ServiceLocationPage extends BasePage { await this.saveZipButton.click(); } await this.inShopButton.click(); - await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); + await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click()); } else { - await this.firstAppointmentButton.click(); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } @@ -130,8 +135,13 @@ export class ServiceLocationPage extends BasePage { if (appointmentDetails && appointmentDetails.shopAddress) { await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); } else { - // await this.firstAppointmentButton.click(); - await this.clickWithRetry(this.firstAppointmentButton, this.page); + /// await this.clickWithRetry(this.firstAppointmentButton, this.page); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 8b1597598..65bba63b8 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -41,7 +41,7 @@ export class ServicePackagesPage extends BasePage { 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' }); - this.repeatedClicksModalCloseButton = this.page.locator('#app').getByRole('dialog').locator('button').filter({ hasText: 'Close' }); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); // this.validateURL(this.url); } From 3c84eefa05a999f75aadedc31fd964f02432aa04 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Wed, 23 Apr 2025 15:26:01 -0400 Subject: [PATCH 17/50] Adds sqlite dependencies Adds sqlite and sqlite3 as dependencies. These dependencies are required for implementing local database functionality. --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 923fec124..24c5e32e9 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "prettier": "^3.3.3", "sass": "^1.77.8", "sass-loader": "^8.0.2", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "typescript": "^4.9.5" }, "eslintConfig": { From eb97825cdfa9a2850595444c3a68ffdbf2bc1bc3 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 24 Apr 2025 10:00:50 -0400 Subject: [PATCH 18/50] fixed axios imports --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 24c5e32e9..fb65152d5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@iframe-resizer/child": "^5.3.3", + "axios": "^0.23.0", "bootstrap": "^5.3.3", "core-js": "^3.38.1", "http-status-codes": "^2.3.0", @@ -41,9 +42,6 @@ "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "^27.0.0", - "axios": "^1.8.4", - "axios-mock-adapter": "^2.1.0", - "axios-retry": "^4.5.0", "dotenv-safe": "^9.1.0", "eslint": "8.57", "eslint-plugin-prettier": "^5.2.1", From 932949004e1cc3695ebea7f486fc97a0446aa25d Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:17:56 -0400 Subject: [PATCH 19/50] Refactored master script with step decorator feature --- .../business-logic/types/ITestPages.ts | 4 +- playwright-tests/business-logic/types/Step.ts | 27 + .../business-logic/types/TestCase.ts | 4 +- playwright-tests/pages/CCPolicyInfoPage.ts | 10 + .../pages/CapabilityQuestionsPage.ts | 16 + playwright-tests/pages/ContactDetailsPage.ts | 9 + .../pages/CoverageStatementPage.ts | 11 +- playwright-tests/pages/DuplicateCheckPage.ts | 7 + playwright-tests/pages/EndorsementsPage.ts | 10 + playwright-tests/pages/EstimatePage.ts | 10 +- .../pages/InsuranceCompanyPage.ts | 11 + .../pages/MoldingQuestionsPage.ts | 10 + .../pages/OrderConfirmationPage.ts | 14 +- playwright-tests/pages/PartQuestionPage.ts | 10 + playwright-tests/pages/PaymentMethodPage.ts | 18 + playwright-tests/pages/PolicyDriverPage.ts | 10 +- .../pages/PolicyInfoSubmittedPage.ts | 8 +- playwright-tests/pages/PolicyVehiclesPage.ts | 27 + .../pages/RecalibrationInfoPage.ts | 6 + playwright-tests/pages/SchedulePage.ts | 8 + playwright-tests/pages/ServiceLocationPage.ts | 9 + playwright-tests/pages/ServicePackagesPage.ts | 53 ++ playwright-tests/pages/ServiceZipPage.ts | 20 + playwright-tests/pages/VehicleDamagePage.ts | 20 + .../pages/VehicleLookupAddressPage.ts | 9 + .../pages/VehicleLookupLicensePage.ts | 10 + playwright-tests/pages/VehiclePartsPage.ts | 10 + .../pages/VehicleSelectionPage.ts | 18 + playwright-tests/pages/VerifyDetailsPage.ts | 9 + playwright-tests/pages/VinLookupPage.ts | 10 + playwright-tests/tests/0000__M.test.ts | 463 ++++-------------- 31 files changed, 487 insertions(+), 374 deletions(-) create mode 100644 playwright-tests/business-logic/types/Step.ts create mode 100644 playwright-tests/pages/ServiceZipPage.ts diff --git a/playwright-tests/business-logic/types/ITestPages.ts b/playwright-tests/business-logic/types/ITestPages.ts index 77b9689e7..d8ba7e5bd 100644 --- a/playwright-tests/business-logic/types/ITestPages.ts +++ b/playwright-tests/business-logic/types/ITestPages.ts @@ -27,6 +27,7 @@ import { CoverageStatementPage } from "../../pages/CoverageStatementPage" import { VerifyDetailsPage } from "../../pages/VerifyDetailsPage" import { EndorsementsPage } from "../../pages/EndorsementsPage" import { PolicyDriverPage } from "../../pages/PolicyDriverPage" +import { ServiceZipPage } from "../../pages/ServiceZipPage" // File containing interface for all Page Object Models export default interface ITestPages { @@ -48,7 +49,8 @@ export default interface ITestPages { recalibrationInfoPage: RecalibrationInfoPage, schedulePage: SchedulePage, serviceLocationPage: ServiceLocationPage, - servicePackagePage: ServicePackagesPage, + servicePackagesPage: ServicePackagesPage, + serviceZipPage: ServiceZipPage, vehicleDamagePage: VehicleDamagePage, vehicleLookupAddressPage: VehicleLookupAddressPage, vehicleLookupLicensePage: VehicleLookupLicensePage, diff --git a/playwright-tests/business-logic/types/Step.ts b/playwright-tests/business-logic/types/Step.ts new file mode 100644 index 000000000..693e34da2 --- /dev/null +++ b/playwright-tests/business-logic/types/Step.ts @@ -0,0 +1,27 @@ +import { test } from '@business-logic/types/Test'; + +export type Context = { + kind: string; + name: string | symbol; + access: { + get?(): unknown; + set?(value: unknown): void; + has?(value: unknown): boolean; + }; + private?: boolean; + static?: boolean; + addInitializer?(initializer: () => void): void; +}; + +export type Method = (...args: any[]) => any; + +export function step(label: string): Method { + return (value: T, context: Context): Method => { + return function (this: any, ...args: any[]): Promise { + // console.log(`Method ${value.name} has been decorated`, context, args, label); + return test.step(label, async () => { + return value.call(this, ...args); + }); + }; + }; +} \ No newline at end of file diff --git a/playwright-tests/business-logic/types/TestCase.ts b/playwright-tests/business-logic/types/TestCase.ts index 5fe3ae857..ab45a6078 100644 --- a/playwright-tests/business-logic/types/TestCase.ts +++ b/playwright-tests/business-logic/types/TestCase.ts @@ -41,6 +41,7 @@ import { CoverageStatementPage } from "../../pages/CoverageStatementPage"; import { VerifyDetailsPage } from "../../pages/VerifyDetailsPage"; import { EndorsementsPage } from "../../pages/EndorsementsPage"; import { PolicyDriverPage } from "../../pages/PolicyDriverPage"; +import { ServiceZipPage } from "../../pages/ServiceZipPage"; // File for Test Case Class @@ -164,7 +165,8 @@ export default class TestCase extends DisposableBase implements ITestCase { recalibrationInfoPage: new RecalibrationInfoPage(page), schedulePage: new SchedulePage(page), serviceLocationPage: new ServiceLocationPage(page), - servicePackagePage: new ServicePackagesPage(page), + servicePackagesPage: new ServicePackagesPage(page), + serviceZipPage: new ServiceZipPage(page), vehicleDamagePage: new VehicleDamagePage(page), vehicleLookupAddressPage: new VehicleLookupAddressPage(page), vehicleLookupLicensePage: new VehicleLookupLicensePage(page), diff --git a/playwright-tests/pages/CCPolicyInfoPage.ts b/playwright-tests/pages/CCPolicyInfoPage.ts index 42ee3632b..7151e3c06 100644 --- a/playwright-tests/pages/CCPolicyInfoPage.ts +++ b/playwright-tests/pages/CCPolicyInfoPage.ts @@ -2,6 +2,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { IClaimDetails, ICustomerDetails } from '@business-logic/types/CustomerDetails'; import { InsuranceBasePage } from './InsuranceBasePage'; import { STATE_ABBREVIATIONS } from '@business-logic/constants/StateAbbreviation'; +import { ITestData } from '@business-logic/types/ITestData'; +import { step } from '@business-logic/types/Step'; export class CCPolicyInfoPage extends InsuranceBasePage { readonly policyNumber: Locator; @@ -70,4 +72,12 @@ export class CCPolicyInfoPage extends InsuranceBasePage { throw error; } } + + @step("CCPolicyInfoPage >> Fill out claim information") + async handleCCPolicyInfoPage(testData: Partial) { + const { customerDetails, claimDetails } = testData; + + await this.populatePage(customerDetails!, claimDetails!, await this.hasCityInfo()); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/CapabilityQuestionsPage.ts b/playwright-tests/pages/CapabilityQuestionsPage.ts index e2b6260a3..092a424f3 100644 --- a/playwright-tests/pages/CapabilityQuestionsPage.ts +++ b/playwright-tests/pages/CapabilityQuestionsPage.ts @@ -1,5 +1,7 @@ import { Page } from "@playwright/test"; import { PartQuestionsPage } from "./PartQuestionPage"; +import { step } from "@business-logic/types/Step"; +import { ITestData } from "@business-logic/types/ITestData"; export default class CapabilityQuestionsPage extends PartQuestionsPage { url = process.env['BASE_URL']! + '/fmg/?fmgPage=capability-questions'; @@ -7,4 +9,18 @@ export default class CapabilityQuestionsPage extends PartQuestionsPage { constructor(page: Page) { super(page); } + + @step("CapabilityQuestionsPage >> Select Capability Questions: ") + async handleCapabilityQuestionsPage(testCase: Partial) { + const { capabilityQuestions } = testCase; + + // Validate the capability questions are on the page + await this.validatePartQuestions(capabilityQuestions!); + + // Select the capability question responses + await this.selectPartQuestionResponses(capabilityQuestions!); + + // Proceed to the next page + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/ContactDetailsPage.ts b/playwright-tests/pages/ContactDetailsPage.ts index 0b10be42a..2641f6436 100644 --- a/playwright-tests/pages/ContactDetailsPage.ts +++ b/playwright-tests/pages/ContactDetailsPage.ts @@ -1,6 +1,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { ICustomerDetails } from '@business-logic/types/CustomerDetails'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class ContactDetailsPage extends BasePage { readonly page: Page; @@ -39,4 +41,11 @@ export class ContactDetailsPage extends BasePage { await this.notesTextBox.fill(notes); } } + + @step("ContactDetailsPage >> Enter contact details: ") + async handleContactDetailsPage(testData: Partial) { + const { customerDetails } = testData; + await this.enterContactDetails(customerDetails!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/CoverageStatementPage.ts b/playwright-tests/pages/CoverageStatementPage.ts index a140604f7..7085c6794 100644 --- a/playwright-tests/pages/CoverageStatementPage.ts +++ b/playwright-tests/pages/CoverageStatementPage.ts @@ -1,6 +1,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { InsuranceBasePage } from './InsuranceBasePage'; import { IClaimDetails } from '@business-logic/types/CustomerDetails'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class CoverageStatementPage extends InsuranceBasePage { readonly page: Page; @@ -54,5 +56,12 @@ export class CoverageStatementPage extends InsuranceBasePage { async validateUnverifiedText(){ await expect(this.verfiyingCoverageText).toBeEnabled(); } - + + @step("CoverageStatementPage >> Next page: ") + async handleCoverageStatementPage(testData: Partial) { + const { claimDetails } = testData; + + await this.validateDeductibleAmount(claimDetails!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/DuplicateCheckPage.ts b/playwright-tests/pages/DuplicateCheckPage.ts index 58d5463dd..e5f44492c 100644 --- a/playwright-tests/pages/DuplicateCheckPage.ts +++ b/playwright-tests/pages/DuplicateCheckPage.ts @@ -1,5 +1,7 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { InsuranceBasePage } from './InsuranceBasePage'; +import { ITestData } from '@business-logic/types/ITestData'; +import { step } from '@business-logic/types/Step'; export class DuplicateCheckPage extends InsuranceBasePage { readonly page: Page; @@ -18,4 +20,9 @@ export class DuplicateCheckPage extends InsuranceBasePage { await this.page.waitForLoadState(); } + @step("DuplicateCheckPage >> Start a new claim: ") + async handleDuplicateCheckPage(testCase: Partial) { + await this.startNewClaim(); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/EndorsementsPage.ts b/playwright-tests/pages/EndorsementsPage.ts index 08d69e4e3..3b63dcac0 100644 --- a/playwright-tests/pages/EndorsementsPage.ts +++ b/playwright-tests/pages/EndorsementsPage.ts @@ -3,6 +3,8 @@ import { BasePage } from './BasePage'; import { IEndorsementDetails } from '@business-logic/types/CustomerDetails'; import { EndorsementType } from '@business-logic/types/Enums'; import { InsuranceBasePage } from './InsuranceBasePage'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class EndorsementsPage extends InsuranceBasePage { readonly page: Page; @@ -52,4 +54,12 @@ export class EndorsementsPage extends InsuranceBasePage { } } } + + @step("EndorsementsPage >> Select endorsements: ") + async handleEndorsementsPage(testData: Partial) { + const { endorsements } = testData; + await this.verifyEndorsements(endorsements!); + await this.selectEndorsements(endorsements!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/EstimatePage.ts b/playwright-tests/pages/EstimatePage.ts index 141fa73a6..2beb507b3 100644 --- a/playwright-tests/pages/EstimatePage.ts +++ b/playwright-tests/pages/EstimatePage.ts @@ -5,6 +5,8 @@ import { IVehicleDetails } from '@business-logic/types/CustomerDetails'; import { VinLookupPage } from './VinLookupPage'; import { VehicleLookupAddressPage } from './VehicleLookupAddressPage'; import { VehicleLookupLicensePage } from './VehicleLookupLicensePage'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class EstimatePage extends BasePage { readonly page: Page; @@ -20,7 +22,7 @@ export class EstimatePage extends BasePage { constructor(page: Page) { super(page); this.page = page; - this.vinLookupButton = page.getByLabel('Provide my VIN', { exact: true }); + this.vinLookupButton = page.locator('label').filter({ hasText: 'Provide my VIN' }).locator('div'); this.zipLookupButton = page.locator('label').filter({ hasText: 'I\'d rather not share my VIN' }).locator('div'); this.addressLookupButton = page.getByLabel('Provide my home address', { exact: true }); this.licenseLookupButton = page.getByLabel('Provide my license plate #', { exact: true }); @@ -68,4 +70,10 @@ export class EstimatePage extends BasePage { async selectZipLookup(){ await this.zipLookupButton.click(); } + + @step("EstimatePage >> Select Lookup Type") + async handleEstimatePage(testData: Partial) { + const { vehicleDetails } = testData; + await this.vehicleLookup(vehicleDetails!); + } } diff --git a/playwright-tests/pages/InsuranceCompanyPage.ts b/playwright-tests/pages/InsuranceCompanyPage.ts index 6bc125225..e2d63ec2d 100644 --- a/playwright-tests/pages/InsuranceCompanyPage.ts +++ b/playwright-tests/pages/InsuranceCompanyPage.ts @@ -1,6 +1,9 @@ import { type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IClaimDetails } from '@business-logic/types/CustomerDetails'; +import { step } from '@business-logic/types/Step'; +import TestCase from '@business-logic/types/TestCase'; +import { ITestData } from '@business-logic/types/ITestData'; export class InsuranceCompanyPage extends BasePage { @@ -44,4 +47,12 @@ export class InsuranceCompanyPage extends BasePage { } } } + + @step("InsuranceCompanyPage >> Enter insurance company: ") + async handleInsuranceCompanyPage(testData: Partial) { + const { claimDetails } = testData; + + await this.enterInsuranceCompany(claimDetails!.client!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/MoldingQuestionsPage.ts b/playwright-tests/pages/MoldingQuestionsPage.ts index 3221a35e2..5671900ff 100644 --- a/playwright-tests/pages/MoldingQuestionsPage.ts +++ b/playwright-tests/pages/MoldingQuestionsPage.ts @@ -1,5 +1,7 @@ import { Page } from "@playwright/test"; import { PartQuestionsPage } from "./PartQuestionPage"; +import { step } from "@business-logic/types/Step"; +import { ITestData } from "@business-logic/types/ITestData"; export default class MoldingQuestionsPage extends PartQuestionsPage { url = process.env['BASE_URL']! + '/fmg/?fmgPage=molding-questions'; @@ -7,4 +9,12 @@ export default class MoldingQuestionsPage extends PartQuestionsPage { constructor(page: Page) { super(page); } + + @step("MoldingQuestionsPage >> Select Molding Questions: ") + async handleMoldingQuestionsPage(testCase: Partial) { + const { moldingQuestions } = testCase; + await this.validatePartQuestions(moldingQuestions!); + await this.selectPartQuestionResponses(moldingQuestions!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/OrderConfirmationPage.ts b/playwright-tests/pages/OrderConfirmationPage.ts index c8b5f63f6..9ac31826f 100644 --- a/playwright-tests/pages/OrderConfirmationPage.ts +++ b/playwright-tests/pages/OrderConfirmationPage.ts @@ -1,8 +1,10 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; +import { test } from '@business-logic/types/Test'; import { ICustomerDetails, IVehicleDetails } from '@business-logic/types/CustomerDetails'; import { ServicePackage, PaymentType, PaymentMethod } from '@business-logic/types/Enums'; import { ITestData } from '@business-logic/types/ITestData'; +import { step } from '@business-logic/types/Step'; export class OrderConfirmationPage extends BasePage { readonly page: Page; @@ -125,7 +127,15 @@ export class OrderConfirmationPage extends BasePage { async logOrderNumber() { const sessionStorage = JSON.parse(await this.page.evaluate('sessionStorage.getItem(\'submittedState\')')); - return sessionStorage.workOrderNumber; + return sessionStorage.order.workOrderNumber; } - + + @step("OrderConfirmationPage >> Validate order") + async verifyOrderConfirmationPage(testData: Partial) { + await this.validateOrderConfirmationPage(testData); + const workOrderNumber = await this.logOrderNumber(); + await test.step(`Session Storage Work Order Number: ${workOrderNumber}`, async () => { + console.log(`Session Storage Work Order Number: ${workOrderNumber}`); + }); + } } \ No newline at end of file diff --git a/playwright-tests/pages/PartQuestionPage.ts b/playwright-tests/pages/PartQuestionPage.ts index 02c26490f..831a27dc1 100644 --- a/playwright-tests/pages/PartQuestionPage.ts +++ b/playwright-tests/pages/PartQuestionPage.ts @@ -1,6 +1,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IPartQuestion } from '@business-logic/types/CustomerDetails'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class PartQuestionsPage extends BasePage { readonly page: Page; @@ -56,4 +58,12 @@ export class PartQuestionsPage extends BasePage { } } } + + @step("PartQuestionsPage >> Select Vehicle Part Question Responses") + async handlePartQuestionsPage(testData: Partial) { + const { partQuestions } = testData; + await this.validatePartQuestions(partQuestions!); + await this.selectPartQuestionResponses(partQuestions!); + await this.nextPage(); + } } diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 519cface5..210e9291f 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -6,6 +6,7 @@ import { PaymentPage } from './PaymentPage'; import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; import { ITestData } from '@business-logic/types/ITestData'; +import { step } from '@business-logic/types/Step'; export class PaymentMethodPage extends BasePage { readonly page: Page; @@ -464,4 +465,21 @@ l let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); return updatedAppointmentDate; } + + @step("PaymentMethodPage >> Select Payment Method: ") + async handlePaymentMethodPage(testData: Partial) { + const { servicePackage, isRecalVehicle, paymentDetails } = testData; + + await this.validatePaymentDetailsPage(testData); + + // Verify VAPS wipers on backend for standard and premium packages + if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) { + await this.verifyVAPS(); + } + if (paymentDetails?.paymentType) { + await this.executePayment(paymentDetails!, isRecalVehicle!); + } else { + await this.nextPage(); + } + } } \ No newline at end of file diff --git a/playwright-tests/pages/PolicyDriverPage.ts b/playwright-tests/pages/PolicyDriverPage.ts index f6e56ca5f..1bb140088 100644 --- a/playwright-tests/pages/PolicyDriverPage.ts +++ b/playwright-tests/pages/PolicyDriverPage.ts @@ -1,6 +1,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { InsuranceBasePage } from './InsuranceBasePage'; import { ICustomerDetails } from '@business-logic/types/CustomerDetails'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class PolicyDriverPage extends InsuranceBasePage { readonly page: Page; @@ -47,5 +49,11 @@ export class PolicyDriverPage extends InsuranceBasePage { await this.driverNotListedOption.click(); } } - + + @step("PolicyDriverPage >> Select policy driver: ") + async handlePolicyDriverPage(testData: Partial) { + const { customerDetails } = testData; + await this.selectPolicyDriver(customerDetails!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/PolicyInfoSubmittedPage.ts b/playwright-tests/pages/PolicyInfoSubmittedPage.ts index 6d5c96c5c..0a4bf79be 100644 --- a/playwright-tests/pages/PolicyInfoSubmittedPage.ts +++ b/playwright-tests/pages/PolicyInfoSubmittedPage.ts @@ -1,5 +1,6 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { InsuranceBasePage } from './InsuranceBasePage'; +import { step } from '@business-logic/types/Step'; export class PolicyInfoSubmittedPage extends InsuranceBasePage { readonly page: Page; @@ -20,5 +21,10 @@ export class PolicyInfoSubmittedPage extends InsuranceBasePage { const policyInfoMessage = await this.policyInfoMessage.textContent(); expect(policyInfoMessage).toBe("Your policy and vehicle information has been submitted for coverage verification") } - + + @step("PolicyInfoSubmittedPage >> Verify policy info submitted: ") + async handlePolicyInfoSubmittedPage() { + await this.verifyPolicyInfoSubmitted(); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/PolicyVehiclesPage.ts b/playwright-tests/pages/PolicyVehiclesPage.ts index 90882062d..67776adf6 100644 --- a/playwright-tests/pages/PolicyVehiclesPage.ts +++ b/playwright-tests/pages/PolicyVehiclesPage.ts @@ -1,6 +1,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { IVehicleDetails } from '@business-logic/types/CustomerDetails'; import { InsuranceBasePage } from './InsuranceBasePage'; +import { ITestData } from '@business-logic/types/ITestData'; +import { step } from '@business-logic/types/Step'; export class PolicyVehiclesPage extends InsuranceBasePage { readonly page: Page; @@ -26,4 +28,29 @@ export class PolicyVehiclesPage extends InsuranceBasePage { async selectVehicleNotListed(){ await this.page.getByText('Vehicle not listed').click(); } + + @step("PolicyVehiclesPage >> Select Vehicle: ") + async handlePolicyVehiclesPage(testCase: Partial) { + + const { vehicleDetails, otherVehiclesOnPolicy, isUseVehicleOnPolicy } = testCase; + + // Validate other vehicles on policy + if (otherVehiclesOnPolicy && otherVehiclesOnPolicy.length > 0) { + for (const vehicle of otherVehiclesOnPolicy) { + await this.validateVehicleIsOnPolicy(vehicle); + } + } + + // If vehicle is not on the policy, select new vehicle + if (!(isUseVehicleOnPolicy ?? true)) { + await this.selectVehicleNotListed(); + await this.nextPage(); + await this.selectVehicle(vehicleDetails!); + await this.nextPage(); + } else { + // Otherwise, select the vehicle entered in Safelite.com + await this.selectVehicle(vehicleDetails!); + await this.nextPage(); + } + } } \ No newline at end of file diff --git a/playwright-tests/pages/RecalibrationInfoPage.ts b/playwright-tests/pages/RecalibrationInfoPage.ts index 0af4b0a57..7310d6c16 100644 --- a/playwright-tests/pages/RecalibrationInfoPage.ts +++ b/playwright-tests/pages/RecalibrationInfoPage.ts @@ -1,5 +1,6 @@ import { Locator, Page } from "@playwright/test"; import { InsuranceBasePage } from "./InsuranceBasePage"; +import { step } from "@business-logic/types/Step"; export default class RecalibrationInfoPage extends InsuranceBasePage { url = process.env['BASE_URL']! + '/FixMyGlass/RecalibrationInfo.aspx'; @@ -11,4 +12,9 @@ export default class RecalibrationInfoPage extends InsuranceBasePage { this.continueButton = page.getByRole('button', { name: 'Continue' }); } + + @step("RecalibrationInfoPage >> Click continue button: ") + async handleRecalibrationInfoPage() { + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 0bf2b1105..80ae29920 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -4,6 +4,8 @@ import { IAppointmentDetails, ICustomerDetails } from '@business-logic/types/Cus import { formatDate, formatTime } from '@impl/utils/DateUtils'; import { AppointmentType, ServiceLocation } from '@business-logic/types/Enums'; import { time } from 'console'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class SchedulePage extends BasePage { readonly page: Page; @@ -81,4 +83,10 @@ export class SchedulePage extends BasePage { } return formattedTimeSlot; } + + @step("SchedulePage >> Schedule appointment: ") + async handleSchedulePage(testData: Partial) { + const { customerDetails } = testData; + await this.scheduleFirstAppointment(customerDetails!); + } } \ No newline at end of file diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index f405175e6..a31f38eb5 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -4,6 +4,8 @@ import { IAppointmentDetails } from '@business-logic/types/CustomerDetails'; import { AppointmentType } from '@business-logic/types/Enums'; import { AddressForm } from './forms/AddressForm'; import { faker } from '@faker-js/faker'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class ServiceLocationPage extends BasePage { readonly page: Page; @@ -154,4 +156,11 @@ export class ServiceLocationPage extends BasePage { await expect(this.RecalWarningMessage2).toBeVisible(); } + + @step("ServiceLocationPage >> Select service location: ") + async handleServiceLocationPage(testData: Partial) { + const { appointmentDetails } = testData; + await this.selectLocation(appointmentDetails!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 360b285c3..89f085d3b 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -2,6 +2,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; import { PaymentMethod } from '@business-logic/types/Enums'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class ServicePackagesPage extends BasePage { readonly page: Page; @@ -178,4 +180,55 @@ export class ServicePackagesPage extends BasePage { throw new Error("No glass parts found in the order"); } } + + @step("ServicePackagePage >> Select Payment Method and Service Type: ") + async handleServicePackagePage(testData: Partial) { + const { customerDetails, paymentMethod, servicePackage, promoCode, canNotRecal, dynamicRecal, hasOemEndorsement, vehicleDamage } = testData; + + // Define repair damage types (vs. replacement types) + const repairTypes: VehicleDamage[] = [ + VehicleDamage.WindshieldOneChip, + VehicleDamage.WindshieldTwoChips, + VehicleDamage.WindshieldThreeChips + ]; + + // Determine if we're replacing or repairing + const isReplace = !repairTypes.some(damageType => { + return vehicleDamage!.includes(damageType); + }); + + await this.handleQuotePopup(customerDetails!.email!); + await this.selectPaymentMethod(paymentMethod!); + await this.selectServicePackage(servicePackage!); + + // Enter promo code + if (promoCode) { + await this.enterPromo(promoCode); + } + + // Backend Validations + // Validate backend for can not recal if applicable + if (canNotRecal) { + await this.verifyCanNotRecal(); + } + + // Validate backend for dynamic recal if applicable + if (dynamicRecal) { + await this.verifyDynamicRecal(); + } + + // Validate backend for repair info (including chip verification) + await this.verifyIsRepair(!isReplace, vehicleDamage!); + if (isReplace) { + // Validate backend for parts info + await this.verifyVehicleParts(vehicleDamage!); + } + + // Validate backend for OEM endorsement + if (hasOemEndorsement) { + await this.verifyOEMPart(); + } + + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/ServiceZipPage.ts b/playwright-tests/pages/ServiceZipPage.ts new file mode 100644 index 000000000..d11ffa139 --- /dev/null +++ b/playwright-tests/pages/ServiceZipPage.ts @@ -0,0 +1,20 @@ +import { Page } from "@playwright/test"; +import { LookupPage } from "./LookupPage"; +import { step } from "@business-logic/types/Step"; +import { ITestData } from "@business-logic/types/ITestData"; + +export class ServiceZipPage extends LookupPage { + + url = process.env['BASE_URL']! + '/fmg/?fmgPage=service-zip'; + + constructor(page: Page) { + super(page); + } + + @step("ZipLookupPage >> Lookup by service ZIP: ") + async handleServiceZipPage(testData: Partial) { + const { customerDetails, vehicleDetails, alertFlags } = testData; + await this.enterZip(customerDetails!.address.postalCode!); + await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VehicleDamagePage.ts b/playwright-tests/pages/VehicleDamagePage.ts index 33d778a79..1b48a1d4d 100644 --- a/playwright-tests/pages/VehicleDamagePage.ts +++ b/playwright-tests/pages/VehicleDamagePage.ts @@ -2,6 +2,8 @@ import { type Locator, type Page, expect, test } from '@playwright/test'; import { BasePage } from './BasePage'; import { SideDoorDamage, VehicleDamage, WindshieldDamage } from '@business-logic/types/Enums'; import TestSuccessAlert from '@business-logic/types/TestSuccessAlert'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class VehicleDamagePage extends BasePage { readonly page: Page; @@ -161,4 +163,22 @@ export class VehicleDamagePage extends BasePage { console.log(`Alert encountered: ${alertMessage}`); expect(alertMessage).toContain("Service not availableWe're sorry, but we currently offer only repair service for your vehicle type. Need help with next steps? Call us at800-394-0288.") } + + @step('VehicleDamagePage >> Select Damage') + async handleVehicleDamagePage(testData: Partial): Promise { + const {vehicleDamage} = testData; + const {isRepairReplace, isRepairOnly} = testData.alertFlags || {}; + + await this.selectDamage(vehicleDamage!); + // Handle alert conditions for vehicle damage + if (isRepairReplace) { + await this.checkForBothRepairReplaceAlertMessage(); + throw new TestSuccessAlert('Both assertions are met successfully.'); + } + if (isRepairOnly) { + await this.checkForRepairOnlyAlertMessage(); + throw new TestSuccessAlert('Both assertions are met successfully.'); + } + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/VehicleLookupAddressPage.ts b/playwright-tests/pages/VehicleLookupAddressPage.ts index 23ba8521a..ab0ad86f8 100644 --- a/playwright-tests/pages/VehicleLookupAddressPage.ts +++ b/playwright-tests/pages/VehicleLookupAddressPage.ts @@ -3,6 +3,8 @@ import { LookupPage } from './LookupPage'; import { AddressForm } from './forms/AddressForm'; import { VehicleSelectionForm } from './forms/VehicleSelectionForm'; import { ICustomerDetails, IVehicleDetails } from '@business-logic/types/CustomerDetails'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class VehicleLookupAddressPage extends LookupPage { readonly addressForm: AddressForm; @@ -25,4 +27,11 @@ export class VehicleLookupAddressPage extends LookupPage { //await this.nextPage(); //await this.vehicleSelectionForm.selectVehicle(vehicleDetails); } + + @step("VehicleLookupAddressPage >> Lookup by address: ") + async handleVehicleLookupAddressPage(testData: Partial) { + const { customerDetails, vehicleDetails, alertFlags } = testData; + await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!); + await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + } } \ No newline at end of file diff --git a/playwright-tests/pages/VehicleLookupLicensePage.ts b/playwright-tests/pages/VehicleLookupLicensePage.ts index 492dfeffe..ef7f51f85 100644 --- a/playwright-tests/pages/VehicleLookupLicensePage.ts +++ b/playwright-tests/pages/VehicleLookupLicensePage.ts @@ -1,6 +1,8 @@ import { type Locator, type Page } from '@playwright/test'; import { LookupPage } from './LookupPage'; import { IVehicleDetails } from '@business-logic/types/CustomerDetails'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class VehicleLookupLicensePage extends LookupPage { readonly licensePlateNumTextBox: Locator; @@ -24,4 +26,12 @@ export class VehicleLookupLicensePage extends LookupPage { async enterPlateDetails(vehicleDetails: IVehicleDetails) { await this.licensePlateNumTextBox.fill(vehicleDetails.licensePlateNumber || ''); } + + @step("VehicleLookupLicensePage >> Lookup by license plate: ") + async handleVehicleLookupLicensePage(testData: Partial) { + const { customerDetails, vehicleDetails, alertFlags } = testData; + await this.enterPlateDetails(vehicleDetails!); + await this.enterZip(customerDetails!.address.postalCode!); + await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + } } \ No newline at end of file diff --git a/playwright-tests/pages/VehiclePartsPage.ts b/playwright-tests/pages/VehiclePartsPage.ts index 1dc6b84f5..aabaa1f4e 100644 --- a/playwright-tests/pages/VehiclePartsPage.ts +++ b/playwright-tests/pages/VehiclePartsPage.ts @@ -1,5 +1,7 @@ import { Page } from "@playwright/test"; import { PartQuestionsPage } from "./PartQuestionPage"; +import { step } from "@business-logic/types/Step"; +import { ITestData } from "@business-logic/types/ITestData"; export default class VehiclePartQuestionsPage extends PartQuestionsPage{ url = process.env['BASE_URL']! + '/fmg/?fmgPage=vehicle-parts'; @@ -7,4 +9,12 @@ export default class VehiclePartQuestionsPage extends PartQuestionsPage{ constructor(page: Page) { super(page); } + + @step("VehiclePartsPage >> Select Vehicle Part Questions: ") + async handleVehiclePartsPage(testCase: Partial) { + const { vehiclePartQuestions } = testCase; + await this.validatePartQuestions(vehiclePartQuestions!); + await this.selectPartQuestionResponses(vehiclePartQuestions!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/VehicleSelectionPage.ts b/playwright-tests/pages/VehicleSelectionPage.ts index b2d370ef8..049f9fb8d 100644 --- a/playwright-tests/pages/VehicleSelectionPage.ts +++ b/playwright-tests/pages/VehicleSelectionPage.ts @@ -2,6 +2,8 @@ import { type Locator, type Page, expect, test } from '@playwright/test'; import { BasePage } from './BasePage'; import { IVehicleDetails } from '@business-logic/types/CustomerDetails'; import TestSuccessAlert from '@business-logic/types/TestSuccessAlert'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class VehicleSelectionPage extends BasePage { readonly page: Page; @@ -42,4 +44,20 @@ export class VehicleSelectionPage extends BasePage { await console.log(`Alert encountered: ${alertMessage}`); await expect(alertMessage).toContain('Service not available in your areaWe do not offer glass service for your vehicle in your ZIP code. We apologize for the inconvenience.'); } + + @step("VehicleSelectionPage >> Select Vehicle: ") + async handleVehicleSelectionPage(testData: Partial) { + const { vehicleDetails } = testData; + const { isHeavyTruckVehicle, isSplitWindshield } = testData.alertFlags || {}; + + await this.selectVehicle(vehicleDetails!); + + // Handle alert conditions for vehicle selection + if (isHeavyTruckVehicle || isSplitWindshield) { + await this.checkForAlertMessages(); + throw new TestSuccessAlert('Both assertions are met successfully.'); + } + + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/VerifyDetailsPage.ts b/playwright-tests/pages/VerifyDetailsPage.ts index b74eb251e..212ed7968 100644 --- a/playwright-tests/pages/VerifyDetailsPage.ts +++ b/playwright-tests/pages/VerifyDetailsPage.ts @@ -1,6 +1,8 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { IClaimDetails, ICustomerDetails } from '@business-logic/types/CustomerDetails'; import { InsuranceBasePage } from './InsuranceBasePage'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class VerifyDetailsPage extends InsuranceBasePage { readonly page: Page; @@ -74,4 +76,11 @@ export class VerifyDetailsPage extends InsuranceBasePage { // Return the original string if format is unknown return date; } + + @step("VerifyDetailsPage >> Verify policy details: ") + async handleVerifyDetailsPage(testData: Partial) { + const { customerDetails, claimDetails } = testData; + await this.verifyPolicyDetails(customerDetails!, claimDetails!); + await this.nextPage(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/VinLookupPage.ts b/playwright-tests/pages/VinLookupPage.ts index 4ba2de260..1e96c14c5 100644 --- a/playwright-tests/pages/VinLookupPage.ts +++ b/playwright-tests/pages/VinLookupPage.ts @@ -1,5 +1,7 @@ import { type Locator, type Page } from '@playwright/test'; import { LookupPage } from './LookupPage'; +import { step } from '@business-logic/types/Step'; +import { ITestData } from '@business-logic/types/ITestData'; export class VinLookupPage extends LookupPage { readonly vinLookupTextBox: Locator; @@ -20,4 +22,12 @@ export class VinLookupPage extends LookupPage { async enterVin(vin: string) { await this.vinLookupTextBox.fill(vin); } + + @step("VinLookupPage >> Lookup by VIN: ") + async handleVehicleLookupVinPage(testData: Partial) { + const { customerDetails, vehicleDetails, alertFlags } = testData; + await this.enterVin(vehicleDetails!.vin!); + await this.enterZip(customerDetails!.address.postalCode!); + await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + } } \ No newline at end of file diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 6f84c9431..3c4349c87 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -164,27 +164,6 @@ async function runWorkflow(page: Page, testCase: TestCase) { dynamicRecal, hasOemEndorsement, promoCode } = testCase.testData; - // Destructure alert flag data - const { - isHeavyTruckVehicle, isRepairReplace, isSplitWindshield, isRepairOnly, - isUnserviceableZip, isInvalidZip, isVinNotFound - } = testCase.testData.alertFlags || {}; - - // Define repair damage types (vs. replacement types) - const repairTypes: VehicleDamage[] = [ - VehicleDamage.WindshieldOneChip, - VehicleDamage.WindshieldTwoChips, - VehicleDamage.WindshieldThreeChips - ]; - - // Determine if we're replacing or repairing - const isReplace = !repairTypes.some(damageType => { - return vehicleDamage!.includes(damageType); - }); - - // Check if the insurance policy has endorsements - const hasEndorsements = endorsements && endorsements.length > 0; - // Check if the vehicle damage includes a windshield crack const hasWindshieldCrack = vehicleDamage!.some(damage => damage === VehicleDamage.WindshieldCrack @@ -211,414 +190,166 @@ async function runWorkflow(page: Page, testCase: TestCase) { console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`); } + // Handle vehicle selection page + let vehicleSelectionPage = testCase.pages.vehicleSelectionPage; + await vehicleSelectionPage.handleVehicleSelectionPage(testCase.testData); - await test.step('VehicleSelectionPage >> Select Vehicle', async () => { - let vehicleSelectionPage = testCase.pages.vehicleSelectionPage; - await vehicleSelectionPage.selectVehicle(vehicleDetails!); - - // Handle alert conditions for vehicle selection - if (isHeavyTruckVehicle || isSplitWindshield) { - await vehicleSelectionPage.checkForAlertMessages(); - throw new TestSuccessAlert('Both assertions are met successfully.'); - } - - await vehicleSelectionPage.nextPage(); - }); - - await test.step('VehicleDamagePage >> Select Damage', async () => { - let vehicleDamagePage = testCase.pages.vehicleDamagePage; - await vehicleDamagePage.selectDamage(vehicleDamage!); - - // Handle alert conditions for vehicle damage - if (isRepairReplace) { - await vehicleDamagePage.checkForBothRepairReplaceAlertMessage(); - throw new TestSuccessAlert('Both assertions are met successfully.'); - } - if (isRepairOnly) { - await vehicleDamagePage.checkForRepairOnlyAlertMessage(); - throw new TestSuccessAlert('Both assertions are met successfully.'); - } - - await vehicleDamagePage.nextPage(); - }); + // Handle vehicle damage page + let vehicleDamagePage = testCase.pages.vehicleDamagePage; + await vehicleDamagePage.handleVehicleDamagePage(testCase.testData); // If the vehicle has a windshield crack as part of its damage, go to estimate page and select lookup type if (hasWindshieldCrack && !skipEstimatePage) { - await test.step('EstimatePage >> Select Lookup Type', async () => { - let estimatePage = testCase.pages.estimatePage; - await estimatePage.vehicleLookup(vehicleDetails!); - }); + let estimatePage = testCase.pages.estimatePage; + await estimatePage.handleEstimatePage(testCase.testData); // Handle different vehicle lookup methods switch (vehicleDetails!.vehicleLookupType!) { case VehicleLookupType.Address: - await test.step('VehicleLookupAddressPage >> Lookup by address: ' + customerDetails!.address.street, async () => { - let vehicleLookupAddressPage = testCase.pages.vehicleLookupAddressPage; - await vehicleLookupAddressPage.lookupVehicleByAddress(customerDetails!, vehicleDetails!); - await vehicleLookupAddressPage.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); - }); + let vehicleLookupAddressPage = testCase.pages.vehicleLookupAddressPage; + await vehicleLookupAddressPage.handleVehicleLookupAddressPage(testCase.testData); break; case VehicleLookupType.LicensePlateNumber: - await test.step('VehicleLookupLicensePage >> Lookup by license plate: ' + vehicleDetails!.licensePlateNumber, async () => { - let vehicleLookupLicensePage = testCase.pages.vehicleLookupLicensePage; - await vehicleLookupLicensePage.enterPlateDetails(vehicleDetails!); - await vehicleLookupLicensePage.enterZip(customerDetails!.address.postalCode!); - await vehicleLookupLicensePage.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); - }); + let vehicleLookupLicensePage = testCase.pages.vehicleLookupLicensePage; + await vehicleLookupLicensePage.handleVehicleLookupLicensePage(testCase.testData); break; case VehicleLookupType.Vin: - await test.step('VinLookupPage >> Lookup by VIN: ' + vehicleDetails!.vin!, async () => { - let vinLookupPage = testCase.pages.vinLookupPage; - await vinLookupPage.enterVin(vehicleDetails!.vin!); - await vinLookupPage.enterZip(customerDetails!.address.postalCode!); - await vinLookupPage.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); - }); + let vinLookupPage = testCase.pages.vinLookupPage; + await vinLookupPage.handleVehicleLookupVinPage(testCase.testData); break; case VehicleLookupType.Zip: - await test.step('ZipLookupPage >> Lookup by service ZIP: ' + customerDetails!.address.postalCode!, async () => { - let zipLookupPage = testCase.pages.zipLookupPage; - let vinLookupPage = testCase.pages.vinLookupPage; - await vinLookupPage.enterZip(customerDetails!.address.postalCode!); - await zipLookupPage.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); - }); + let serviceZipPage = testCase.pages.serviceZipPage; + await serviceZipPage.handleServiceZipPage(testCase.testData); + break; } } else { - // Otherwise just use zip lookup - await test.step('ZipLookupPage >> Lookup by service ZIP: ' + customerDetails!.address.postalCode!, async () => { - let zipLookupPage = testCase.pages.zipLookupPage; - let vinLookupPage = testCase.pages.vinLookupPage; - await vinLookupPage.enterZip(customerDetails!.address.postalCode!); - await zipLookupPage.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); - }); + // Otherwise just use zip lookup for service zip page + let serviceZipPage = testCase.pages.serviceZipPage; + await serviceZipPage.handleServiceZipPage(testCase.testData); } // Handle part questions if applicable if (partQuestions && partQuestions.length > 0) { - await test.step('PartQuestionsPage >> Select Vehicle Part Question Responses', async () => { - let partQuestionsPage = testCase.pages.partQuestionsPage; - await partQuestionsPage.validatePartQuestions(partQuestions); - await partQuestionsPage.selectPartQuestionResponses(partQuestions); - await partQuestionsPage.nextPage(); - }); + let partQuestionsPage = testCase.pages.partQuestionsPage; + await partQuestionsPage.handlePartQuestionsPage(testCase.testData); } // Handle molding questions if applicable if (moldingQuestions && moldingQuestions.length > 0) { - await test.step('MoldingQuestionsPage >> Select Molding Question Responses', async () => { - let moldingQuestionsPage = testCase.pages.moldingQuestionsPage; - await moldingQuestionsPage.validatePartQuestions(moldingQuestions); - await moldingQuestionsPage.selectPartQuestionResponses(moldingQuestions); - await moldingQuestionsPage.nextPage(); - }); + let moldingQuestionsPage = testCase.pages.moldingQuestionsPage; + await moldingQuestionsPage.handleMoldingQuestionsPage(testCase.testData); } // Handle vehicle part questions if applicable if (vehiclePartQuestions && vehiclePartQuestions.length > 0) { - await test.step('VehiclePartsPage >> Select Vehicle Part Responses', async () => { - let vehiclePartsPage = testCase.pages.vehiclePartsPage; - await vehiclePartsPage.validatePartQuestions(vehiclePartQuestions); - await vehiclePartsPage.selectPartQuestionResponses(vehiclePartQuestions); - await vehiclePartsPage.nextPage(); - }); + let vehiclePartsPage = testCase.pages.vehiclePartsPage; + await vehiclePartsPage.handleVehiclePartsPage(testCase.testData); } // Handle capability questions if applicable if (capabilityQuestions && capabilityQuestions.length > 0) { - await test.step('CapabilityQuestionsPage >> Select Capability Question Responses', async () => { - let capabilityQuestionsPage = testCase.pages.capabilityQuestionsPage; - await capabilityQuestionsPage.validatePartQuestions(capabilityQuestions); - await capabilityQuestionsPage.selectPartQuestionResponses(capabilityQuestions); - await capabilityQuestionsPage.nextPage(); - }); + let capabilityQuestionsPage = testCase.pages.capabilityQuestionsPage; + await capabilityQuestionsPage.handleCapabilityQuestionsPage(testCase.testData); } // Select service package and payment method - await test.step('ServicePackagePage >> Select Payment Method and Service Type', async() => { - let servicePackagePage = testCase.pages.servicePackagePage; - await servicePackagePage.handleQuotePopup(customerDetails!.email!); - await servicePackagePage.selectPaymentMethod(paymentMethod!); - await servicePackagePage.selectServicePackage(servicePackage!); - // Enter promo code - if (promoCode) { - await servicePackagePage.enterPromo(promoCode); - } - - // Backend Validations - // Validate backend for can not recal if applicable - if (canNotRecal) { - await servicePackagePage.verifyCanNotRecal(); - } - // Validate backend for dynamic recal if applicable - if (dynamicRecal) { - await servicePackagePage.verifyDynamicRecal(); - } - // Validate backend for repair info (including chip verification) - await servicePackagePage.verifyIsRepair(!isReplace, vehicleDamage!); - if (isReplace) { - // Validate backend for parts info - await servicePackagePage.verifyVehicleParts(vehicleDamage!); - } - // Validate backend for OEM endorsement - if (hasOemEndorsement) { - await servicePackagePage.verifyOEMPart(); - } - - await servicePackagePage.nextPage(); - }); + let servicePackagesPage = testCase.pages.servicePackagesPage; + await servicePackagesPage.handleServicePackagePage(testCase.testData); //============================= INSURANCE FLOW =============================` // Insurance flow - if user selected Insurance as Payment Method if (paymentMethod == PaymentMethod.Insurance) { - await test.step('InsuranceCoveragePage >> Select your insurance', async() => { - let insuranceCompanyPage = testCase.pages.insuranceCompanyPage; - await insuranceCompanyPage.enterInsuranceCompany(claimDetails!.client!); - await insuranceCompanyPage.nextPage(); - }); - - await test.step('CCPolicyInfoPage >> Fill out claim information', async() => { - let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage; - await ccPolicyInfoPage.populatePage(customerDetails!, claimDetails!, await ccPolicyInfoPage.hasCityInfo()); - await ccPolicyInfoPage.nextPage(); - }); - - // Handle duplicate claim case if applicable - if (isDuplicateClaim) { - await test.step('DuplicateCheckPage >> Start New Claim', async () => { - let duplicateCheckPage = testCase.pages.duplicateCheckPage; - await duplicateCheckPage.startNewClaim(); - await duplicateCheckPage.nextPage(); - }); - } - - // Handle policy found vs. not found flows - if (isPolicyFound) { - await test.step('PolicyVehiclesPage >> Select vehicle', async () => { - let policyVehiclesPage = testCase.pages.policyVehiclesPage; - let vehicleSelectionPage = testCase.pages.vehicleSelectionPage; - - // Validate other vehicles on policy - if (otherVehiclesOnPolicy && otherVehiclesOnPolicy.length > 0) { - for (const vehicle of otherVehiclesOnPolicy) { - await policyVehiclesPage.validateVehicleIsOnPolicy(vehicle); - } - } - - // If vehicle is not on the policy, select new vehicle - if (!(isUseVehicleOnPolicy ?? true)) { - await policyVehiclesPage.selectVehicleNotListed(); - await policyVehiclesPage.nextPage(); - await vehicleSelectionPage.selectVehicle(vehicleDetails!); - await policyVehiclesPage.nextPage(); - } else { - // Otherwise, select the vehicle entered in Safelite.com - await policyVehiclesPage.selectVehicle(vehicleDetails!); - await policyVehiclesPage.nextPage(); - } - }); - - // Handle policy driver selection if applicable - if (isPolicyDriver) { - await test.step('PolicyDriverPage >> Confirm Driver at time of damage', async () => { - let policyDriverPage = testCase.pages.policyDriverPage; - await policyDriverPage.selectPolicyDriver(customerDetails!); - await policyDriverPage.nextPage(); - }); - } - - // Handle endorsements if applicable - if (hasEndorsements) { - await test.step('EndorsementsPage >> Select Endorsements', async () => { - let endorsementsPage = testCase.pages.endorsementsPage; - await endorsementsPage.verifyEndorsements(endorsements); - await endorsementsPage.selectEndorsements(endorsements); - await endorsementsPage.nextPage(); - }); - } - } else { - // Policy not found flow - await test.step('VerifyDetailsPage >> Verify Details', async () => { - let verifyDetailsPage = testCase.pages.verifyDetailsPage; - await verifyDetailsPage.verifyPolicyDetails(customerDetails!, claimDetails!); - await verifyDetailsPage.nextPage(); - }); - } - - // Continue with the insurance flow after policy information - await test.step('PolicyInfoSubmittedPage >> Continue With Safelite Autoglass', async () => { - let policyInfoSubmittedPage = testCase.pages.policyInfoSubmittedPage; - await policyInfoSubmittedPage.verifyPolicyInfoSubmitted(); - await policyInfoSubmittedPage.nextPage(); - }); - - // Handle recalibration notification if applicable - if (isRecalNotification) { - await test.step('RecallibrationInfoPage >> Continue With Recalibration Information', async () => { - let recalibrationInfoPage = testCase.pages.recalibrationInfoPage; - await recalibrationInfoPage.nextPage(); - }); - } - - // Continue to coverage statement - await test.step('CoverageStatementPage >> Next page', async () => { - let coverageStatementPage = testCase.pages.coverageStatementPage; - await coverageStatementPage.validateDeductibleAmount(claimDetails!); - await coverageStatementPage.nextPage(); - }); + await handleInsuranceFlow(testCase); } //============================= SERVICE SCHEDULING ============================= // Select service location - await test.step('ServiceLocationPage >> Select service location', async () => { - let serviceLocationPage = testCase.pages.serviceLocationPage; - await serviceLocationPage.selectLocation(appointmentDetails!); - await serviceLocationPage.nextPage(); - }); + let serviceLocationPage = testCase.pages.serviceLocationPage; + await serviceLocationPage.handleServiceLocationPage(testCase.testData); // Schedule appointment - await test.step('SchedulePage >> Select day and time', async () => { - let schedulePage = testCase.pages.schedulePage; - await schedulePage.scheduleFirstAppointment(customerDetails!); - }); + let schedulePage = testCase.pages.schedulePage; + await schedulePage.handleSchedulePage(testCase.testData); // Enter contact details - await test.step('ContactDetailsPage >> Enter contact details', async () => { - let contactDetailsPage = testCase.pages.contactDetailsPage; - await contactDetailsPage.enterContactDetails(customerDetails!); - await contactDetailsPage.nextPage(); - }); + let contactDetailsPage = testCase.pages.contactDetailsPage; + await contactDetailsPage.handleContactDetailsPage(testCase.testData); //============================= PAYMENT PROCESSING ============================= // Handle payment - await test.step('PaymentMethodPage >> Execute Payment', async () => { - let paymentMethodPage = testCase.pages.paymentMethodPage; - await paymentMethodPage.validatePaymentDetailsPage(testCase.testData); - // Verify VAPS wipers on backend for standard and premium packages - if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) { - await paymentMethodPage.verifyVAPS(); - } - if (paymentDetails?.paymentType) { - await paymentMethodPage.executePayment(paymentDetails!, isRecalVehicle!); - } else { - await paymentMethodPage.nextPage(); - } - }); + let paymentMethodPage = testCase.pages.paymentMethodPage; + await paymentMethodPage.handlePaymentMethodPage(testCase.testData); // If user selected Pay with Insurance as Payment Method, Enter Insurance Flow if (paymentDetails?.paymentType === PaymentType.PayWithInsurance) { - await test.step('InsuranceCoveragePage >> Select your insurance', async() => { - let insuranceCompanyPage = testCase.pages.insuranceCompanyPage; - await insuranceCompanyPage.enterInsuranceCompany(claimDetails!.client!); - await insuranceCompanyPage.nextPage(); - }); - - await test.step('CCPolicyInfoPage >> Fill out claim information', async() => { - const ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage; - await ccPolicyInfoPage.populatePage(customerDetails!, claimDetails!, await ccPolicyInfoPage.hasCityInfo()); - await ccPolicyInfoPage.nextPage(); - }); - - // Handle duplicate claim case if applicable - if (isDuplicateClaim) { - await test.step('DuplicateCheckPage >> Start New Claim', async () => { - const duplicateCheckPage = testCase.pages.duplicateCheckPage; - await duplicateCheckPage.startNewClaim(); - await duplicateCheckPage.nextPage(); - }); - } - - // Handle policy found vs. not found flows - if (isPolicyFound) { - await test.step('PolicyVehiclesPage >> Select vehicle', async () => { - const policyVehiclesPage = testCase.pages.policyVehiclesPage; - const vehicleSelectionPage = testCase.pages.vehicleSelectionPage; - - // Validate other vehicles on policy - if (otherVehiclesOnPolicy && otherVehiclesOnPolicy.length > 0) { - for (const vehicle of otherVehiclesOnPolicy) { - await policyVehiclesPage.validateVehicleIsOnPolicy(vehicle); - } - } - - // If vehicle is not on the policy, select new vehicle - if (!(isUseVehicleOnPolicy ?? true)) { - await policyVehiclesPage.selectVehicleNotListed(); - await policyVehiclesPage.nextPage(); - await vehicleSelectionPage.selectVehicle(vehicleDetails!); - await policyVehiclesPage.nextPage(); - } else { - // Otherwise, select the vehicle entered in Safelite.com - await policyVehiclesPage.selectVehicle(vehicleDetails!); - await policyVehiclesPage.nextPage(); - } - }); - - // Handle policy driver selection if applicable - if (isPolicyDriver) { - await test.step('PolicyDriverPage >> Confirm Driver at time of damage', async () => { - const policyDriverPage = testCase.pages.policyDriverPage; - await policyDriverPage.selectPolicyDriver(customerDetails!); - await policyDriverPage.nextPage(); - }); - } - - // Handle endorsements if applicable - if (hasEndorsements) { - await test.step('EndorsementsPage >> Select Endorsements', async () => { - const endorsementsPage = testCase.pages.endorsementsPage; - await endorsementsPage.verifyEndorsements(endorsements); - await endorsementsPage.selectEndorsements(endorsements); - await endorsementsPage.nextPage(); - }); - } - } else { - // Policy not found flow - await test.step('VerifyDetailsPage >> Verify Details', async () => { - const verifyDetailsPage = testCase.pages.verifyDetailsPage; - await verifyDetailsPage.verifyPolicyDetails(customerDetails!, claimDetails!); - await verifyDetailsPage.nextPage(); - }); - } - - // Continue with the insurance flow after policy information - await test.step('PolicyInfoSubmittedPage >> Continue With Safelite Autoglass', async () => { - const policyInfoSubmittedPage = testCase.pages.policyInfoSubmittedPage; - await policyInfoSubmittedPage.verifyPolicyInfoSubmitted(); - await policyInfoSubmittedPage.nextPage(); - }); - - // Handle recalibration notification if applicable - if (isRecalNotification) { - await test.step('RecallibrationInfoPage >> Continue With Recalibration Information', async () => { - const recalibrationInfoPage = testCase.pages.recalibrationInfoPage; - await recalibrationInfoPage.nextPage(); - }); - } - - // Continue to coverage statement - await test.step('CoverageStatementPage >> Next page', async () => { - const coverageStatementPage = testCase.pages.coverageStatementPage; - await coverageStatementPage.validateDeductibleAmount(claimDetails!); - await coverageStatementPage.nextPage(); - }); + await handleInsuranceFlow(testCase); } //============================= ORDER CONFIRMATION ============================= // Validate order confirmation - await test.step('OrderConfirmationPage >> Validate order', async () => { - let orderConfirmationPage = testCase.pages.orderConfirmationPage; - await orderConfirmationPage.validateOrderConfirmationPage(testCase.testData); - }); + let orderConfirmationPage = testCase.pages.orderConfirmationPage; + await orderConfirmationPage.verifyOrderConfirmationPage(testCase.testData); +} - // Get the order number and wrap it in a test step - const workOrderNumber = await testCase.pages.orderConfirmationPage.logOrderNumber(); - await test.step(`Session Storage Work Order Number: ${workOrderNumber}`, async () => { - console.log(`Session Storage Work Order Number: ${workOrderNumber}`); - }); +export async function handleInsuranceFlow(testCase: TestCase) { + const { isPolicyFound, isPolicyDriver, endorsements, isRecalNotification } = testCase.testData; + + // Check if the insurance policy has endorsements + const hasEndorsements = endorsements && endorsements.length > 0; + + // Handle insurance company page + let insuranceCompanyPage = testCase.pages.insuranceCompanyPage; + await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData); + + // Handle ccPolicyInfoPage + let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage; + await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData); + + let duplicateCheckPage = testCase.pages.duplicateCheckPage; + if (duplicateCheckPage.page.url().includes('dup')) { + await duplicateCheckPage.handleDuplicateCheckPage(testCase.testData); + } + + if (isPolicyFound) { + let policyVehiclesPage = testCase.pages.policyVehiclesPage; + await policyVehiclesPage.handlePolicyVehiclesPage(testCase.testData); + + // Handle policy driver selection if applicable + if (isPolicyDriver) { + let policyDriverPage = testCase.pages.policyDriverPage; + await policyDriverPage.handlePolicyDriverPage(testCase.testData); + } + + // Handle endorsements if applicable + if (hasEndorsements) { + let endorsementsPage = testCase.pages.endorsementsPage; + await endorsementsPage.handleEndorsementsPage(testCase.testData); + } + }else { + + //Handle verify details page + let verifyDetailsPage = testCase.pages.verifyDetailsPage; + await verifyDetailsPage.handleVerifyDetailsPage(testCase.testData); + } + + // Handle Policy info submitted page + let policyInfoSubmittedPage = testCase.pages.policyInfoSubmittedPage; + await policyInfoSubmittedPage.handlePolicyInfoSubmittedPage(); + + if(isRecalNotification) + { + let recalibrationInfoPage = testCase.pages.recalibrationInfoPage; + await recalibrationInfoPage.handleRecalibrationInfoPage(); + } + + //handle coveraage statement page + let coverageStatementPage = testCase.pages.coverageStatementPage; + await coverageStatementPage.handleCoverageStatementPage(testCase.testData); } \ No newline at end of file From 87b7b8158bf082eac9a0f306cd897d245213e67f Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 24 Apr 2025 11:21:56 -0400 Subject: [PATCH 20/50] changed duplicate check url expect --- playwright-tests/tests/0000__M.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 3c4349c87..cb2ac2a82 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -313,7 +313,7 @@ export async function handleInsuranceFlow(testCase: TestCase) { await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData); let duplicateCheckPage = testCase.pages.duplicateCheckPage; - if (duplicateCheckPage.page.url().includes('dup')) { + if (duplicateCheckPage.page.url().includes('DuplicateCheck.aspx')) { await duplicateCheckPage.handleDuplicateCheckPage(testCase.testData); } From ae4c26499bc5adaa890aaae3b103863b8ee71629 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 24 Apr 2025 11:26:21 -0400 Subject: [PATCH 21/50] Uses env var to control content site execution Updates the workflow to conditionally execute the content site (home page) based on the SKIP_CONTENT_SITE environment variable. This allows for skipping the content site in specific environments. Also, removes unused test data properties. --- playwright-tests/.env.dev | 4 +++- playwright-tests/tests/0000__M.test.ts | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev index 47ec8336d..0330919bf 100644 --- a/playwright-tests/.env.dev +++ b/playwright-tests/.env.dev @@ -19,4 +19,6 @@ CCIS_API_URL="https://api.test.belronus.io" ADMIN_SERVICE_API_URL="https://issadminapi.dev.sagaws.net/iss-admin/api/v1/" # API authentication (replace with actual value when running tests) -CCIS_API_AUTH="undefined" \ No newline at end of file +CCIS_API_AUTH="undefined" + +SKIP_CONTENT_SITE=false \ No newline at end of file diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index cb2ac2a82..0c1abb757 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -156,12 +156,9 @@ async function runWorkflow(page: Page, testCase: TestCase) { // Destructure test data for easier access const { - servicePackage, paymentMethod, customerDetails, vehicleDetails, vehicleDamage, - appointmentDetails, paymentDetails, claimDetails, partQuestions, enterFunnelWithZip, - capabilityQuestions, vehiclePartQuestions, moldingQuestions, isPolicyFound, - otherVehiclesOnPolicy, isUseVehicleOnPolicy, isDuplicateClaim, isRecalNotification, - alertFlags, endorsements, isPolicyDriver, skipEstimatePage, isRecalVehicle, canNotRecal, - dynamicRecal, hasOemEndorsement, promoCode + paymentMethod, customerDetails, vehicleDetails, vehicleDamage, + paymentDetails, partQuestions, enterFunnelWithZip, capabilityQuestions, + vehiclePartQuestions, moldingQuestions, skipEstimatePage } = testCase.testData; // Check if the vehicle damage includes a windshield crack @@ -172,7 +169,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { //============================= TEST WORKFLOW STEPS ============================= // Execute home page for qa and dev environments (skip for sys) - if (process.env.NODE_ENV !== 'sys') { + if (process.env.SKIP_CONTENT_SITE == "false") { await test.step('HomePage >> Lets Get Started', async () => { let homePage = testCase.pages.homePage; console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`); From daf89bdb32c95e2b507a6a5d491e558e83bdd788 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 24 Apr 2025 12:41:00 -0400 Subject: [PATCH 22/50] CASH-353 | Fix missing 'const' error --- src/layouts/vin-lookup/vin-lookup.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index 1b57ee068..14a87bf45 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -341,7 +341,7 @@ export default { false ); if (this.isHeavyTruck) { - closestShops = await this.findClosestApplicableShops( + const closestShops = await this.findClosestApplicableShops( this.serviceZipCode, this.carId ); From c7ca767a25dc9e560b2dc5c772345cba354e4316 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 24 Apr 2025 13:32:48 -0400 Subject: [PATCH 23/50] CASH-188 | Debug info added. --- .../salesforce-webchat/salesforce-helper-qa.js | 2 ++ .../salesforce-webchat/salesforce-webchat.vue | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/digital-components/salesforce-webchat/salesforce-helper-qa.js b/src/digital-components/salesforce-webchat/salesforce-helper-qa.js index ef08d42de..46ae49359 100644 --- a/src/digital-components/salesforce-webchat/salesforce-helper-qa.js +++ b/src/digital-components/salesforce-webchat/salesforce-helper-qa.js @@ -52,6 +52,7 @@ export function initializeSalesforceWebchatForQa() { }; if (!window.embedded_svc) { + console.log("no embedded svc found"); var s = document.createElement("script"); s.setAttribute( "src", @@ -62,6 +63,7 @@ export function initializeSalesforceWebchatForQa() { }; document.body.appendChild(s); } else { + console.log("embedded svc found"); initESW("https://service.force.com"); } } diff --git a/src/digital-components/salesforce-webchat/salesforce-webchat.vue b/src/digital-components/salesforce-webchat/salesforce-webchat.vue index 75f689d10..95c1348eb 100644 --- a/src/digital-components/salesforce-webchat/salesforce-webchat.vue +++ b/src/digital-components/salesforce-webchat/salesforce-webchat.vue @@ -28,16 +28,23 @@ export default { }, methods: { initializeSalesforceWebchat() { + console.log( + "process.env.VUE_APP_CURRENT_ENVIRONMENT", + process.env.VUE_APP_CURRENT_ENVIRONMENT + ); switch (process.env.VUE_APP_CURRENT_ENVIRONMENT) { case "Prod": + console.log("Prod"); initializeSalesforceWebchatForProd(); break; case "QA": case "SysTest": + console.log("QA"); initializeSalesforceWebchatForQa(); break; case "Dev": case "Localhost": + console.log("Dev"); initializeSalesforceWebchatForDev(); break; } From bf6e80d05db377b162089a0ac69eb60a17745536 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 24 Apr 2025 14:12:14 -0400 Subject: [PATCH 24/50] Revert "Merge pull request #2413 from Safelite/feature/CASH-488" This reverts commit e79314e8fe66fba819dcec3c0975368caf263b6d, reversing changes made to 7356268f2fd77175529b216f55ff215d6326f48b. --- package-lock.json | 135 +----------------- package.json | 2 - playwright-tests/.env.dev | 4 - .../azure-pipelines-automated-testing.yml | 15 +- .../constants/DefaultTestData.ts | 2 +- playwright-tests/pages/ServiceLocationPage.ts | 25 ++-- playwright-tests/pages/ServicePackagesPage.ts | 3 +- playwright-tests/tests/0000__M.test.ts | 5 +- 8 files changed, 29 insertions(+), 162 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b541b385..00fc7673a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@iframe-resizer/child": "^5.3.3", + "axios": "^0.23.0", "bootstrap": "^5.3.3", "core-js": "^3.38.1", "http-status-codes": "^2.3.0", @@ -37,9 +38,6 @@ "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "^27.0.0", - "axios": "^1.8.4", - "axios-mock-adapter": "^2.1.0", - "axios-retry": "^4.5.0", "dotenv-safe": "^9.1.0", "eslint": "8.57", "eslint-plugin-prettier": "^5.2.1", @@ -5414,82 +5412,12 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", - "dev": true, + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz", + "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios-mock-adapter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", - "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "is-buffer": "^2.0.5" - }, - "peerDependencies": { - "axios": ">= 0.17.0" - } - }, - "node_modules/axios-mock-adapter/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/axios-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", - "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" + "follow-redirects": "^1.14.4" } }, "node_modules/babel-jest": { @@ -8077,22 +8005,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -9315,7 +9227,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -9783,22 +9694,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -10614,19 +10509,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -16365,13 +16247,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", diff --git a/package.json b/package.json index fb65152d5..c63d8b5e8 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,6 @@ "prettier": "^3.3.3", "sass": "^1.77.8", "sass-loader": "^8.0.2", - "sqlite": "^5.1.1", - "sqlite3": "^5.1.7", "typescript": "^4.9.5" }, "eslintConfig": { diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev index 8c064ba62..47ec8336d 100644 --- a/playwright-tests/.env.dev +++ b/playwright-tests/.env.dev @@ -4,13 +4,9 @@ # Environment type NODE_ENV="qa" -SKIP_CONTENT_SITE=false - # Base URLs by environment (uncomment the one you need) # qa BASE_URL="https://www-qa2.safelite.com/" -# qa with skipToInsurance Turned Off -# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true" # sys # BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle" # dev diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index ac332ef44..b4d7da831 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -1,3 +1,11 @@ +schedules: +- cron: 0 9 * * MON-FRI + always: true + displayName: Daily Test Automation Run for FMG-NextGen + branches: + include: + - main + pool: 'AmazonLinuxPool' variables: @@ -42,10 +50,10 @@ stages: container_id=$(docker create \ --ipc=host \ -e CCIS_API_AUTH=$(CCIS_API_AUTH) \ - -e BASE_URL="$(BASE_URL)" \ + -e BASE_URL=$(BASE_URL) \ -e CCIS_API_URL=$(CCIS_API_URL) \ -e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \ - -e SKIP_CONTENT_SITE=$(SKIP_CONTENT_SITE) \ + -e SHARD=$(shardNumber) \ -e CI=true \ -e NODE_ENV=$(NODE_ENV) \ $(dockerImageName):$(imageTag) \ @@ -123,7 +131,6 @@ stages: -e REPORT_NAME="$report_name" \ -e CURRENT_DAY="$current_day" \ -e NUMERIC_DATE="$numeric_date" \ - -e NODE_ENV=$(NODE_ENV) \ $(dockerImageName):$(imageTag) \ bash -c "chmod +x devops/scripts/jira_writeback.sh echo \"Moving Playwright reports out of subfolders...\" @@ -144,7 +151,7 @@ stages: fi echo \"Writing report to Jira card '$jira_card_number'...\" - /app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE}) - Environment: \${NODE_ENV}\" ") + /app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE})\" ") # Start container and stream logs echo "Starting merge" diff --git a/playwright-tests/business-logic/constants/DefaultTestData.ts b/playwright-tests/business-logic/constants/DefaultTestData.ts index 197297b4b..6efb2c1db 100644 --- a/playwright-tests/business-logic/constants/DefaultTestData.ts +++ b/playwright-tests/business-logic/constants/DefaultTestData.ts @@ -43,7 +43,7 @@ export function getCustomerDetails(): ICustomerDetails { street: faker.location.streetAddress(), city: 'Columbus', state: 'Ohio', - postalCode: '21237', + postalCode: '43215', country: 'United States' } }; diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index 609f2d1db..b8c8649b6 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -55,7 +55,7 @@ export class ServiceLocationPage extends BasePage { // For in-shop and drop off this.selectAShopOptions = this.page.locator('[class="shop-question"]'); - this.firstAppointmentButton = this.page.locator('fieldset:has(legend#chooseShop)').locator('label').first(); + this.firstAppointmentButton = this.page.locator('div').filter({ hasText: /Appts/}).first(); this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))'); this.updateZipTextBox = this.page.locator('#serviceZipCode'); this.saveZipButton = this.page.getByText('Save ZIP code', { exact: true }); @@ -71,7 +71,7 @@ export class ServiceLocationPage extends BasePage { this.vehicleProtectedYesButton = this.page.locator('label').filter({ hasText: 'Yes' }).locator('div'); this.vehicleProtectedNoButton = this.page.locator('label').filter({ hasText: 'No' }).locator('div'); this.saveAddressButton = this.page.getByRole('button', { name: 'Continue' }) - this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); + this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); } async selectLocation(appointmentDetails: IAppointmentDetails){ @@ -103,14 +103,9 @@ export class ServiceLocationPage extends BasePage { await this.saveZipButton.click(); } await this.inShopButton.click(); - await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click()); + await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); } else { - await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { - await this.firstAppointmentButton.click(); - if (appointmentDetails) { - appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); - } - }); + await this.firstAppointmentButton.click(); } } @@ -119,6 +114,9 @@ export class ServiceLocationPage extends BasePage { await this.mobileButton.click(); await this.enterServiceAddressButton.click(); await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! }); + if (await this.repeatedClicksModalCloseButton.isVisible()) { + await this.repeatedClicksModalCloseButton.click(); + } if (faker.datatype.boolean()) { await this.vehicleProtectedYesButton.check(); } else { @@ -135,13 +133,8 @@ export class ServiceLocationPage extends BasePage { if (appointmentDetails && appointmentDetails.shopAddress) { await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); } else { - /// await this.clickWithRetry(this.firstAppointmentButton, this.page); - await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { - await this.firstAppointmentButton.click(); - if (appointmentDetails) { - appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); - } - }); + // await this.firstAppointmentButton.click(); + await this.clickWithRetry(this.firstAppointmentButton, this.page); } } diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 65bba63b8..9a3b87ae3 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -41,7 +41,7 @@ export class ServicePackagesPage extends BasePage { 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' }); - this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); + this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); // this.validateURL(this.url); } @@ -76,7 +76,6 @@ export class ServicePackagesPage extends BasePage { if (await this.repeatedClicksModalCloseButton.isVisible()) { await this.repeatedClicksModalCloseButton.click(); } - await this.page.waitForTimeout(3000); await this.closeButton.click(); } else { diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 656e078c2..6bef5a2f3 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -192,9 +192,8 @@ async function runWorkflow(page: Page, testCase: TestCase) { //============================= TEST WORKFLOW STEPS ============================= - // Use Environment Variable to decide whether or not we want to skip content site aka home page - - if (process.env.SKIP_CONTENT_SITE == "false") { + // Execute home page for qa and dev environments (skip for sys) + if (process.env.NODE_ENV !== 'sys') { await test.step('HomePage >> Lets Get Started', async () => { let homePage = testCase.pages.homePage; console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`); From faf892b307e30bbb6046563733b3f83ad22bc5d1 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:15:31 -0400 Subject: [PATCH 25/50] fix for the schedule page changes --- playwright-tests/pages/OrderConfirmationPage.ts | 13 ++++++++++++- playwright-tests/pages/PaymentMethodPage.ts | 11 +---------- playwright-tests/pages/SchedulePage.ts | 6 +++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/playwright-tests/pages/OrderConfirmationPage.ts b/playwright-tests/pages/OrderConfirmationPage.ts index 9ac31826f..af05c132d 100644 --- a/playwright-tests/pages/OrderConfirmationPage.ts +++ b/playwright-tests/pages/OrderConfirmationPage.ts @@ -60,7 +60,8 @@ export class OrderConfirmationPage extends BasePage { // General Validations expect.soft(serviceTextValue).toContain(`${vehicleDetails!.year} ${vehicleDetails!.make} ${vehicleDetails!.model}`); - expect.soft(apptDateValue).toContain(customerDetails!.apptDate); + const formattedExpectedAppointmentDate = await this.getFormattedAppointmentDate(customerDetails!.apptDate!); + expect.soft(apptDateValue).toContain(formattedExpectedAppointmentDate); expect.soft(emailTextValue).toContain(customerDetails!.email); // Service package validations @@ -125,6 +126,16 @@ export class OrderConfirmationPage extends BasePage { } } + async getFormattedAppointmentDate(appointmentDate: string) { + + // Parse the original date + let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00'); + + // Format the new date as a string + let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }); + return updatedAppointmentDate; + } + async logOrderNumber() { const sessionStorage = JSON.parse(await this.page.evaluate('sessionStorage.getItem(\'submittedState\')')); return sessionStorage.order.workOrderNumber; diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 210e9291f..7971cef59 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -448,18 +448,9 @@ l } async getFormattedAppointmentDate(appointmentDate: string) { - const currentYear = new Date().getFullYear(); // Parse the original date - let parsedAppointmentDate = new Date(`${appointmentDate}, ${currentYear}`); - - let currentDate = new Date(); - currentDate.setDate(currentDate.getDate() - 2); - - // If the original date is not in the future, adjust the year - if (parsedAppointmentDate < currentDate) { - parsedAppointmentDate.setFullYear(parsedAppointmentDate.getFullYear() + 1); - } + let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00'); // Format the new date as a string let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 80ae29920..dd81a4f94 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -29,7 +29,7 @@ export class SchedulePage extends BasePage { this.dateText = this.page.locator('label.modal-title'); this.viewMoreDatesLink = this.page.getByText(/View more dates/).first(); this.appointmentDuration = this.page.locator('.duration-text-block'); - this.timeSlots = this.page.locator('fieldset:has(legend[id *= \'chooseTimeSlot\']) label'); + this.timeSlots = this.page.locator('fieldset[aria-labelledby=\'chooseTimeSlot\'] label'); } async scheduleAppointment(appointmentDetails: IAppointmentDetails) { @@ -52,6 +52,7 @@ export class SchedulePage extends BasePage { await this.viewMoreDatesLink.click(); } await this.firstAvailableDate.click(); + customerDetails.apptDate = `${await this.firstAvailableDate.getAttribute("id")}` // const timeSlots = this.timeSlots; const timeSlotCount = await this.timeSlots.count(); @@ -61,9 +62,8 @@ export class SchedulePage extends BasePage { customerDetails.apptTime = await this.getFormattedTimeSlot(timeSlot); }); // appointmentmentDetails.serviceLocation === AppointmentType.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); - customerDetails.apptDate = `${await this.dateText.allInnerTexts()}` customerDetails.apptDuration = (await this.appointmentDuration.innerText()).replace("Duration: ", ""); - await this.modalContinueButton.click(); + await this.nextPage(); } async getFormattedTimeSlot(timeSlot: Locator) { From 16949a78a91a2955fce535cdaffc329309fecf94 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 24 Apr 2025 14:54:40 -0400 Subject: [PATCH 26/50] added CASH-488 changes --- playwright-tests/.env.dev | 4 +++ .../azure-pipelines-automated-testing.yml | 15 +++-------- .../constants/DefaultTestData.ts | 2 +- playwright-tests/pages/ServiceLocationPage.ts | 25 ++++++++++++------- playwright-tests/pages/ServicePackagesPage.ts | 4 +-- playwright-tests/tests/0000__M.test.ts | 4 +-- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev index 47ec8336d..8c064ba62 100644 --- a/playwright-tests/.env.dev +++ b/playwright-tests/.env.dev @@ -4,9 +4,13 @@ # Environment type NODE_ENV="qa" +SKIP_CONTENT_SITE=false + # Base URLs by environment (uncomment the one you need) # qa BASE_URL="https://www-qa2.safelite.com/" +# qa with skipToInsurance Turned Off +# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true" # sys # BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle" # dev diff --git a/playwright-tests/azure-pipelines-automated-testing.yml b/playwright-tests/azure-pipelines-automated-testing.yml index b4d7da831..ac332ef44 100644 --- a/playwright-tests/azure-pipelines-automated-testing.yml +++ b/playwright-tests/azure-pipelines-automated-testing.yml @@ -1,11 +1,3 @@ -schedules: -- cron: 0 9 * * MON-FRI - always: true - displayName: Daily Test Automation Run for FMG-NextGen - branches: - include: - - main - pool: 'AmazonLinuxPool' variables: @@ -50,10 +42,10 @@ stages: container_id=$(docker create \ --ipc=host \ -e CCIS_API_AUTH=$(CCIS_API_AUTH) \ - -e BASE_URL=$(BASE_URL) \ + -e BASE_URL="$(BASE_URL)" \ -e CCIS_API_URL=$(CCIS_API_URL) \ -e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \ - -e SHARD=$(shardNumber) \ + -e SKIP_CONTENT_SITE=$(SKIP_CONTENT_SITE) \ -e CI=true \ -e NODE_ENV=$(NODE_ENV) \ $(dockerImageName):$(imageTag) \ @@ -131,6 +123,7 @@ stages: -e REPORT_NAME="$report_name" \ -e CURRENT_DAY="$current_day" \ -e NUMERIC_DATE="$numeric_date" \ + -e NODE_ENV=$(NODE_ENV) \ $(dockerImageName):$(imageTag) \ bash -c "chmod +x devops/scripts/jira_writeback.sh echo \"Moving Playwright reports out of subfolders...\" @@ -151,7 +144,7 @@ stages: fi echo \"Writing report to Jira card '$jira_card_number'...\" - /app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE})\" ") + /app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE}) - Environment: \${NODE_ENV}\" ") # Start container and stream logs echo "Starting merge" diff --git a/playwright-tests/business-logic/constants/DefaultTestData.ts b/playwright-tests/business-logic/constants/DefaultTestData.ts index 6efb2c1db..197297b4b 100644 --- a/playwright-tests/business-logic/constants/DefaultTestData.ts +++ b/playwright-tests/business-logic/constants/DefaultTestData.ts @@ -43,7 +43,7 @@ export function getCustomerDetails(): ICustomerDetails { street: faker.location.streetAddress(), city: 'Columbus', state: 'Ohio', - postalCode: '43215', + postalCode: '21237', country: 'United States' } }; diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index b8c8649b6..609f2d1db 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -55,7 +55,7 @@ export class ServiceLocationPage extends BasePage { // For in-shop and drop off this.selectAShopOptions = this.page.locator('[class="shop-question"]'); - this.firstAppointmentButton = this.page.locator('div').filter({ hasText: /Appts/}).first(); + this.firstAppointmentButton = this.page.locator('fieldset:has(legend#chooseShop)').locator('label').first(); this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))'); this.updateZipTextBox = this.page.locator('#serviceZipCode'); this.saveZipButton = this.page.getByText('Save ZIP code', { exact: true }); @@ -71,7 +71,7 @@ export class ServiceLocationPage extends BasePage { this.vehicleProtectedYesButton = this.page.locator('label').filter({ hasText: 'Yes' }).locator('div'); this.vehicleProtectedNoButton = this.page.locator('label').filter({ hasText: 'No' }).locator('div'); this.saveAddressButton = this.page.getByRole('button', { name: 'Continue' }) - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); } async selectLocation(appointmentDetails: IAppointmentDetails){ @@ -103,9 +103,14 @@ export class ServiceLocationPage extends BasePage { await this.saveZipButton.click(); } await this.inShopButton.click(); - await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); + await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click()); } else { - await this.firstAppointmentButton.click(); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } @@ -114,9 +119,6 @@ export class ServiceLocationPage extends BasePage { await this.mobileButton.click(); await this.enterServiceAddressButton.click(); await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! }); - if (await this.repeatedClicksModalCloseButton.isVisible()) { - await this.repeatedClicksModalCloseButton.click(); - } if (faker.datatype.boolean()) { await this.vehicleProtectedYesButton.check(); } else { @@ -133,8 +135,13 @@ export class ServiceLocationPage extends BasePage { if (appointmentDetails && appointmentDetails.shopAddress) { await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); } else { - // await this.firstAppointmentButton.click(); - await this.clickWithRetry(this.firstAppointmentButton, this.page); + /// await this.clickWithRetry(this.firstAppointmentButton, this.page); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 9a3b87ae3..6e09c003e 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -41,7 +41,7 @@ export class ServicePackagesPage extends BasePage { 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' }); - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); // this.validateURL(this.url); } @@ -76,8 +76,8 @@ export class ServicePackagesPage extends BasePage { if (await this.repeatedClicksModalCloseButton.isVisible()) { await this.repeatedClicksModalCloseButton.click(); } + await this.page.waitForTimeout(3000); await this.closeButton.click(); - } else { await this.skipQuoteEmailButton.click(); } diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 6bef5a2f3..524e93664 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -192,8 +192,8 @@ async function runWorkflow(page: Page, testCase: TestCase) { //============================= TEST WORKFLOW STEPS ============================= - // Execute home page for qa and dev environments (skip for sys) - if (process.env.NODE_ENV !== 'sys') { + // Use Environment Variable to decide whether or not we want to skip content site aka home page + if (process.env.SKIP_CONTENT_SITE == "false") { await test.step('HomePage >> Lets Get Started', async () => { let homePage = testCase.pages.homePage; console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`); From 97b7933cc3d3b7f1694a38ce22b514d0e65e77cb Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Fri, 25 Apr 2025 14:35:46 +0530 Subject: [PATCH 27/50] CASH-205 remove extra space and use of css constants --- .../afterpay-breakout/afterpay-breakout.vue | 65 ++++++++++++------- src/styles/ux-variables.scss | 6 ++ 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue b/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue index 06a717289..7fabe40aa 100644 --- a/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue +++ b/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue @@ -3,18 +3,16 @@
-   {{ this.afterpayPrice }}
-   - -   + + data-bind="click:afterpayLearnMore" + class="afterpay-learn-more"> Info Icon
@@ -151,8 +149,8 @@ export default { &.alert-info { .alert-heading { - color: #000000; - font-weight: 600; + color: $black; + font-weight: $font-weight-600; margin-left: 0.5rem; margin-right: 0.5rem; @include media-breakpoint-up(md) { @@ -162,11 +160,12 @@ export default { } } .afterpay-amount { - color: #0c7e47; + color: $green; + margin-left: 0.125rem; } .alert-sub-heading { - color: #4b4c4e; - font-weight: 400; + color: $gray-1100; + font-weight: $font-weight-normal; } .after-pay { .after-pay-toggle { @@ -180,7 +179,7 @@ export default { background-position: right center; margin-left: 0.5rem; width: 1rem; - height: 9px; + height: 0.5625rem; display: inline-flex; } &.expanded:after { @@ -194,8 +193,8 @@ export default { } } .after-pay-toggle a { - font-weight: 600; - color: #0070d1; + font-weight: $font-weight-600; + color: $blue; text-decoration: none; } .after-pay-details { @@ -218,27 +217,45 @@ export default { .payment-card { display: flex; flex-direction: column; - border-radius: 6px; - border: 0.25px #b3b4b5; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + border-radius: 0.375rem; + border: 0.25px $gray-1000; + box-shadow: 0px 4px 8px -4px rgba(0, 0, 0, 0.15); } .calendar-section { - background-color: #fff; /* White background for calendar area */ + background-color: $white; /* White background for calendar area */ padding: 0.5rem; text-align: center; - gap: 2px; - font-weight: 700; + gap: 0.125rem; + font-weight: $font-weight-700; white-space: nowrap; + border-top-left-radius: 0.375rem; + border-top-right-radius: 0.375rem; + width: 4.375rem; + @include media-breakpoint-up(md) { + width: 5rem; + } } .payment-amount-section { - background-color: #e0f7e9; /* Light green background for payment area */ + background-color: $green-600; /* Light green background for payment area */ padding: 0.5rem; text-align: center; - font-weight: 700; + font-weight: $font-weight-700; + border-bottom-left-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; + width: 4.375rem; + @include media-breakpoint-up(md) { + width: 5rem; + } + } + .afterpay-learn-more { + margin-left: 0.125rem; + } + .afterpay-image { + margin-left: 0.125rem; } .amount-due { - color: #00723e; /* Green text */ + color: $green-1000; /* Green text */ } .legend { display: flex; @@ -249,11 +266,11 @@ export default { .legend-item { display: flex; align-items: center; - gap: 2px; + gap: 0.125rem; white-space: nowrap; } .legend-item span { - font-weight: 400; + font-weight: $font-weight-normal; } } diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 5c4045fd7..6f377a068 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -33,9 +33,11 @@ $green-300: #94d7b6; $green-400: #5abc8c; // Used in theme $green-500: #2ca168; $green: #0c7e47; // Default Green +$green-600: #e7f2ed; $green-700: #006a36; $green-800: #004f28; $green-900: #00331a; +$green-1000: #00723e; // Yellows $yellow-100: #fff5eb; @@ -59,6 +61,8 @@ $gray-600: #4d5151; // Used in theme $gray-700: #303333; // Used in theme $gray-800: #222424; // Used in theme $gray-900: #181a1a; // Used in theme +$gray-1000: #b3b4b5; +$gray-1100: #4b4c4e; //orange $orange-600: #e86421; @@ -134,6 +138,8 @@ $font-weight-lighter: lighter; $font-weight-light: 300; $font-weight-normal: 400; $font-weight-bold: 500; +$font-weight-600: 600; +$font-weight-700: 700; $font-weight-bolder: bolder; //Headings From a7e9d7c8fe4554d1979036100d4dced479752d0c Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Fri, 25 Apr 2025 19:33:19 +0530 Subject: [PATCH 28/50] CASH-205 more margin for amount info icon and afterpay image --- .../payment-method/afterpay-breakout/afterpay-breakout.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue b/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue index 7fabe40aa..90a3edeb1 100644 --- a/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue +++ b/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue @@ -161,7 +161,7 @@ export default { } .afterpay-amount { color: $green; - margin-left: 0.125rem; + margin-left: 0.250rem; } .alert-sub-heading { color: $gray-1100; @@ -248,10 +248,10 @@ export default { } } .afterpay-learn-more { - margin-left: 0.125rem; + margin-left: 0.250rem; } .afterpay-image { - margin-left: 0.125rem; + margin-left: 0.250rem; } .amount-due { From 9d0bfa3ae281a847f3cbc5cf42801ef291698852 Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Fri, 25 Apr 2025 19:37:50 +0530 Subject: [PATCH 29/50] CASH-205 formatting --- .../payment-method/afterpay-breakout/afterpay-breakout.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue b/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue index 90a3edeb1..0e1e3d58a 100644 --- a/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue +++ b/src/layouts/payment-method/afterpay-breakout/afterpay-breakout.vue @@ -161,7 +161,7 @@ export default { } .afterpay-amount { color: $green; - margin-left: 0.250rem; + margin-left: 0.25rem; } .alert-sub-heading { color: $gray-1100; @@ -248,10 +248,10 @@ export default { } } .afterpay-learn-more { - margin-left: 0.250rem; + margin-left: 0.25rem; } .afterpay-image { - margin-left: 0.250rem; + margin-left: 0.25rem; } .amount-due { From 9143df10e4b4f6203784c0f0f38bd8837e2c6c7a Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:09:21 -0400 Subject: [PATCH 30/50] few minor changes. --- .../pages/OrderConfirmationPage.ts | 47 ++++++++++++++++--- playwright-tests/pages/PaymentMethodPage.ts | 7 ++- playwright-tests/pages/SchedulePage.ts | 5 +- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/playwright-tests/pages/OrderConfirmationPage.ts b/playwright-tests/pages/OrderConfirmationPage.ts index af05c132d..1c6158c68 100644 --- a/playwright-tests/pages/OrderConfirmationPage.ts +++ b/playwright-tests/pages/OrderConfirmationPage.ts @@ -2,7 +2,7 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { test } from '@business-logic/types/Test'; import { ICustomerDetails, IVehicleDetails } from '@business-logic/types/CustomerDetails'; -import { ServicePackage, PaymentType, PaymentMethod } from '@business-logic/types/Enums'; +import { ServicePackage, PaymentType, PaymentMethod, AppointmentType } from '@business-logic/types/Enums'; import { ITestData } from '@business-logic/types/ITestData'; import { step } from '@business-logic/types/Step'; @@ -19,6 +19,7 @@ export class OrderConfirmationPage extends BasePage { readonly cartServicePackageText: Locator; readonly winshieldWiper: Locator; readonly rainDefense: Locator; + readonly appointmentSummarySection: Locator; url = process.env['BASE_URL']! + '/fmg/?fmgPage=confirmation'; @@ -36,6 +37,7 @@ export class OrderConfirmationPage extends BasePage { this.subtotalText = this.page.locator('.sub-total'); this.finalAmountDue = this.page.locator('div.amount-due'); this.cartServicePackageText = this.cartServicePackageText = this.page.locator('.cart-panel'); + this.appointmentSummarySection = this.page.locator('.main .scheduleText, .main .add-to-calendar-text, .main .appointment-text, .main .duration-text-block'); // this.validateURL(this.url); } @@ -44,10 +46,10 @@ export class OrderConfirmationPage extends BasePage { const { vehicleDetails, customerDetails, servicePackage, promoCode, isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod } = testData; await this.serviceText.waitFor({ state: "visible" }); + + expect.soft((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase())); // Grab text - const serviceTextValue = await this.serviceText.textContent(); - const apptDateValue = await this.apptDateText.textContent(); const emailTextValue = await this.emailText.textContent(); const servicePackageValue = await this.cartServicePackageText.textContent(); const amountDueValue = await this.amountDueText.textContent(); @@ -59,9 +61,6 @@ export class OrderConfirmationPage extends BasePage { const servicePackageAmt = Number.parseFloat(servicePackageValue!.split('$')[1].replaceAll(',', '')); // General Validations - expect.soft(serviceTextValue).toContain(`${vehicleDetails!.year} ${vehicleDetails!.make} ${vehicleDetails!.model}`); - const formattedExpectedAppointmentDate = await this.getFormattedAppointmentDate(customerDetails!.apptDate!); - expect.soft(apptDateValue).toContain(formattedExpectedAppointmentDate); expect.soft(emailTextValue).toContain(customerDetails!.email); // Service package validations @@ -141,6 +140,42 @@ export class OrderConfirmationPage extends BasePage { return sessionStorage.order.workOrderNumber; } + async getActualAppointmentSummary(): Promise { + let appointmentSummary: string[] = []; + for (let element of await this.appointmentSummarySection.all()){ + let text = (await element.textContent() || "").replaceAll(/\u00A0| /g, ' '); + appointmentSummary.push(text.trim()); + } + return appointmentSummary; + } + + async getExpectedAppointmentSummary(testData: Partial): Promise { + const { appointmentDetails, customerDetails, vehicleDetails } = testData; + let appointmentSummary: string[] = []; + + // Format the expected appointment date + const formattedExpectedAppointmentDate = await this.getFormattedAppointmentDate(customerDetails!.apptDate!); + appointmentSummary.push( + appointmentDetails?.serviceLocation == AppointmentType.Mobile + ? formattedExpectedAppointmentDate + `${customerDetails!.apptTime?.replace("arriving between", "Between").replaceAll(":00", "")}` + : appointmentDetails?.serviceLocation == AppointmentType.InShop + ? formattedExpectedAppointmentDate + `${customerDetails!.apptTime}` + : formattedExpectedAppointmentDate + "Drop off before 9:30 AM" + ); + appointmentSummary.push("Add to calendar"); + appointmentSummary.push( + appointmentDetails?.serviceLocation == AppointmentType.Mobile + ? appointmentDetails?.serviceAddress + ? ("We're coming to you at" + appointmentDetails.serviceAddress.street + ", " + appointmentDetails.serviceAddress.city + ", " + appointmentDetails.serviceAddress.state + " " + appointmentDetails.serviceAddress.postalCode + "to service your " + `${vehicleDetails!.year} ${vehicleDetails!.make} ${vehicleDetails!.model}`) + : "" + : appointmentDetails?.shopAddress + ? ("You're going to a Safelite shop at" + appointmentDetails.shopAddress + "to service your " + `${vehicleDetails!.year} ${vehicleDetails!.make} ${vehicleDetails!.model}`) + : ""); + appointmentSummary.push("Duration: " + customerDetails!.apptDuration!); + + return appointmentSummary; + } + @step("OrderConfirmationPage >> Validate order") async verifyOrderConfirmationPage(testData: Partial) { await this.validateOrderConfirmationPage(testData); diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 7971cef59..92d0da651 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -350,8 +350,11 @@ l hasNonWindshieldGlass = localStorage.order.lineItems.glassParts.find((item: any) => item.partType !== "WINDSHIELD") ? true : false; } let isCaliforniaState = localStorage.order.serviceLocation.state as string == 'CA' ? true : false; - let recalRequired = !isRepair && (isInsurance || isCaliforniaState) && - localStorage.order.lineItems.glassParts.find((item: any) => item["requiresRecalibration"].value === true) ? true : false; + let hasRecalPart: boolean = false; + if (!isRepair) { + hasRecalPart = localStorage.order.lineItems.glassParts.find((item: any) => item.requiresRecalibration === true) ? true : false; + } + let recalRequired = !isRepair && (isInsurance || isCaliforniaState) && hasRecalPart let stringForRepair: string[] = ["Expert windshield repair", "Exclusive resin sealant", "Nationwide lifetime guarantee"]; let stringIfRecal = isRepair ? "" diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index dd81a4f94..5daa3060e 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -51,8 +51,9 @@ export class SchedulePage extends BasePage { while (!(await this.firstAvailableDate.isVisible())) { await this.viewMoreDatesLink.click(); } - await this.firstAvailableDate.click(); - customerDetails.apptDate = `${await this.firstAvailableDate.getAttribute("id")}` + await this.firstAvailableDate.click().then(async () => { + customerDetails.apptDate = `${await this.firstAvailableDate.getAttribute("id")}` + }); // const timeSlots = this.timeSlots; const timeSlotCount = await this.timeSlots.count(); From f8ca961f73c51d4a5c11dc19792e97d7b0cbadf2 Mon Sep 17 00:00:00 2001 From: Matt Caimi Date: Thu, 24 Apr 2025 13:06:02 -0400 Subject: [PATCH 31/50] CASH-376 open mobile address modal when mobile address is incomplete --- .../mobile-location-modal-questions.vue | 25 +++++++++++-------- .../service-location/service-location.vue | 6 ++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index 0d7a82248..96977dd2b 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -169,17 +169,7 @@ export default { return this.getCmsContent(this.linkWidgetName, "HeaderText"); }, mobileLocationLinkText() { - if ( - this.addressModel.streetAddress && - this.addressModel.streetAddress !== "" && - this.addressModel.city && - this.addressModel.city !== "" && - this.addressModel.state && - this.addressModel.state !== "" && - this.addressModel.zipCode && - this.addressModel.zipCode !== "" && - this.internalModel.isVehicleProtected !== null - ) { + if (this.isMobileAddressComplete()) { return `${this.addressModel.streetAddress}\n${this.addressModel.city}, ${this.addressModel.state} ${this.addressModel.zipCode}`; } return this.getCmsContent(this.linkWidgetName, "BodyText"); @@ -218,6 +208,19 @@ export default { }, }, methods: { + isMobileAddressComplete() { + return ( + this.addressModel.streetAddress && + this.addressModel.streetAddress !== "" && + this.addressModel.city && + this.addressModel.city !== "" && + this.addressModel.state && + this.addressModel.state !== "" && + this.addressModel.zipCode && + this.addressModel.zipCode !== "" && + this.internalModel.isVehicleProtected !== null + ); + }, openModal() { this.modal.openModal(); }, diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index a85a5bd17..02066d97e 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -88,7 +88,7 @@
Date: Thu, 24 Apr 2025 14:12:03 -0400 Subject: [PATCH 32/50] CASH-376 removing transition prevents rendering CTA on top of modal --- .../mobile-location-modal-questions.vue | 130 +++++++++--------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index 96977dd2b..5b0a79255 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -1,72 +1,70 @@