Merge branch 'feature/CASH-481' into feature/CASH-488

This commit is contained in:
maguire-arman 2025-04-10 13:53:52 -04:00
commit 4f434c5488
11 changed files with 904 additions and 13 deletions

View file

@ -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

135
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -43,7 +43,7 @@ export function getCustomerDetails(): ICustomerDetails {
street: faker.location.streetAddress(),
city: 'Columbus',
state: 'Ohio',
postalCode: '43215',
postalCode: '21237',
country: 'United States'
}
};

View file

@ -0,0 +1,111 @@
export interface PutEditIssueRequestBody {
fields: Partial<JiraIssueFields>
}
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 {}

View file

@ -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<GetIssueResponse>(url, {
headers: {
Authorization: `Basic ${encodedAuthKey}`,
Accept: 'application/json'
}
});
}
postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) {
return this.axiosClient.post<PostBulkFetchIssuesResponse>(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<GetIssueTransitionsResponse>(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<GetIssueTypesResponse>(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<PostCreateIssueResponse>(this.issueUrl, requestBody, {
headers: {
Authorization: `Basic ${encodedAuthKey}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
});
}
postBulkCreateIssue(requestBody: PostBulkCreateIssueRequestBody) {
return this.axiosClient.post<PostBulkCreateIssueResponse>(this.bulkIssueCreateUrl, requestBody, {
headers: {
Authorization: `Basic ${encodedAuthKey}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
});
}
}

View file

@ -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<void> {
// return this.ortoniReport.onExit();
// }
// onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void {
// this.ortoniReport.onStdOut(chunk, test, result);
// }
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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}`);