qa-shadow-report
Version:
npm package that prints formatted test reports into a google sheet or csv file
605 lines (567 loc) • 20.1 kB
JavaScript
import { getTabValuesByTitle } from '../google/googleSheetIntegration/getSheetData.js';
import { copyPasteNormal } from '../google/sheetDataMethods/copyPaste.js';
import {
findMatchingColumnByWeeklyTabId,
findTabTitleDataInArray,
getTabIdFromWeeklyTitle,
getHeaderAndFooterDataByWeeklyTabTitle,
} from '../google/sheetDataMethods/getSheetInfo.js';
import { dataObjects } from '../index.js';
import { numberToLetter } from '../sharedMethods/dataHandler.js';
import {
solidBlackWidthOne,
solidBlackWidthTwo,
} from '../sharedMethods/summaryStyles.js';
import { DEFAULT_HEADER_METRICS, HEADER_INDICATORS } from '../../constants.js';
/**
* Asynchronously fetches values for given tab titles from the previous month.
*
* @param {string[]} titles - An array of tab titles to fetch values for.
* @returns {Promise<object[]>} - A promise that resolves to an array of tab values corresponding to the provided titles.
*/
export const fetchLastWeekTabValues = async (titles) => {
// Define the additional arguments that getTabValuesByTitle expects
const sheetsInstance = '';
const authParam = '';
const spreadsheetIdParam = '';
const data = await Promise.all(
titles.map((title) =>
getTabValuesByTitle(title, sheetsInstance, authParam, spreadsheetIdParam)
)
);
dataObjects.lastWeekSheetValues.push(data);
return data;
};
/**
* Retrieves the column numbers corresponding to the given header indicators.
*
* @param {Array<string>} headerIndicators - List of header indicators to locate in the tab.
* @param {Object} headerFooterData - Data containing header values from the tab.
* @returns {Array<number>} - List of column numbers corresponding to the provided header indicators.
*/
export const getSourceColumnNumbers = (headerIndicators, headerFooterData) => {
return headerIndicators
.map((value) => headerFooterData.headerValues.indexOf(value))
.filter((index) => index !== -1);
};
/**
* Identifies the index of the header row within the provided rows.
* The header row is determined by the presence of all header indicators.
*
* @param {string[][]} rows - A 2D array representing rows of data.
* @returns {number} - The index of the header row. If not found, it returns a value incremented by 1.
*/
export const findHeaderRowIndex = (rows) => {
return (
rows.findIndex((row) =>
HEADER_INDICATORS.every((indicator) => row.includes(indicator))
) + 1
);
};
/**
* Finds the starting column index for header metrics based on the title and default metrics.
*
* @param {string} title - The title to search for.
* @param {Array} defaultHeaderMetrics - Default header metrics to consider.
* @returns {Promise<number>} - Starting column index for header metrics.
*/
export const getHeaderMetricsSourceColumnStart = async (
title,
defaultHeaderMetrics
) => {
return await findMatchingColumnByWeeklyTabId(title, defaultHeaderMetrics);
};
/**
* Calculates the ending column index for header metrics.
*
* @param {number} startColumn - Starting column index.
* @param {number} indicatorsLength - Length of header indicators.
* @returns {number} - Ending column index for header metrics.
*/
export const getHeaderMetricsSourceColumnEnd = (
startColumn,
indicatorsLength
) => {
return startColumn + indicatorsLength;
};
/**
* Retrieves the header row index from the provided data.
*
* @param {Object} headerFooterData - Data containing header row details.
* @returns {number} - Header row index.
*/
export const getAdjustedHeaderRowIndex = (headerFooterData) => {
return headerFooterData.headerRow - 1;
};
/**
* Finds the maximum header index within a series of source tabs.
*
* @param {string[]} sourceTabTitles - The titles of the source tabs.
* @param {Array} weeklyTabValues - The tab values from the last week.
* @param {Object} columnMetrics - The current metrics for columns.
* @returns {Promise<Object>} - The updated column metrics.
* @throws {Error} - If data for any source tab title cannot be found or is invalid.
*/
export const findLongestHeaderWithinWeeklySeries = async (
sourceTabTitles,
weeklyTabValues,
columnMetrics
) => {
let updatedColumnMetrics = { ...columnMetrics };
try {
for (const sourceTabTitle of sourceTabTitles) {
const sourceData = await findTabTitleDataInArray(
weeklyTabValues,
sourceTabTitle
);
if (
!sourceData ||
!sourceData.data ||
!Array.isArray(sourceData.data.values)
) {
throw new Error(
`Data for source tab title "${sourceTabTitle}" could not be found or is invalid.`
);
}
const headerRowIndex = findHeaderRowIndex(sourceData.data.values);
if (typeof headerRowIndex !== 'number') {
throw new Error(
`findHeaderRowIndex did not return a numeric value for title "${sourceTabTitle}".`
);
}
const headerRowActual = headerRowIndex + 1;
updatedColumnMetrics.longestHeaderEnd = Math.max(
updatedColumnMetrics.longestHeaderEnd || 0,
headerRowActual
);
}
return updatedColumnMetrics.longestHeaderEnd;
} catch (error) {
console.error('Failed to find longest header within series:', error);
throw error;
}
};
/**
* Initializes an object to hold various payload categories required for report construction.
*
* @returns {object} - An object with empty arrays for body payload, header payload, summary header style payload, and summary grid styles.
*/
export const initializeWeeklyReportPayload = () => ({
bodyPayload: [],
headerPayload: [],
summaryHeaderStylePayload: [],
summaryGridStyles: [],
});
/**
* Processes source columns and populates the summary payload for further actions.
* This function iterates over each header indicator, identifies relevant columns,
* and prepares configuration payloads for copy-pasting operations.
*
* @param {Object} headerFooterData - Data regarding the header and footer positions of the tab.
* @param {string} title - Title of the tab being processed.
* @param {number} sourcePageId - ID of the source tab.
* @param {number} destinationTabId - ID of the destination tab.
* @param {Object} summaryPayload - Aggregated payload object that accumulates configurations.
* @param {Object} columnMetrics - Metrics associated with columns being processed.
* @param {string} destinationTabTitle - Title of the destination tab.
* @param {number} headerIndicatorsLength - Length of header indicators to process.
*/
const processSourceColumns = async (
headerFooterData,
title,
sourcePageId,
destinationTabId,
summaryPayload,
columnMetrics,
destinationTabTitle,
headerIndicatorsLength
) => {
const lightGrey = { red: 0.9, green: 0.9, blue: 0.9 }; // Light grey color (e.g., #D9D9D9)
const headerAndFooterRowFormat = {
userEnteredFormat: {
textFormat: { bold: true, fontSize: 12 },
},
};
const sourceColumnNumbers = getSourceColumnNumbers(
HEADER_INDICATORS,
headerFooterData
);
const headerMetricsSourceColumnStart =
await getHeaderMetricsSourceColumnStart(title, DEFAULT_HEADER_METRICS);
const headerMetricsSourceColumnEnd = getHeaderMetricsSourceColumnEnd(
headerMetricsSourceColumnStart,
headerIndicatorsLength
);
const headerRowIndex = getAdjustedHeaderRowIndex(headerFooterData);
/**
* Calculates the ending row index for destination.
*
* @param {Object} columnMetrics - Metrics containing details about columns.
* @param {Object} headerFooterData - Data containing footer row details.
* @returns {number} - Ending row index for destination.
*/
const destinationEndRowIndex =
headerFooterData.footerRow - (headerRowIndex - 4);
/**
* Calculates the ending column for a given source column.
*
* @param {number} srcColNumber - Source column number.
* @returns {number} - Ending column number.
*/
const calculateSourceEndColumn = (srcColNumber) => {
return srcColNumber + 1;
};
/**
* Computes the ending column for the destination.
*
* @param {Object} columnMetrics - Metrics containing details about columns.
* @returns {number} - Ending column for destination.
*/
const calculateDestinationEndColumn = (columnMetrics) => {
return columnMetrics.nextAvailableColumn + 1;
};
/**
* Constructs the body source parameters.
*
* @param {Object} details - An object containing necessary details.
* @param details.sourcePageId
* @param details.headerRowIndex
* @param details.footerRow
* @param details.srcColNumber
* @param details.srcEndCol
* @returns {Object} - Body source parameters.
*/
const buildBodySourceParams = ({
sourcePageId,
headerRowIndex,
footerRow,
srcColNumber,
srcEndCol,
}) => ({
sourcePageId,
startRow: headerRowIndex,
endRow: footerRow,
startCol: srcColNumber,
endCol: srcEndCol,
});
/**
* Constructs the body destination parameters.
*
* @param {Object} details - An object containing necessary details.
* @param details.destinationTabId
* @param details.startRowIndex
* @param details.endRowIndex
* @param details.nextAvailableColumn
* @param details.endColumn
* @returns {Object} - Body destination parameters.
*/
const buildBodyDestinationParams = ({
destinationTabId,
startRowIndex,
endRowIndex,
nextAvailableColumn,
endColumn,
}) => ({
destinationTabId,
startRow: startRowIndex,
endRow: endRowIndex,
startCol: nextAvailableColumn,
endCol: endColumn,
});
sourceColumnNumbers.forEach((srcColNumber) => {
const srcEndCol = calculateSourceEndColumn(srcColNumber);
const destinationEndColumn = calculateDestinationEndColumn(columnMetrics);
const bodySourceParams = buildBodySourceParams({
sourcePageId,
headerRowIndex,
footerRow: headerFooterData.footerRow,
srcColNumber,
srcEndCol,
});
const bodyDestinationParams = buildBodyDestinationParams({
destinationTabId: destinationTabId,
startRowIndex: 5,
endRowIndex: destinationEndRowIndex,
nextAvailableColumn: columnMetrics.nextAvailableColumn,
endColumn: destinationEndColumn,
});
const copyPasteData = copyPasteNormal(
bodySourceParams,
bodyDestinationParams
);
summaryPayload.bodyPayload.push(copyPasteData);
columnMetrics.nextAvailableColumn++;
});
/**
* Constructs header metric source parameters.
*
* @param {Object} config - Configuration object with required properties.
* @param config.sourcePageId
* @param config.startRow
* @param config.startColumn
* @param config.endColumn
* @returns {Object} - Header metric source parameters.
*/
const buildHeaderMetricSourceParams = ({
sourcePageId,
startRow = 0,
startColumn,
endColumn,
}) => ({
sourcePageId,
startRow,
endRow: 4,
startCol: startColumn,
endCol: endColumn,
});
const headerMetricSourceParams = buildHeaderMetricSourceParams({
sourcePageId,
headerRowIndex,
startColumn: headerMetricsSourceColumnStart,
endColumn: headerMetricsSourceColumnEnd,
});
/**
* Calculates the start row for the header metric destination based on
* the longest header and the header row data.
* @constant
* @type {number}
*/
const headerMetricDestinationStartRow = 1;
/**
* Represents the parameters for the header metric destination.
* @constant
* @type {object}
* @property {number|string} destinationTabId - The ID of the destination tab.
* @property {number} startRow - The starting row index for the header metric destination.
* @property {number} endRow - The ending row index for the header metric destination.
* @property {number} startCol - The starting column index for the header metric destination.
* @property {number} endCol - The ending column index for the header metric destination.
*/
const headerMetricDestinationParams = {
destinationTabId,
startRow: headerMetricDestinationStartRow,
endRow: 4,
startCol: columnMetrics.defaultHeaderMetricsDestinationColumn,
endCol: columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
};
/**
* Utilizes the copyPasteNormal function to generate data for copying and pasting
* from the source parameters to the destination parameters for header metrics.
* @constant
* @type {object}
*/
const copyPasteData = copyPasteNormal(
headerMetricSourceParams,
headerMetricDestinationParams
);
/**
* Adds the generated copyPasteData to the body payload of the summary.
*/
summaryPayload.bodyPayload.push(copyPasteData);
/**
* Converts the numeric value of the default header metrics destination column
* to its corresponding alphabetical column representation (e.g., 1 -> A, 2 -> B).
* @constant
* @type {string}
*/
const column = numberToLetter(
columnMetrics.defaultHeaderMetricsDestinationColumn
);
/**
* Pushes the header data into the header payload of the summary.
* Constructs a range using the destination tab title and the computed column
* to specify the location in the destination where the title will be placed.
*/
summaryPayload.headerPayload.push({
range: `${destinationTabTitle}!${column}1`,
values: [[title]],
});
/**
* Define starting and ending row positions for summary header.
* `summaryHeaderRowStart` represents the starting row index for the summary header.
* `summaryHeaderRowEnd` represents the ending row index for the summary header.
*/
const summaryHeaderRowStart = 0;
const summaryHeaderRowEnd = 1;
/**
* Adds a 'merge cells' style action to the summary payload.
* This action aims to merge a specific range of cells in the header
* of the summary tab for visual emphasis and clarity.
*/
summaryPayload.summaryHeaderStylePayload.push(
{
mergeCells: {
range: {
sheetId: destinationTabId,
startRowIndex: summaryHeaderRowStart,
endRowIndex: summaryHeaderRowEnd,
startColumnIndex: columnMetrics.defaultHeaderMetricsDestinationColumn,
endColumnIndex:
columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
},
mergeType: 'MERGE_ALL',
},
},
{
mergeCells: {
range: {
sheetId: destinationTabId,
startRowIndex: destinationEndRowIndex,
endRowIndex: destinationEndRowIndex + 1,
startColumnIndex: columnMetrics.defaultHeaderMetricsDestinationColumn,
endColumnIndex:
columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
},
mergeType: 'MERGE_ALL',
},
},
{
repeatCell: {
range: {
sheetId: destinationTabId,
startRowIndex: summaryHeaderRowStart,
endRowIndex: summaryHeaderRowEnd,
startColumnIndex: columnMetrics.defaultHeaderMetricsDestinationColumn,
endColumnIndex:
columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
},
cell: {
userEnteredFormat: {
horizontalAlignment: 'CENTER',
},
},
fields: 'userEnteredFormat.horizontalAlignment',
},
},
{
updateBorders: {
range: {
sheetId: destinationTabId,
startRowIndex: summaryHeaderRowStart,
endRowIndex: summaryHeaderRowEnd,
startColumnIndex: columnMetrics.defaultHeaderMetricsDestinationColumn,
endColumnIndex:
columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
},
top: solidBlackWidthTwo,
bottom: solidBlackWidthTwo,
left: solidBlackWidthOne,
right: solidBlackWidthOne,
innerHorizontal: solidBlackWidthTwo,
innerVertical: solidBlackWidthOne,
},
},
{
updateBorders: {
range: {
sheetId: destinationTabId,
startRowIndex: destinationEndRowIndex,
endRowIndex: destinationEndRowIndex + 1,
startColumnIndex: columnMetrics.defaultHeaderMetricsDestinationColumn,
endColumnIndex:
columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
},
top: solidBlackWidthTwo,
bottom: solidBlackWidthTwo,
left: solidBlackWidthOne,
right: solidBlackWidthOne,
innerHorizontal: solidBlackWidthTwo,
innerVertical: solidBlackWidthOne,
},
},
{
// Apply dark grey color to the header row
repeatCell: {
range: {
sheetId: destinationTabId,
startRowIndex: summaryHeaderRowStart,
endRowIndex: summaryHeaderRowEnd,
startColumnIndex: columnMetrics.defaultHeaderMetricsDestinationColumn,
endColumnIndex:
columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
},
cell: {
userEnteredFormat: {
backgroundColor: lightGrey,
},
},
fields: 'userEnteredFormat.backgroundColor',
},
},
{
// Apply bold and fontSize 12 to header row
repeatCell: {
range: {
sheetId: destinationTabId,
startRowIndex: summaryHeaderRowStart,
endRowIndex: summaryHeaderRowEnd,
startColumnIndex: columnMetrics.defaultHeaderMetricsDestinationColumn,
endColumnIndex:
columnMetrics.defaultHeaderMetricsDestinationColumnEnd,
},
cell: headerAndFooterRowFormat,
fields: 'userEnteredFormat.textFormat',
},
}
);
/**
* Adjusts the starting column index for the default header metrics destination.
* This is typically required when there are additional header indicators or
* columns introduced which shift the position of the header metrics.
*
* @param {number} length - The number of additional header indicators/columns.
*/
const adjustDefaultHeaderMetricsDestinationStart = (length) => {
columnMetrics.defaultHeaderMetricsDestinationColumn += length;
};
/**
* Adjusts the ending column index for the default header metrics destination.
* Like the start, the end may need adjustment when new columns or indicators
* are introduced which affect the ending position of the header metrics.
*
* @param {number} length - The number of additional header indicators/columns.
*/
const adjustDefaultHeaderMetricsDestinationEnd = (length) => {
columnMetrics.defaultHeaderMetricsDestinationColumnEnd += length;
};
adjustDefaultHeaderMetricsDestinationStart(headerIndicatorsLength);
adjustDefaultHeaderMetricsDestinationEnd(headerIndicatorsLength);
};
/**
* Processes source tab titles by fetching associated tab data and processing
* its source columns. Each title corresponds to a tab whose data needs to be
* processed and possibly merged/copied to a destination tab.
*
* @param {string[]} titles - An array of source tab titles to process.
* @param {number} destinationTabId - The ID of the destination tab.
* @param {Object} summaryPayload - An object to accumulate processing results.
* @param {Object} columnMetrics - Metrics related to columns in the source and destination tabs.
* @param {string} destinationTabTitle - Title of the destination tab.
* @param {number} headerIndicatorsLength - Number of header indicators/columns to adjust columns by.
*
* @returns {Promise<void>}
*/
export const processWeeklySourceTabTitles = async (
titles,
destinationTabId,
summaryPayload,
columnMetrics,
destinationTabTitle,
headerIndicatorsLength
) => {
for (let title of titles) {
const headerFooterData =
await getHeaderAndFooterDataByWeeklyTabTitle(title);
const sourcePageId = await getTabIdFromWeeklyTitle(title);
await processSourceColumns(
headerFooterData,
title,
sourcePageId,
destinationTabId,
summaryPayload,
columnMetrics,
destinationTabTitle,
headerIndicatorsLength
);
}
};