Article Overview:
This article will cover how you can achieve the following:
- Execute multiple journeys sequentially without relying on Flows.
- Use a custom extension to streamline sequential execution.
Problem Statement:
Executing multiple journeys in sequence typically requires creating separate Flows in Virtuoso. However, this approach may become cumbersome when dealing with a large number of test cases. Customers need an efficient way to execute journeys sequentially without relying on Flows.
Solution:
A custom extension named readAndProcessJourneysData
allows sequential execution of journeys without relying on Flows.
Note: This extension does not pass data between journeys.
Extension Details:
-
Extension Name:
readAndProcessJourneysData
-
Parameters:
-
token
: Authorization token for API calls. -
desiredJourneyName
: Name of the journey to execute. -
env
: Execution environment (PRODUCTION
,US
,DEVELOPMENT
, etc.). -
goalIdInput
: ID of the goal associated with the journey. -
jobIdInput
: ID of the job.
-
-
Resource Required:
Axios Library v1.7.2
How It Works:
- The extension dynamically fetches journey and execution details using Virtuoso APIs.
- It identifies the target journey by matching the
desiredJourneyName
andgoalIdInput
. - Execution steps are retrieved and processed in sequence.
Code:
Res: https://cdnjs.cloudflare.com/ajax/libs/axios/1.7.2/axios.min.js
// Note this extension is not a product feature of Virtuoso and is not officially supported
// Extensions use javascript which may or may not be compatible with systems under test (SUTs)
// We welcome you to use this extension or adapt it for your needs
//INSTRUCTIONS:
/*
Example Usage:
readAndProcessJourneysData($Token, "Master Data", "PRODUCTION", "149022", "3295200") returning $data
*/
/*
Created By : Suhas. YS
Created Date : 22/07/2024
*/
const desiredGoalId = Number(goalIdInput);
const jobId = Number(jobIdInput);
const endpoints = {
PRODUCTION: "https://api.virtuoso.qa/api",
US: "https://api-us.virtuoso.qa/api",
DEVELOPMENT: "https://api-dev.virtuoso.qa/api",
APP2: "https://api-app2.virtuoso.qa/api",
UK: "https://api-uk.virtuoso.qa/api"
};
//const env = "PRODUCTION";
const basicUrl = endpoints[env];
if (!basicUrl) {
throw new Error("Invalid environment " + env + ". Please use PRODUCTION, US, DEVELOPMENT, UK or APP2");
}
//const basicUrl = 'https://api.virtuoso.qa/api';
//const token = '';
if (!desiredGoalId) {
throw new Error('desiredGoalId is missing!');
}
if (!/^\d+$/.test(desiredGoalId)) {
throw new Error('desiredGoalId must contain only digits!');
}
if (!jobId) {
throw new Error('jobId is missing!');
}
if (!/^\d+$/.test(jobId)) {
throw new Error('jobId must contain only digits!');
}
if (!basicUrl) {
throw new Error('basicUrl is missing!');
}
if (!token) {
throw new Error('token is missing!');
}
if (!desiredJourneyName) {
throw new Error('desiredJourneyName is missing!');
}
const options = {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
async function sendRequest(url) {
try {
const response = await axios.get(url, options);
return response.data;
} catch (error) {
console.error('Error in sendRequest:', error.message);
throw error;
}
}
async function fetchAndProcessJourneys() {
try {
// First request to identify the totalCount
const latestStatusUrl = `${basicUrl}/testsuites/latest_status?jobId=${jobId}&includeSequencesDetails=true`;
const latestStatusResponse = await axios.get(latestStatusUrl, options);
let totalCountFound = false;
let journeysWithDataDrivenDetails = [];
const statusMap = latestStatusResponse.data.map;
for (const suiteId in statusMap) {
if (statusMap.hasOwnProperty(suiteId)) {
const journeyData = statusMap[suiteId];
const journey = journeyData.journey;
if (journey) {
const goalId = journey.goalId;
const journeyTitle = journey.title;
console.log(`Processing latest status journey: ${journeyTitle} (Suite ID: ${suiteId}, Goal ID: ${goalId})`);
if (journeyTitle === desiredJourneyName && goalId === desiredGoalId) {
const sequencesDetails = journeyData.lastExecution.sequencesDetails;
if (sequencesDetails && sequencesDetails.totalCount !== undefined) {
const totalCount = sequencesDetails.totalCount;
console.log(`Total count for journey "${journeyTitle}" with goal ID ${goalId}: ${totalCount}`);
totalCountFound = true;
journeysWithDataDrivenDetails.push({ suiteId, totalCount });
} else {
console.log(`totalCount not found for journey "${journeyTitle}" with goal ID ${goalId}`);
}
break;
}
} else {
console.log(`Journey data not found for suite ID: ${suiteId}`);
}
}
}
if (!totalCountFound) {
// Send the first request if totalCount is not found
console.log('Sending request to fetch execution details');
const url = `${basicUrl}/testsuites/execution?jobId=${jobId}`;
const response = await axios.get(url, options);
console.log('API request completed.');
const journeys = response.data.item.journeys;
console.log('Journeys retrieved:', JSON.stringify(journeys, null, 2));
const journeyArray = [];
for (const journeyId in journeys) {
if (journeys.hasOwnProperty(journeyId)) {
const journeyData = journeys[journeyId];
const journey = journeyData.journey;
if (journey) {
const goalId = journey.goalId;
const journeyTitle = journey.title;
console.log(`Processing journey: ${journeyTitle} (ID: ${journeyId}, Goal ID: ${goalId})`);
if (journeyTitle === desiredJourneyName && goalId === desiredGoalId) {
console.log(`Found desired journey: ${journeyTitle}`);
const journeyObject = {
journeyTitle: journeyTitle,
goalId: goalId,
sideEffects: {},
sequence: null
};
const lastExecution = journeyData.lastExecution;
const readJourneyId = lastExecution.report.journeyId;
if (readJourneyId) {
journeyObject.journeyId = readJourneyId;
}
const sequence = lastExecution.execution.sequence;
journeyObject.sequence = sequence;
console.log(`Sequence: ${sequence}`);
if (lastExecution && lastExecution.report && lastExecution.report.checkpoints) {
for (const checkpointId in lastExecution.report.checkpoints) {
if (lastExecution.report.checkpoints.hasOwnProperty(checkpointId)) {
const checkpoint = lastExecution.report.checkpoints[checkpointId];
if (checkpoint.steps) {
for (const stepId in checkpoint.steps) {
if (checkpoint.steps.hasOwnProperty(stepId)) {
const step = checkpoint.steps[stepId];
console.log(`Processing step: ${stepId}`);
if (goalId === desiredGoalId) {
const usedData = step.sideEffects ? step.sideEffects.usedData : {};
if (Object.keys(usedData).length > 0) {
console.log('Found used data:', JSON.stringify(usedData, null, 2));
Object.assign(journeyObject.sideEffects, usedData);
}
}
}
}
}
}
}
}
// Process libraryCheckpointExecutionReports
if (lastExecution.report.libraryCheckpointExecutionReports) {
for (const libCheckpointPosition in lastExecution.report.libraryCheckpointExecutionReports) {
const libCheckpoint = lastExecution.report.libraryCheckpointExecutionReports[libCheckpointPosition];
if (libCheckpoint.steps) {
for (const stepId in libCheckpoint.steps) {
if (libCheckpoint.steps.hasOwnProperty(stepId)) {
const step = libCheckpoint.steps[stepId];
console.log(`Processing library checkpoint step: ${stepId}`);
const usedData = step.sideEffects ? step.sideEffects.usedData : {};
if (Object.keys(usedData).length > 0) {
console.log('Found used data in library checkpoint:', usedData);
Object.assign(journeyObject.sideEffects, usedData);
}
}
}
}
}
}
journeyArray.push(journeyObject);
console.log('Journey object added to array:', JSON.stringify(journeyObject, null, 2));
break;
}
} else {
console.log(`Journey data not found for journey ID: ${journeyId}`);
}
}
}
console.log(`'Final journey array:', ${JSON.stringify(journeyArray, null, 2)}`);
if (journeyArray.length === 0) {
throw new Error(`Desired journey "${desiredJourneyName}" not found when validated with goal Id ${desiredGoalId}`);
}
return (journeyArray);
}
// If totalCount is found, send additional requests
if (totalCountFound) {
const requests = [];
for (const { suiteId, totalCount } of journeysWithDataDrivenDetails) {
for (let i = 1; i <= totalCount; i++) {
const sequenceUrl = `${basicUrl}/testsuites/execution?suiteId=${suiteId}&jobId=${jobId}&sequence=${i}&includeJourneyDetails=true&envelope=false`;
console.log('Sequence URL:', sequenceUrl);
const sequenceResponse = sendRequest(sequenceUrl);
requests.push(sequenceResponse);
}
}
const results = await Promise.all(requests);
const consolidatedData = results.reduce((acc, result) => {
for (let key in result) {
if (result.hasOwnProperty(key)) {
if (!acc[key]) {
acc[key] = [];
}
acc[key] = acc[key].concat(result[key]);
}
}
return acc;
}, {});
console.log('Consolidated Data:', JSON.stringify(consolidatedData, null, 2));
const journeyArray = [];
for (const journeyObj of consolidatedData.journeys) {
const journeyId = Object.keys(journeyObj)[0];
const journeyData = journeyObj[journeyId];
const journey = journeyData.journey;
if (journey && journey.title === desiredJourneyName && journey.goalId === desiredGoalId) {
const journeyObject = {
journeyTitle: journey.title,
goalId: journey.goalId,
sideEffects: {},
sequence: journeyData.lastExecution.execution.sequence
};
for (const checkpointId in journeyData.lastExecution.report.checkpoints) {
if (journeyData.lastExecution.report.checkpoints.hasOwnProperty(checkpointId)) {
const checkpoint = journeyData.lastExecution.report.checkpoints[checkpointId];
for (const stepId in checkpoint.steps) {
if (checkpoint.steps.hasOwnProperty(stepId)) {
const step = checkpoint.steps[stepId];
if (step.sideEffects && step.sideEffects.usedData && Object.keys(step.sideEffects.usedData).length > 0) {
journeyObject.sideEffects = step.sideEffects.usedData;
}
}
}
}
}
// Process libraryCheckpointExecutionReports
if (journeyData.lastExecution.report.libraryCheckpointExecutionReports) {
for (const libCheckpointPosition in journeyData.lastExecution.report.libraryCheckpointExecutionReports) {
const libCheckpoint = journeyData.lastExecution.report.libraryCheckpointExecutionReports[libCheckpointPosition];
if (libCheckpoint.steps) {
for (const stepId in libCheckpoint.steps) {
if (libCheckpoint.steps.hasOwnProperty(stepId)) {
const step = libCheckpoint.steps[stepId];
console.log(`Processing library checkpoint step: ${stepId}`);
const usedData = step.sideEffects ? step.sideEffects.usedData : {};
if (Object.keys(usedData).length > 0) {
console.log('Found used data in library checkpoint:', usedData);
Object.assign(journeyObject.sideEffects, usedData);
}
}
}
}
}
}
journeyArray.push(journeyObject);
}
}
console.log(`Final journey array:', ${JSON.stringify(journeyArray, null, 2)}`);
if (journeyArray.length === 0) {
throw new Error(`Desired journey "${desiredJourneyName}" not found when validated with goal Id ${desiredGoalId}`);
}
return (journeyArray);
}
} catch (error) {
console.error('Error Message:', error.message);
throw new Error('Error Message: ' + error.message);
}
}
fetchAndProcessJourneys().then(done).catch(doneError);
Execution Process:
-
Setup Parameters
Provide the required parameters:-
token
: API authorization token. -
desiredJourneyName
: The journey you want to execute. -
env
: The environment (e.g.,PRODUCTION
). -
goalIdInput
andjobIdInput
: IDs associated with the journey.
-
-
Fetch and Process Journeys
The extension dynamically retrieves journey details, validates them, and executes them sequentially based on the provided inputs. -
Handle Results
Execution results are consolidated and logged for validation.
Limitations:
- Data Sharing: This extension does not support passing data between journeys.
- Support: This is a custom extension and not an officially supported Virtuoso feature.
- Compatibility: JavaScript-based extensions may need adaptation for specific Systems Under Test (SUTs).
Example Use Case:
A user needed to sequentially execute multiple journeys without creating separate Flows. Using the readAndProcessJourneysData
extension, they:
- Automated sequential execution.
- Eliminated the overhead of creating and managing multiple Flows.
Notes:
- The extension is customizable for different use cases.
- Ensure compatibility with your SUT before implementation.
- For advanced support, refer to the attached full code or contact Virtuoso support.
For further assistance, refer to the attached code or contact our support team.
Comments
0 comments
Please sign in to leave a comment.