qa-shadow-report
Version:
npm package that prints formatted test reports into a google sheet or csv file
372 lines (345 loc) • 12.6 kB
JavaScript
import {
getExistingTabTitlesInRange,
getExistingTabTitlesInWeeklyRange,
getTabIdFromTitle,
getTabIdFromWeeklyTitle,
} from '../google/sheetDataMethods/getSheetInfo.js';
import { auth, sheets, spreadsheetId } from '../google/auth.js';
import { addColumnsAndRowsToTabId } from '../google/sheetDataMethods/processSheetData.js';
import { createSummaryTitle } from '../google/sheetDataMethods/createTabNames.js';
import { constructPayloadForCopyPaste } from '../monthlySummaryMethods/buildSummary.js';
import { constructWeeklyPayloadForCopyPaste } from '../weeklySummaryMethods/buildSummary.js';
import { getLastMonthTabTitles } from '../google/sheetDataMethods/getLastMonthTabTitles.js';
import {
createNewTab,
createNewWeeklyTab,
} from '../google/googleSheetIntegration/createNewTab.js';
import { HEADER_INDICATORS } from '../../constants.js';
import {
getCurrentTime,
getDayIndex,
getFormattedMonth,
getPreviousMonthsYear,
} from './dateFormatting.js';
import {
isSummaryRequired,
isWeeklySummaryRequired,
} from './summaryRequired.js';
import chalk from 'chalk';
import { WEEK_START, DAYS, SHORT_DAYS, MONTHS } from '../../constants.js';
export const getHeaderIndicatorsLength = () => {
return HEADER_INDICATORS.length;
};
/**
* Initializes column metrics object to manage column positions and header lengths during report construction.
*
* @param headerIndicatorsLength
* @returns {object} - An object containing metrics for next available column, default header metrics for destination column,
* longest header end position, and end position for default header metrics destination column.
*/
export const initializeReportColumnMetrics = (headerIndicatorsLength) => {
return {
nextAvailableColumn: 0,
defaultHeaderMetricsDestinationColumn: 0,
longestHeaderEnd: 0,
defaultHeaderMetricsDestinationColumnEnd: headerIndicatorsLength,
};
};
/**
* Send the summary data (payload) to a specified destination sheet tab.
*
* @param {Object} payload - The data to be written to the destination sheet.
* @returns {Promise<void>}
*/
const sendSummaryBody = async (payload) => {
try {
await sheets.spreadsheets.batchUpdate({
auth,
spreadsheetId,
resource: {
requests: [payload],
},
});
} catch (error) {
console.error(`Error writing to project: ${spreadsheetId}`, error);
throw new Error('Failed to send summary body.');
}
};
/**
* Add required number of columns and rows to the specified sheet tab.
*
* @param {string[]} lastMonthSheetTitles - Titles from last month's sheet to calculate the number of columns.
* @param {string[]} headerIndicators - Headers to calculate the number of columns.
* @param {Object[]} payload - Data objects containing information about where data should be pasted in the sheet.
* @param {string} destinationTabTitle - The title of the destination tab in the sheet.
* @returns {Promise<void>}
*/
const addColumnsAndRows = async (
lastMonthSheetTitles,
headerIndicators,
payload,
destinationTabTitle
) => {
try {
const numberOfColumnsNeeded =
lastMonthSheetTitles.length * headerIndicators.length;
const numberOfRowsNeeded = Math.max(
...payload.map((item) => item.copyPaste.destination.endRowIndex)
);
let destinationTabId;
destinationTabId = await getTabIdFromTitle(destinationTabTitle);
if (!destinationTabId) {
destinationTabId = await getTabIdFromWeeklyTitle(destinationTabTitle);
}
await addColumnsAndRowsToTabId(
destinationTabId,
numberOfColumnsNeeded,
numberOfRowsNeeded
);
} catch (error) {
console.error('Error in adding columns and rows:', error);
throw new Error('Unable to add columns and rows to the sheet.');
}
};
/**
* Send headers to a Google Sheets spreadsheet using Google Sheets API.
*
* @param {Array<Object>} payload - An array of objects containing data for batch updating values in the sheet.
* @returns {Promise<void>}
*/
const sendSummaryHeaders = async (payload) => {
try {
await sheets.spreadsheets.values.batchUpdate({
auth,
spreadsheetId,
requestBody: {
valueInputOption: 'RAW',
data: payload,
},
});
} catch (error) {
console.error('An error occurred while sending summary headers:', error);
throw new Error('Failed to send summary headers.');
}
};
/**
* Apply styling to the header of a Google Sheets spreadsheet using a batch update request.
*
* @param {Object} payload - The request payload, specifically containing mergeCells request for defining the range and type of cells to be merged.
* @returns {Promise<void>}
*/
const summaryHeaderStyling = async (payload) => {
try {
await sheets.spreadsheets.batchUpdate({
auth,
spreadsheetId,
requestBody: {
requests: [payload],
},
});
} catch (error) {
console.error('An error occurred while applying header styling:', error);
throw new Error('Failed to apply summary header styling.');
}
};
/**
* Handles the creation and population of a monthly summary report.
*
* @param {Object} options - Configuration options for generating reports.
* @param {boolean} options.csv - If true, outputs in CSV format.
* @param {boolean} options.duplicate - If true, allows creating a duplicate report for the day.
* @param {boolean} options.cypress - If true, parses test result JSON in cypress format.
* @param {boolean} options.playwright - If true, parses test result JSON in playwright format.
*/
export const handleSummary = async ({
csv,
duplicate,
cypress,
playwright,
}) => {
try {
const summaryRequired = await isSummaryRequired({ csv });
const lastMonth = getFormattedMonth('lastMonth');
const noSummaryMessage = chalk.yellow(
`No ${lastMonth} summary required. If you would like to create a duplicate, use the Monthly Summary command directly, with the optional flag ${chalk.green('--duplicate')}, e.g. ${chalk.green('qa-shadow-report monthly-summary --duplicate')}.`
);
if (csv) {
console.warn(
chalk.yellow('CSV format is not supported for summary reports')
);
return;
}
if (!summaryRequired && !duplicate) {
console.info(noSummaryMessage);
return;
}
const existingSheetTitles = await getExistingTabTitlesInRange();
const lastMonthSheetTitles =
await getLastMonthTabTitles(existingSheetTitles);
const summaryTitle = createSummaryTitle();
const currentTime = getCurrentTime();
const summaryPageTitle = duplicate
? `${summaryTitle}_${currentTime}`
: summaryTitle;
await createNewTab(summaryPageTitle);
const fullSummaryPayload = await constructPayloadForCopyPaste(
lastMonthSheetTitles,
summaryPageTitle
);
await addColumnsAndRows(
lastMonthSheetTitles,
HEADER_INDICATORS,
fullSummaryPayload.bodyPayload,
summaryPageTitle
);
await sendSummaryHeaders(fullSummaryPayload.headerPayload);
await sendSummaryBody(fullSummaryPayload.bodyPayload);
await summaryHeaderStyling(fullSummaryPayload.summaryHeaderStylePayload);
console.info(
chalk.green(
`Monthly Summary created for ${getFormattedMonth('lastMonth')} ${getPreviousMonthsYear(getFormattedMonth())}.`
)
);
} catch (error) {
console.error('An error occurred in handleSummary:', error);
throw new Error('Failed to handle summary.');
}
};
/**
* Formats a day name for display (e.g., "Monday" to "Mon").
*
* @param {string} dayName - Day name to format.
* @returns {string} Formatted day name.
*/
function getFormattedDay(dayName) {
return SHORT_DAYS[dayName] || dayName;
}
/**
* Calculates the weekly date range starting from the most recent WEEK_START.
*
* @returns {{ startDate: Date, endDate: Date }} The start and end dates of the week.
*/
function getWeeklyDateRange() {
const now = new Date();
const startDate = new Date(now);
startDate.setDate(
now.getDate() - ((now.getDay() + 7 - getDayIndex(WEEK_START())) % 7)
);
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
endDate.setHours(23, 59, 59, 999);
return { startDate, endDate };
}
/**
* Creates a weekly-specific summary title (e.g., "Weekly Summary Monday Jul 5-12 2025").
*
* @returns {string} Formatted weekly summary title.
*/
export function createWeeklySummaryTitle() {
const { startDate, endDate } = getWeeklyDateRange();
const dayNames = Object.keys(DAYS);
return `Weekly Summary ${dayNames[startDate.getDay()]} ${MONTHS[startDate.getMonth()]} ${startDate.getDate()}-${endDate.getDate()} ${startDate.getFullYear()}`;
}
/**
* Creates a formatted string for the weekly range (e.g., "Mon Jul 3-9").
*
* @returns {string} Formatted weekly range.
*/
function getFormattedWeekRange() {
const { startDate, endDate } = getWeeklyDateRange();
const dayNames = Object.keys(DAYS);
return `${getFormattedDay(dayNames[startDate.getDay()])} ${MONTHS[startDate.getMonth()]} ${startDate.getDate()}-${endDate.getDate()}`;
}
/**
* Handles the creation and population of a weekly summary report.
*
* @param {Object} options - Configuration options for generating reports.
* @param {boolean} options.csv - If true, outputs in CSV format.
* @param {boolean} options.duplicate - If true, allows creating a duplicate report for the week.
* @param {boolean} options.cypress - If true, parses test result JSON in Cypress format.
* @param {boolean} options.playwright - If true, parses test result JSON in Playwright format.
*/
export const handleWeeklySummary = async ({
csv,
duplicate,
cypress,
playwright,
}) => {
try {
if (csv) {
console.warn(
chalk.yellow('CSV format is not supported for weekly summary reports')
);
return;
}
const summaryRequired = await isWeeklySummaryRequired({ csv });
if (!summaryRequired && !duplicate) {
const weekRange = getFormattedWeekRange();
console.info(
chalk.yellow(
`No summary required for week ${weekRange}. If you would like to create a duplicate, use the Weekly Summary command with the ${chalk.green('--duplicate')} flag, e.g., ${chalk.green('qa-shadow-report weekly-summary --duplicate')}.`
)
);
return;
}
const existingSheetTitles = await getExistingTabTitlesInWeeklyRange();
const currentTime = getCurrentTime();
const summaryTitle = createWeeklySummaryTitle();
const summaryPageTitle = duplicate
? `${summaryTitle}_${currentTime}`
: summaryTitle;
await createNewWeeklyTab(summaryPageTitle);
const { startDate, endDate } = getWeeklyDateRange();
const weeklySheetTitles = existingSheetTitles.filter((title) => {
const tabDate = new Date(title);
return tabDate >= startDate && tabDate <= endDate;
});
const fullSummaryPayload = await constructWeeklyPayloadForCopyPaste(
weeklySheetTitles,
summaryPageTitle
);
const updatedWeeklySheetTitles = weeklySheetTitles.map((title) => {
const tabDate = new Date(title);
const dayName = tabDate.toLocaleString('en-US', { weekday: 'short' });
return `${dayName} ${title}`;
});
fullSummaryPayload.headerPayload = fullSummaryPayload.headerPayload.map(
(header) => {
return {
...header,
values: header.values.map((row) =>
row.map((value) => {
const matchingTitle = weeklySheetTitles.find(
(title) => value === title
);
if (matchingTitle) {
const tabDate = new Date(matchingTitle);
const dayName = tabDate.toLocaleString('en-US', {
weekday: 'short',
});
return `${dayName} ${value}`;
}
return value;
})
),
};
}
);
await addColumnsAndRows(
updatedWeeklySheetTitles,
HEADER_INDICATORS,
fullSummaryPayload.bodyPayload,
summaryPageTitle
);
await sendSummaryHeaders(fullSummaryPayload.headerPayload);
await sendSummaryBody(fullSummaryPayload.bodyPayload);
await summaryHeaderStyling(fullSummaryPayload.summaryHeaderStylePayload);
console.info(
chalk.green(`Weekly Summary created for ${getFormattedWeekRange()}.`)
);
} catch (error) {
console.error('An error occurred in handleWeeklySummary:', error);
throw new Error('Failed to handle weekly summary.');
}
};