Set up daily regression run and extended Jira Writeback Reporter to cover all Jira API functionality (#938)
* Initial regression pipeline * Progress toward Jira API Util changes * Updated Regression run and Jira Writeback Reporter to work together * Temporarily disabled cron * Allow Jira Writeback Reporter to set UAT tester * Updates to azure-pipelines to avoid using jira command line * Deleted unused script * Changed pool to default * Added back closing quote * Added missing paren * Set up cron schedule
This commit is contained in:
parent
6bef3840cc
commit
2791150daf
10 changed files with 555 additions and 228 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -28,6 +28,7 @@ pnpm-debug.log*
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
artifacts/
|
artifacts/
|
||||||
|
ortoni-report/
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
coverage/*
|
coverage/*
|
||||||
|
|
|
||||||
187
azure-pipelines-automated-testing.yml
Normal file
187
azure-pipelines-automated-testing.yml
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
trigger: none
|
||||||
|
schedules:
|
||||||
|
- cron: 0 9 * * MON-FRI
|
||||||
|
always: true
|
||||||
|
displayName: Daily Test Automation Run for ISS-NextGen
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- develop
|
||||||
|
pool: 'Default'
|
||||||
|
|
||||||
|
variables:
|
||||||
|
# - group: Digital-Infrastructure
|
||||||
|
# - group: ISS-BuildBranches
|
||||||
|
- name: dockerImageName
|
||||||
|
value: 'playwright-tests'
|
||||||
|
- name: imageTag
|
||||||
|
value: '$(Build.BuildId)'
|
||||||
|
- name: totalShards
|
||||||
|
value: 4
|
||||||
|
- name: IS_REGRESSION
|
||||||
|
value: 'true'
|
||||||
|
- name: JIRA_BOARD_ID
|
||||||
|
value: '853'
|
||||||
|
- name: JIRA_EPIC_KEY
|
||||||
|
value: 'INSR-414'
|
||||||
|
- name: JIRA_PROJECT_KEY
|
||||||
|
value: 'INSR'
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- stage: TestPr
|
||||||
|
displayName: Run Playwright Test
|
||||||
|
jobs:
|
||||||
|
- job: playwright_tests
|
||||||
|
continueOnError: true
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
shard1:
|
||||||
|
shardNumber: 1
|
||||||
|
shard2:
|
||||||
|
shardNumber: 2
|
||||||
|
shard3:
|
||||||
|
shardNumber: 3
|
||||||
|
shard4:
|
||||||
|
shardNumber: 4
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: Docker@2
|
||||||
|
displayName: 'Build Docker Image'
|
||||||
|
inputs:
|
||||||
|
command: build
|
||||||
|
dockerfile: Dockerfile.playwright
|
||||||
|
repository: $(dockerImageName)
|
||||||
|
tags: $(imageTag)
|
||||||
|
arguments: '--no-cache --pull'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
# 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 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 \"@smoke|@Advanced\"")
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
- job: download_and_merge_reports
|
||||||
|
dependsOn: playwright_tests
|
||||||
|
timeoutInMinutes: 8
|
||||||
|
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: |
|
||||||
|
# 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 IS_REGRESSION=$(IS_REGRESSION) \
|
||||||
|
-e JIRA_BOARD_ID=$(JIRA_BOARD_ID) \
|
||||||
|
-e JIRA_EPIC_KEY=$(JIRA_EPIC_KEY) \
|
||||||
|
-e JIRA_PROJECT_KEY=$(JIRA_PROJECT_KEY) \
|
||||||
|
$(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...\" &&
|
||||||
|
PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx playwright merge-reports --reporter='playwright-tests/impl/reporter/JiraWritebackReporter.ts',junit ./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 ")
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
@ -39,6 +39,8 @@ variables:
|
||||||
value: '$(Build.BuildId)'
|
value: '$(Build.BuildId)'
|
||||||
- name: totalShards
|
- name: totalShards
|
||||||
value: 2
|
value: 2
|
||||||
|
- name: IS_REGRESSION
|
||||||
|
value: 'false'
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
# PR's
|
# PR's
|
||||||
|
|
@ -160,19 +162,18 @@ stages:
|
||||||
-e JIRA_SERVER=$(JIRA_SERVER) \
|
-e JIRA_SERVER=$(JIRA_SERVER) \
|
||||||
-e JIRA_USERNAME=$(JIRA_USERNAME) \
|
-e JIRA_USERNAME=$(JIRA_USERNAME) \
|
||||||
-e JIRA_API_KEY=$(JIRA_API_KEY) \
|
-e JIRA_API_KEY=$(JIRA_API_KEY) \
|
||||||
|
-e IS_REGRESSION=$(IS_REGRESSION) \
|
||||||
-e JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER" \
|
-e JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER" \
|
||||||
$(dockerImageName):$(imageTag) \
|
$(dockerImageName):$(imageTag) \
|
||||||
bash -c "echo \"Moving Playwright reports out of subfolders...\" &&
|
bash -c "echo \"Moving Playwright reports out of subfolders...\" &&
|
||||||
find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \; &&
|
find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \; &&
|
||||||
echo \"Merging reports...\" &&
|
echo \"Merging reports...\" &&
|
||||||
export NODE_OPTIONS=--max_old_space_size=4096
|
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
|
PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx playwright merge-reports --reporter=junit,\"/app/playwright-tests/impl/reporter/JiraWritebackReporter.ts\" ./playwright-reports
|
||||||
echo \"Contents of ortoni-report:\" && ls ./ortoni-report &&
|
echo \"Contents of ortoni-report:\" && ls ./ortoni-report &&
|
||||||
echo 'Current dir: ' && pwd
|
echo 'Current dir: ' && pwd
|
||||||
echo 'Contents of current dir: ' && ls
|
echo 'Contents of current dir: ' && ls
|
||||||
echo 'Contents of /app/test-results' && ls /app/test-results
|
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
|
# Start container and stream logs
|
||||||
echo "Starting merge"
|
echo "Starting merge"
|
||||||
|
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
create_issue() {
|
|
||||||
local title="$1"
|
|
||||||
local project_key="$2"
|
|
||||||
local issue_type="$3"
|
|
||||||
local parent_issue_key="$4"
|
|
||||||
|
|
||||||
AUTH=$(echo -ne "$JIRA_USERNAME:$JIRA_API_KEY" | base64 --wrap 0)
|
|
||||||
|
|
||||||
local parent_issue=$(curl -s -H "Authorization: Basic $AUTH" \
|
|
||||||
"$JIRA_SERVER/rest/api/3/issue/$parent_issue_key")
|
|
||||||
|
|
||||||
local parent_fix_versions=$(echo $parent_issue | jq -r '.fields.fixVersions')
|
|
||||||
|
|
||||||
local created_issue=$(curl -X POST -H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Basic $AUTH" \
|
|
||||||
-d '{
|
|
||||||
"fields": {
|
|
||||||
"summary": "'"$title"'",
|
|
||||||
"project": {
|
|
||||||
"key": "'"$project_key"'"
|
|
||||||
},
|
|
||||||
"issuetype": {
|
|
||||||
"name": "'"$issue_type"'"
|
|
||||||
},
|
|
||||||
"parent": {
|
|
||||||
"key": "'"$parent_issue_key"'"
|
|
||||||
},
|
|
||||||
"fixVersions": '"$parent_fix_versions"'
|
|
||||||
}
|
|
||||||
}' \
|
|
||||||
"$JIRA_SERVER/rest/api/3/issue")
|
|
||||||
|
|
||||||
local created_issue_id=$(echo $created_issue | jq -r '.id')
|
|
||||||
|
|
||||||
curl -X PUT -H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Basic $AUTH" \
|
|
||||||
-d '{
|
|
||||||
"fields": {
|
|
||||||
"fixVersions": '"$parent_fix_versions"'
|
|
||||||
}
|
|
||||||
}' \
|
|
||||||
"$JIRA_SERVER/rest/api/3/issue/$created_issue_id"
|
|
||||||
}
|
|
||||||
|
|
||||||
extract_uuid() {
|
|
||||||
local url="$1"
|
|
||||||
local uuid_regex='[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
|
|
||||||
if [[ "$url" =~ $uuid_regex ]]; then
|
|
||||||
echo "${BASH_REMATCH}"
|
|
||||||
else
|
|
||||||
echo "No UUID found in the URL."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
update_issue_status() {
|
|
||||||
local issue_key="$1"
|
|
||||||
local status_name="$2"
|
|
||||||
|
|
||||||
AUTH=$(echo -ne "$JIRA_USERNAME:$JIRA_API_KEY" | base64 --wrap 0)
|
|
||||||
|
|
||||||
local transitions=$(curl -s -H "Authorization: Basic $AUTH" \
|
|
||||||
"$JIRA_SERVER/rest/api/3/issue/$issue_key/transitions")
|
|
||||||
|
|
||||||
local transition_id=$(echo "$transitions" | jq -r --arg status_name "$status_name" '
|
|
||||||
.transitions[] | select(.isAvailable == true and .to.name == $status_name) | .id
|
|
||||||
')
|
|
||||||
|
|
||||||
if [ -z "$transition_id" ]; then
|
|
||||||
echo "BadRequestError"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
curl -X POST -H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Basic $AUTH" \
|
|
||||||
-d '{
|
|
||||||
"transition": {
|
|
||||||
"id": "'"$transition_id"'"
|
|
||||||
}
|
|
||||||
}' \
|
|
||||||
"$JIRA_SERVER/rest/api/3/issue/$issue_key/transitions"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
add_attachments() {
|
|
||||||
AUTH=$(echo -ne "$JIRA_USERNAME:$JIRA_API_KEY" | base64 --wrap 0)
|
|
||||||
|
|
||||||
local issue_key="$1"
|
|
||||||
shift
|
|
||||||
local attachments=("$@")
|
|
||||||
|
|
||||||
echo $issue_key
|
|
||||||
echo $attachments
|
|
||||||
|
|
||||||
local form_data=""
|
|
||||||
for attachment in "${attachments[@]}"; do
|
|
||||||
form_data+="--form file=@$attachment "
|
|
||||||
done
|
|
||||||
|
|
||||||
echo $form_data
|
|
||||||
|
|
||||||
echo $(curl -X POST $form_data \
|
|
||||||
-H "X-Atlassian-Token: no-check" \
|
|
||||||
-H "Authorization: Basic $AUTH" \
|
|
||||||
"$JIRA_SERVER/rest/api/3/issue/$issue_key/attachments")
|
|
||||||
}
|
|
||||||
|
|
||||||
add_comment() {
|
|
||||||
AUTH=$(echo -ne "$JIRA_USERNAME:$JIRA_API_KEY" | base64 --wrap 0)
|
|
||||||
local issue_key="$1"
|
|
||||||
shift
|
|
||||||
local comment_items_input=("$@")
|
|
||||||
local comment_json="[]"
|
|
||||||
|
|
||||||
for item in "${comment_items_input[@]}"; do
|
|
||||||
if [ -e "$item" ]; then
|
|
||||||
# If it's a file path
|
|
||||||
local attachment=$(add_attachments "$issue_key" "$item")
|
|
||||||
local id=$(echo $attachment | grep -oP '"id":\s*"\K[^"]+')
|
|
||||||
|
|
||||||
local attachment_content=$(curl -s -I -L -H "Authorization: Basic $AUTH" "$JIRA_SERVER/rest/api/3/attachment/content/$id" \
|
|
||||||
| grep -i "Location:" | tail -1 | awk '{print $2}' | tr -d '\r')
|
|
||||||
echo "$JIRA_SERVER/rest/api/3/attachment/content/$id"
|
|
||||||
echo "$attachment_content"
|
|
||||||
|
|
||||||
local uuid=$(extract_uuid "$attachment_content")
|
|
||||||
echo "$uuid"
|
|
||||||
|
|
||||||
json_object=$(jq -n --arg uuid "$uuid" '{ type: "mediaSingle", attrs: { layout: "align-start" }, content: [{ type: "media", attrs: { type: "file", id: $uuid, width: 200, height: 200, collection: "", alt: "" } }]}')
|
|
||||||
|
|
||||||
comment_json=$(echo "$comment_json" | jq --argjson obj "$json_object" '. += [$obj]')
|
|
||||||
else
|
|
||||||
# If it's a string
|
|
||||||
json_object=$(jq -n --arg text "$item" '{ type: "paragraph", content: [{ type: "text", text: $text }]}')
|
|
||||||
|
|
||||||
comment_json=$(echo "$comment_json" | jq --argjson obj "$json_object" '. += [$obj]')
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
request=$(jq -n --argjson content "$comment_json" '{body: { type: "doc", version: 1, content: $content }}')
|
|
||||||
|
|
||||||
curl -X POST -H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Basic $AUTH" \
|
|
||||||
-d "$request" \
|
|
||||||
"$JIRA_SERVER/rest/api/3/issue/$issue_key/comment"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ $# -gt 0 ]]; then # IF function call passed in
|
|
||||||
"$@" # Call function
|
|
||||||
fi
|
|
||||||
169
package-lock.json
generated
169
package-lock.json
generated
|
|
@ -31,6 +31,7 @@
|
||||||
"@testing-library/user-event": "14.4.3",
|
"@testing-library/user-event": "14.4.3",
|
||||||
"@testing-library/vue": "6.6.1",
|
"@testing-library/vue": "6.6.1",
|
||||||
"@types/dotenv-safe": "^8.1.6",
|
"@types/dotenv-safe": "^8.1.6",
|
||||||
|
"@types/form-data": "^2.2.1",
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.5",
|
||||||
"@vitejs/plugin-vue": "4.2.3",
|
"@vitejs/plugin-vue": "4.2.3",
|
||||||
"@vitest/coverage-v8": "^0.34.1",
|
"@vitest/coverage-v8": "^0.34.1",
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-jsdoc": "^46.4.3",
|
"eslint-plugin-jsdoc": "^46.4.3",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"form-data": "^4.0.2",
|
||||||
"jest": "^27.0.5",
|
"jest": "^27.0.5",
|
||||||
"jest-junit": "^13.0.0",
|
"jest-junit": "^13.0.0",
|
||||||
"jest-serializer-vue": "^3.1.0",
|
"jest-serializer-vue": "^3.1.0",
|
||||||
|
|
@ -3852,6 +3854,16 @@
|
||||||
"@types/range-parser": "*"
|
"@types/range-parser": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/form-data": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
|
|
@ -6737,6 +6749,20 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
|
@ -8531,6 +8557,21 @@
|
||||||
"dotenv": ">= 8.2.0"
|
"dotenv": ">= 8.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/duplexer": {
|
"node_modules/duplexer": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||||
|
|
@ -8797,13 +8838,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"license": "MIT",
|
||||||
"get-intrinsic": "^1.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
|
|
@ -8848,6 +8887,35 @@
|
||||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"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/es-shim-unscopables": {
|
"node_modules/es-shim-unscopables": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
|
||||||
|
|
@ -10077,13 +10145,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -10271,16 +10341,22 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.2.4",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"has-proto": "^1.0.1",
|
"get-proto": "^1.0.1",
|
||||||
"has-symbols": "^1.0.3",
|
"gopd": "^1.2.0",
|
||||||
"hasown": "^2.0.0"
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -10298,6 +10374,20 @@
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-stream": {
|
"node_modules/get-stream": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||||
|
|
@ -10402,12 +10492,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.0.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"license": "MIT",
|
||||||
"get-intrinsic": "^1.1.3"
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|
@ -10488,23 +10579,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-proto": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
|
|
@ -10513,12 +10593,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-tostringtag": {
|
"node_modules/has-tostringtag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.2"
|
"has-symbols": "^1.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -15216,6 +15297,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/maska/-/maska-2.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/maska/-/maska-2.1.11.tgz",
|
||||||
"integrity": "sha512-IGqWjBnKxMYcVa06pb4mPfag9sJjnR2T15CdGfQ2llR3gajiSd4AxXCvNqHMEq9W3UBhjjTazgWumsP3sWrUSg=="
|
"integrity": "sha512-IGqWjBnKxMYcVa06pb4mPfag9sJjnR2T15CdGfQ2llR3gajiSd4AxXCvNqHMEq9W3UBhjjTazgWumsP3sWrUSg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mdn-data": {
|
"node_modules/mdn-data": {
|
||||||
"version": "2.0.14",
|
"version": "2.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"@testing-library/user-event": "14.4.3",
|
"@testing-library/user-event": "14.4.3",
|
||||||
"@testing-library/vue": "6.6.1",
|
"@testing-library/vue": "6.6.1",
|
||||||
"@types/dotenv-safe": "^8.1.6",
|
"@types/dotenv-safe": "^8.1.6",
|
||||||
|
"@types/form-data": "^2.2.1",
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.5",
|
||||||
"@vitejs/plugin-vue": "4.2.3",
|
"@vitejs/plugin-vue": "4.2.3",
|
||||||
"@vitest/coverage-v8": "^0.34.1",
|
"@vitest/coverage-v8": "^0.34.1",
|
||||||
|
|
@ -48,6 +49,7 @@
|
||||||
"@vue/cli-service": "~5.0.0",
|
"@vue/cli-service": "~5.0.0",
|
||||||
"@vue/test-utils": "^2.4.1",
|
"@vue/test-utils": "^2.4.1",
|
||||||
"@vue/vue3-jest": "^27.0.0-alpha.1",
|
"@vue/vue3-jest": "^27.0.0-alpha.1",
|
||||||
|
"axe-core": "^4.10.2",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
"axios-mock-adapter": "^1.21.5",
|
"axios-mock-adapter": "^1.21.5",
|
||||||
"babel-jest": "^27.0.6",
|
"babel-jest": "^27.0.6",
|
||||||
|
|
@ -59,6 +61,7 @@
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-jsdoc": "^46.4.3",
|
"eslint-plugin-jsdoc": "^46.4.3",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"form-data": "^4.0.2",
|
||||||
"jest": "^27.0.5",
|
"jest": "^27.0.5",
|
||||||
"jest-junit": "^13.0.0",
|
"jest-junit": "^13.0.0",
|
||||||
"jest-serializer-vue": "^3.1.0",
|
"jest-serializer-vue": "^3.1.0",
|
||||||
|
|
@ -73,7 +76,6 @@
|
||||||
"vite": "^4.5.9",
|
"vite": "^4.5.9",
|
||||||
"vitest": "^0.33.0",
|
"vitest": "^0.33.0",
|
||||||
"volar-service-vetur": "latest",
|
"volar-service-vetur": "latest",
|
||||||
"wait-on": "^8.0.2",
|
"wait-on": "^8.0.2"
|
||||||
"axe-core": "^4.10.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ export interface JiraIssueFields {
|
||||||
parent?: JiraParent,
|
parent?: JiraParent,
|
||||||
fixVersions?: JiraVersion[],
|
fixVersions?: JiraVersion[],
|
||||||
subtasks?: JiraSubTask[],
|
subtasks?: JiraSubTask[],
|
||||||
customfield_14857?: JiraContent // Test Steps field
|
customfield_14857?: JiraContent, // Test Steps field
|
||||||
|
customfield_10007?: number // Sprint field,
|
||||||
|
customfield_13100?: { id: string } // UAT Tester ID field
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JiraSubTask {
|
export interface JiraSubTask {
|
||||||
|
|
@ -108,4 +111,25 @@ export interface JiraIssue {
|
||||||
fields: JiraIssueFields
|
fields: JiraIssueFields
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetIssueResponse extends JiraIssue {}
|
export interface GetIssueResponse extends JiraIssue {}
|
||||||
|
|
||||||
|
export interface GetBoardResponse {
|
||||||
|
id: number,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSprintResponse {
|
||||||
|
values: JiraSprint[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetJqlSearchIssueParams {
|
||||||
|
jql: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetJqlSearchIssueResponse extends PostBulkFetchIssuesResponse {}
|
||||||
|
|
||||||
|
export interface JiraSprint {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
originBoardId: number
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { PostBulkFetchIssuesRequestBody, PostBulkFetchIssuesResponse, GetIssueResponse, GetIssueTransitionsResponse, GetIssueTypesResponse, PostAddCommentResponse, PostBulkCreateIssueRequestBody, PostBulkCreateIssueResponse, PostCreateIssueRequestBody, PostCreateIssueResponse, PostTransitionIssueRequestBody, PutEditIssueRequestBody, PostBulkTransitionIssuesRequestBody } from "@business-logic/types/JiraApi";
|
import { PostBulkFetchIssuesRequestBody, PostBulkFetchIssuesResponse, GetIssueResponse, GetIssueTransitionsResponse, GetIssueTypesResponse, PostAddCommentResponse, PostBulkCreateIssueRequestBody, PostBulkCreateIssueResponse, PostCreateIssueRequestBody, PostCreateIssueResponse, PostTransitionIssueRequestBody, PutEditIssueRequestBody, PostBulkTransitionIssuesRequestBody, GetSprintResponse, GetJqlSearchIssueParams, GetJqlSearchIssueResponse } from "@business-logic/types/JiraApi";
|
||||||
import axios, { request } from "axios";
|
import axios from "axios";
|
||||||
import axiosRetry from "axios-retry";
|
import axiosRetry from "axios-retry";
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import path from "path";
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
const jiraUrl = process.env.JIRA_SERVER!;
|
const jiraUrl = process.env.JIRA_SERVER!;
|
||||||
const jiraUsername = process.env.JIRA_USERNAME!;
|
const jiraUsername = process.env.JIRA_USERNAME!;
|
||||||
const jiraApiKey = process.env.JIRA_API_KEY!;
|
const jiraApiKey = process.env.JIRA_API_KEY!;
|
||||||
|
const boardId = process.env.JIRA_BOARD_ID!;
|
||||||
const encodedAuthKey = Buffer.from(`${jiraUsername}:${jiraApiKey}`).toString('base64');
|
const encodedAuthKey = Buffer.from(`${jiraUsername}:${jiraApiKey}`).toString('base64');
|
||||||
|
|
||||||
export default class JiraApiUtil {
|
export default class JiraApiUtil {
|
||||||
|
|
@ -13,6 +17,9 @@ export default class JiraApiUtil {
|
||||||
readonly bulkIssueCreateUrl: string;
|
readonly bulkIssueCreateUrl: string;
|
||||||
readonly bulkIssueFetchUrl: string;
|
readonly bulkIssueFetchUrl: string;
|
||||||
readonly bulkTransitionIssuesUrl: string;
|
readonly bulkTransitionIssuesUrl: string;
|
||||||
|
readonly getCurrentSprintUrl: string;
|
||||||
|
readonly getJqlSearchIssueUrl: string;
|
||||||
|
|
||||||
readonly axiosClient: axios.AxiosInstance;
|
readonly axiosClient: axios.AxiosInstance;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -20,7 +27,9 @@ export default class JiraApiUtil {
|
||||||
this.issueUrl = `${this.baseUrl}/rest/api/3/issue`;
|
this.issueUrl = `${this.baseUrl}/rest/api/3/issue`;
|
||||||
this.bulkIssueCreateUrl = `${this.issueUrl}/bulk`;
|
this.bulkIssueCreateUrl = `${this.issueUrl}/bulk`;
|
||||||
this.bulkIssueFetchUrl = `${this.issueUrl}/bulkfetch`;
|
this.bulkIssueFetchUrl = `${this.issueUrl}/bulkfetch`;
|
||||||
this.bulkTransitionIssuesUrl = `${this.baseUrl}/rest/api/3/bulk/issues/transition`
|
this.bulkTransitionIssuesUrl = `${this.baseUrl}/rest/api/3/bulk/issues/transition`;
|
||||||
|
this.getCurrentSprintUrl = `${this.baseUrl}/rest/agile/1.0/board/${boardId}/sprint?state=active`;
|
||||||
|
this.getJqlSearchIssueUrl = `${this.baseUrl}/rest/api/3/search/jql`;
|
||||||
this.axiosClient = axios.create();
|
this.axiosClient = axios.create();
|
||||||
// interceptor to log error message from api
|
// interceptor to log error message from api
|
||||||
this.axiosClient.interceptors.response.use(
|
this.axiosClient.interceptors.response.use(
|
||||||
|
|
@ -48,6 +57,33 @@ export default class JiraApiUtil {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCurrentSprint() {
|
||||||
|
const res = await this.axiosClient.get<GetSprintResponse>(this.getCurrentSprintUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encodedAuthKey}`,
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const currentSprint = res.data.values.filter(sprint => {
|
||||||
|
return `${sprint.originBoardId}` === boardId;
|
||||||
|
});
|
||||||
|
if (currentSprint.length === 1) {
|
||||||
|
return currentSprint[0];
|
||||||
|
} else {
|
||||||
|
console.error(`JiraApiUtil >> Multiple active sprints found for board ${boardId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getJqlSearchIssue(params: GetJqlSearchIssueParams) {
|
||||||
|
return this.axiosClient.get<GetJqlSearchIssueResponse>(this.getJqlSearchIssueUrl, {
|
||||||
|
params: params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encodedAuthKey}`,
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) {
|
postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) {
|
||||||
return this.axiosClient.post<PostBulkFetchIssuesResponse>(this.bulkIssueFetchUrl, requestBody, {
|
return this.axiosClient.post<PostBulkFetchIssuesResponse>(this.bulkIssueFetchUrl, requestBody, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -140,4 +176,19 @@ export default class JiraApiUtil {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postUploadAttachment(issueKey: string, filePath: string) {
|
||||||
|
const url = `${this.baseUrl}/rest/api/3/issue/${issueKey}/attachments`
|
||||||
|
const form = new FormData();
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
|
||||||
|
form.append('file', fs.createReadStream(filePath), fileName);
|
||||||
|
return this.axiosClient.post(url, form, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encodedAuthKey}`,
|
||||||
|
'X-Atlassian-Token': 'no-check',
|
||||||
|
...form.getHeaders()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,37 @@
|
||||||
import JiraApiUtil from "@impl/api/JiraApiUtil";
|
import JiraApiUtil from "@impl/api/JiraApiUtil";
|
||||||
import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter";
|
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult } from "@playwright/test/reporter";
|
||||||
import { formatDate } from "@impl/utils/DateUtils";
|
import { formatDate, formatDateForFilename } from "@impl/utils/DateUtils";
|
||||||
import { GetIssueResponse, JiraIssue, JiraIssueFields } from "@business-logic/types/JiraApi";
|
import { GetIssueResponse, JiraIssue, JiraIssueFields } from "@business-logic/types/JiraApi";
|
||||||
|
import OrtoniReport, { OrtoniReportConfig } from "ortoni-report";
|
||||||
|
|
||||||
const jiraCardNumber = process.env.JIRA_CARD_NUMBER!;
|
let jiraCardNumber = process.env.JIRA_CARD_NUMBER || '';
|
||||||
const jiraProjectKey = 'INSR';
|
const isRegressionRun = process.env.IS_REGRESSION === 'true'? true: false;
|
||||||
|
const jiraProjectKey = process.env.JIRA_PROJECT_KEY!;
|
||||||
|
const jiraEpicKey = process.env.JIRA_EPIC_KEY!;
|
||||||
const passTransitionId = '111';
|
const passTransitionId = '111';
|
||||||
const failTransitionId = '101';
|
const failTransitionId = '101';
|
||||||
|
const inTestTransitionId = '271';
|
||||||
|
const currentDate = formatDateForFilename(new Date());
|
||||||
|
|
||||||
|
// Ortoni config
|
||||||
|
const reportName = `ortoni_report_${currentDate}.html`;
|
||||||
|
const reportConfig: OrtoniReportConfig = {
|
||||||
|
port: 1994,
|
||||||
|
open: "never",
|
||||||
|
folderPath: "ortoni-report",
|
||||||
|
filename: reportName,
|
||||||
|
logo: 'playwright-tests/business-logic/data/logo.png',
|
||||||
|
title: "Test Report",
|
||||||
|
showProject: false,
|
||||||
|
projectName: "ISS-Nextgen-Playwright-Report",
|
||||||
|
testType: `E2E- Environment: ${process.env.NODE_ENV} `,
|
||||||
|
preferredTheme: "light",
|
||||||
|
base64Image: true,
|
||||||
|
};
|
||||||
|
|
||||||
export default class JiraWritebackReporter implements Reporter {
|
export default class JiraWritebackReporter implements Reporter {
|
||||||
readonly jiraApiUtil: JiraApiUtil = new JiraApiUtil();
|
readonly jiraApiUtil: JiraApiUtil = new JiraApiUtil();
|
||||||
|
readonly ortoniReport: OrtoniReport = new OrtoniReport(reportConfig);
|
||||||
readonly loadIssueCalls: (() => void)[] = []; // Calls to loadIssue must be deferred because they depend on information from the API.
|
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 issuesToPass: string[] = []; // Issues to transition to "Pass".
|
||||||
readonly issuesToFail: string[] = []; // Issues to transition to "Fail".
|
readonly issuesToFail: string[] = []; // Issues to transition to "Fail".
|
||||||
|
|
@ -17,6 +39,7 @@ export default class JiraWritebackReporter implements Reporter {
|
||||||
existingSubtasks: JiraIssue[] = []; // Array to hold existing subtasks of the dev card.
|
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.
|
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.
|
bugTypeId:string|undefined = undefined; // ID of the bug subtask type.
|
||||||
|
userStoryTypeId:string|undefined = undefined;
|
||||||
parentCard: GetIssueResponse | undefined = undefined; // Variable to hold the parent card. Will be filled by API call.
|
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.
|
// Will need this if we want to move ortoni report upload into this reporter.
|
||||||
|
|
@ -36,6 +59,9 @@ export default class JiraWritebackReporter implements Reporter {
|
||||||
this.bugTypeId = issueTypes.issueTypes.find(issueType => {
|
this.bugTypeId = issueTypes.issueTypes.find(issueType => {
|
||||||
return issueType.subtask === true && issueType.name === 'Bug Sub-task'
|
return issueType.subtask === true && issueType.name === 'Bug Sub-task'
|
||||||
})?.id;
|
})?.id;
|
||||||
|
this.userStoryTypeId = issueTypes.issueTypes.find(issueType => {
|
||||||
|
return issueType.name === 'Story';
|
||||||
|
})?.id;
|
||||||
console.log('JiraWritebackReporter >> Loaded issue types.');
|
console.log('JiraWritebackReporter >> Loaded issue types.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,16 +193,97 @@ export default class JiraWritebackReporter implements Reporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param cardName
|
||||||
|
* @param sprintId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getCreateRegressionCardRequestBody(cardName: string, sprintId: number) {
|
||||||
|
const body: JiraIssueFields = {
|
||||||
|
summary: cardName,
|
||||||
|
description: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": 'Card to hold regression test results for this sprint.',
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
key: jiraProjectKey
|
||||||
|
},
|
||||||
|
issuetype: { id: this.userStoryTypeId!},
|
||||||
|
parent: { key: jiraEpicKey },
|
||||||
|
customfield_10007: sprintId,
|
||||||
|
customfield_13100: { id: '557058:a314fc5b-aed9-4472-90f8-00f106e06207' } // Mark UAT Tester as N/A
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grabs parent card and existing subtasks.
|
* Grabs parent card and existing subtasks.
|
||||||
*/
|
*/
|
||||||
async loadReporterData() {
|
async loadReporterData() {
|
||||||
|
let parentCard: GetIssueResponse;
|
||||||
await this.loadJiraIssueTypes();
|
await this.loadJiraIssueTypes();
|
||||||
|
if (isRegressionRun) {
|
||||||
console.log('JiraWritebackReporter >> Searching for parent card...');
|
console.log('JiraWritebackReporter >> Searching for current regression card...');
|
||||||
const parentRes = await this.jiraApiUtil.getIssue(jiraCardNumber);
|
const sprint = await this.jiraApiUtil.getCurrentSprint();
|
||||||
const parentCard = parentRes.data;
|
if (!sprint) {
|
||||||
console.log(`JiraWritebackReporter >> Retrieved parent card: ${parentCard.key}: ${parentCard.fields.summary}`);
|
console.error('JiraWritebackReporter >> Could not find current sprint');
|
||||||
|
} else {
|
||||||
|
console.log(`JiraWritebackReporter >> Found sprint with id '${sprint.id}' called '${sprint.name}'`);
|
||||||
|
}
|
||||||
|
const regressionCardName = `Playwright Automated Regression Tests ${sprint?.name}`;
|
||||||
|
const parentRes = await this.jiraApiUtil.getJqlSearchIssue({ jql: `sprint = ${sprint?.id} and summary ~ "${regressionCardName}"` });
|
||||||
|
if (parentRes.data.issues.length > 1) {
|
||||||
|
console.log('JiraWritebackReporter >> WARNING: Multiple regression cards found. Using first one.');
|
||||||
|
parentCard = (await this.jiraApiUtil.getIssue(parentRes.data.issues[0].id!)).data;
|
||||||
|
await this.jiraApiUtil.postTransitionIssue(parentCard.id!, {
|
||||||
|
transition: {
|
||||||
|
id: inTestTransitionId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (parentRes.data.issues.length < 1) {
|
||||||
|
// NO regression card found. Make one.
|
||||||
|
console.log('JiraWritebackReporter >> No regression card was found for this sprint. Creating one.');
|
||||||
|
const fields = this.getCreateRegressionCardRequestBody(regressionCardName, sprint!.id);
|
||||||
|
const regressionCard: JiraIssue = {
|
||||||
|
fields: fields,
|
||||||
|
transition: {
|
||||||
|
id: inTestTransitionId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createRes = await this.jiraApiUtil.postCreateIssue(regressionCard);
|
||||||
|
regressionCard.id = createRes.data.id;
|
||||||
|
regressionCard.key = createRes.data.key;
|
||||||
|
parentCard = regressionCard;
|
||||||
|
} else {
|
||||||
|
// Found one
|
||||||
|
console.log('JiraWritebackReporter >> Found one regression card. Using it.');
|
||||||
|
parentCard = (await this.jiraApiUtil.getIssue(parentRes.data.issues[0].id!)).data;
|
||||||
|
await this.jiraApiUtil.postTransitionIssue(parentCard.id!, {
|
||||||
|
transition: {
|
||||||
|
id: inTestTransitionId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
jiraCardNumber = parentCard.key!;
|
||||||
|
console.log(`JiraWritebackReporter >> Retrieved current regression card: ${parentCard.key}: ${parentCard.fields.summary}`);
|
||||||
|
} else {
|
||||||
|
console.log('JiraWritebackReporter >> Searching for parent card...');
|
||||||
|
const parentRes = await this.jiraApiUtil.getIssue(jiraCardNumber);
|
||||||
|
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}'...`);
|
console.log(`JiraWritebackReporter >> Searching for all subtasks of parent card '${jiraCardNumber}'...`);
|
||||||
const subtaskIds = parentCard.fields.subtasks?.map(subtask => {
|
const subtaskIds = parentCard.fields.subtasks?.map(subtask => {
|
||||||
|
|
@ -258,7 +365,6 @@ export default class JiraWritebackReporter implements Reporter {
|
||||||
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)) {
|
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;
|
existingBug = card;
|
||||||
console.log('JiraWritebackReporter >> Found existing bug.');
|
console.log('JiraWritebackReporter >> Found existing bug.');
|
||||||
// TODO: Handle multiple matches for bug/test case. Currently picks one in that case.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +408,7 @@ export default class JiraWritebackReporter implements Reporter {
|
||||||
* @param result
|
* @param result
|
||||||
*/
|
*/
|
||||||
onTestEnd(test: TestCase, result: TestResult) {
|
onTestEnd(test: TestCase, result: TestResult) {
|
||||||
// this.ortoniReport.onTestEnd(test, result);
|
this.ortoniReport.onTestEnd(test, result);
|
||||||
this.loadIssueCalls.push(() => { this.loadIssue(test, result); });
|
this.loadIssueCalls.push(() => { this.loadIssue(test, result); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,28 +418,32 @@ export default class JiraWritebackReporter implements Reporter {
|
||||||
* @returns Promise to write results to Jira.
|
* @returns Promise to write results to Jira.
|
||||||
*/
|
*/
|
||||||
onEnd(result: FullResult): Promise<{ status?: FullResult["status"]; } | undefined | void>|void {
|
onEnd(result: FullResult): Promise<{ status?: FullResult["status"]; } | undefined | void>|void {
|
||||||
// this.ortoniReport.onEnd(result);
|
return this.ortoniReport.onEnd(result).then(async () => {
|
||||||
return this.loadReporterData().then(async () => {
|
await this.loadReporterData().then(async () => {
|
||||||
this.loadIssueCalls.map(fn => fn());
|
this.loadIssueCalls.map(fn => fn());
|
||||||
await this.writeResults();
|
await this.writeResults();
|
||||||
console.log(`JiraWritebackReporter >> Posted necessary changes for '${jiraCardNumber}'`);
|
}).then(async () => {
|
||||||
|
console.log('JiraWritebackReporter >> Uploading Ortoni HTML Report to Jira...');
|
||||||
|
await this.jiraApiUtil.postUploadAttachment(jiraCardNumber, `${reportConfig.folderPath}/${reportConfig.filename}`);
|
||||||
|
console.log('JiraWritebackReporter >> Uploaded Ortoni HTML Report to Jira.');
|
||||||
|
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): Promise<void> {
|
||||||
// onBegin(config: FullConfig, suite: Suite): void {
|
return this.ortoniReport.onBegin(config, suite);
|
||||||
// this.ortoniReport.onBegin(config, suite);
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// onError(error: TestError): void {
|
onError(error: TestError): void {
|
||||||
// this.ortoniReport.onError(error);
|
return this.ortoniReport.onError(error);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// onExit(): Promise<void> {
|
onExit(): Promise<void> {
|
||||||
// return this.ortoniReport.onExit();
|
return this.ortoniReport.onExit();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void {
|
onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void {
|
||||||
// this.ortoniReport.onStdOut(chunk, test, result);
|
return this.ortoniReport.onStdOut(chunk, test, result);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,15 @@ export function formatDate(date: Date) {
|
||||||
return isoString.slice(0, 10);
|
return isoString.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDateForFilename(date: Date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}${month}${day}_${hour}${minute}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatTime(date: Date) {
|
export function formatTime(date: Date) {
|
||||||
return date.toLocaleTimeString('en-US', {
|
return date.toLocaleTimeString('en-US', {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue