qa-shadow-report
Version:
npm package that prints formatted test reports into a google sheet or csv file
452 lines (408 loc) • 15.5 kB
JavaScript
import {
FORMULA_KEYS,
FORMULA_TEMPLATES,
TEST_CATEGORIES_AVAILABLE,
TEST_TYPES_AVAILABLE,
} from '../../../constants.js';
/**
* Generates placeholder entries for report construction.
*
* @param {number} count - Number of placeholders needed. Must be a non-negative integer.
* @returns {string[][]} - A 2D array containing placeholders.
* @throws {TypeError} If the count is not a number or a negative number.
*/
export const generatePlaceholders = (count) => {
if (typeof count !== 'number') {
throw new TypeError('The count must be a number.');
}
if (!Number.isInteger(count)) {
throw new TypeError('The count must be an integer.');
}
if (count < 0) {
throw new TypeError('The count must be a non-negative integer.');
}
return new Array(count).fill(['', '']);
};
/**
* Creates and returns a report entry consisting of a title and a formula.
*
* @param {string} title - The title for the report entry.
* @param {string} formula - The formula for the report entry.
* @returns {Array<string>} - An array consisting of the title and formula.
* @throws {TypeError} If either title or formula is not a string.
* @throws {Error} If either title or formula is an empty string.
*/
export const generateReportEntry = (title, formula) => {
if (typeof title !== 'string') {
throw new TypeError('Title must be a string.');
}
if (title.trim() === '') {
throw new Error('Title cannot be an empty string.');
}
if (typeof formula !== 'string') {
throw new TypeError('Formula must be a string.');
}
if (formula.trim() === '') {
throw new Error('Formula cannot be an empty string.');
}
return [title, formula];
};
/**
* Iterates through metrics headers and generates a report entry for each.
* Adjustments might be required for compatibility with Excel/Google Sheets formulas.
*
* @param {Array<string>} defaultHeaderMetrics - The array of header metric strings.
* @param {number} index - The index of the type within the defaultHeaderMetrics array.
* @returns {Array<Array<string>>} - An array of report entry arrays.
* @throws {TypeError} If defaultHeaderMetrics is not an array of strings or index is not a number.
* @throws {RangeError} If index is out of bounds for defaultHeaderMetrics or type is not found within defaultHeaderMetrics.
*/
export const generateStateReports = (defaultHeaderMetrics, index) => {
if (!Array.isArray(defaultHeaderMetrics)) {
throw new TypeError('defaultHeaderMetrics must be an array of strings.');
}
if (!defaultHeaderMetrics.every((metric) => typeof metric === 'string')) {
throw new TypeError('Each item in defaultHeaderMetrics must be a string.');
}
if (typeof index !== 'number' || !Number.isInteger(index)) {
throw new TypeError('index must be an integer.');
}
if (index < 0 || index >= defaultHeaderMetrics.length) {
throw new RangeError('index is out of bounds for defaultHeaderMetrics.');
}
const type = defaultHeaderMetrics[index]
.slice(2)
.replace(' tests', '')
.trim();
if (typeof type !== 'string' || type === '') {
throw new TypeError('The extracted type must be a non-empty string.');
}
const reports = defaultHeaderMetrics.map((metric) => {
const adjustedMetric = metric.replace('# ', '');
let formula;
switch (type) {
case 'skipped/pending':
formula = `${adjustedMetric} formula skipped/pending`;
break;
case 'total':
formula = `${adjustedMetric} formula total`;
break;
default:
formula = `${type} formula base`;
}
return [`# ${adjustedMetric}`, formula];
});
return reports;
};
/**
* Generates a report entry that contains metrics about passed tests for a team.
*
* @param {string} type - The type of test or team name.
* @returns {Array<string>} - A report entry array.
* @throws {TypeError} If the type is not a string or is empty.
*/
export const generateTeamReport = (type) => {
if (typeof type !== 'string' || type.trim() === '') {
throw new TypeError('Type must be a non-empty string.');
}
const title = `# ${type} tests passed`;
const formula = `${type} formula tests passed`;
return generateReportEntry(title, formula);
};
/**
* Generates two report entries that contain metrics about total and passed tests.
*
* @param {string} type - The type of test.
* @returns {Array<Array<string>>} - An array of report entry arrays.
* @throws {TypeError} If the type is not a string or is empty.
*/
export const generateTypeReport = (type) => {
if (typeof type !== 'string' || type.trim() === '') {
throw new TypeError('Type must be a non-empty string.');
}
const passedTestsTitle = `# ${type} tests passed`;
const passedTestsFormula = `${type} formula tests passed`;
const passedTests = generateReportEntry(passedTestsTitle, passedTestsFormula);
return [passedTests];
};
/**
* Asynchronously generates and returns a report from the payload based on types.
*
* @param {string[]} types - An array of type strings to generate the report.
* @param {any[]} payload - The data payload to generate the report from.
* @param {number} searchIndex - Index at which to search for types in the payload.
* @param {boolean} [isTeam=false] - Flag to indicate if the current report is for a team.
* @returns {string[][]} - A report in the form of a 2D string array.
* @throws {Error} If inputs do not meet required conditions.
*/
export const generateReport = (types, payload, searchIndex, isTeam = false) => {
if (!Array.isArray(types)) {
throw new TypeError('Types must be an array.');
}
if (!types.every((type) => typeof type === 'string')) {
throw new TypeError('All types must be strings.');
}
if (!Array.isArray(payload)) {
throw new TypeError('Payload must be an array.');
}
if (
typeof searchIndex !== 'number' ||
!Number.isInteger(searchIndex) ||
searchIndex < 0
) {
throw new TypeError('SearchIndex must be a non-negative integer.');
}
if (typeof isTeam !== 'boolean') {
throw new TypeError('isTeam must be a boolean.');
}
const report = [];
for (const type of types) {
const typeOccurrences = payload.filter(
(item) =>
item[searchIndex] &&
typeof item[searchIndex] === 'string' &&
item[searchIndex].includes(type)
).length;
if (typeOccurrences > 0) {
const reportEntries = isTeam
? [generateTeamReport(type)]
: generateTypeReport(type);
report.push(...reportEntries);
}
}
return report;
};
/**
* A function that builds specific formulas based on the input type,
* header row index, total number of rows, the number of body rows, and column references.
*
* @function buildFormulas
* @param {string} type - Type used in formula placeholders.
* @param {number} headerRowIndex - Index of the header row.
* @param {number} totalNumberOfRows - Total number of rows including header.
* @param {number} bodyRowCount - Count of the body rows, excluding header.
* @param {string} subjectColumn - Column letter(s) of the subject data.
* @param constants
* @param {string} stateColumn - Column letter(s) of the state data.
* @returns {Object} An object containing formulas as key-value pairs.
* @throws {Error} If arguments do not meet the required conditions.
*/
export const buildFormulas = (
type,
headerRowIndex,
totalNumberOfRows,
bodyRowCount,
subjectColumn,
stateColumn,
constants = { FORMULA_TEMPLATES, FORMULA_KEYS }
) => {
if (typeof type !== 'string') {
throw new Error('Type must be a string.');
}
if (!Number.isInteger(headerRowIndex) || headerRowIndex <= 0) {
throw new Error('Header row index must be a positive integer.');
}
if (
!Number.isInteger(totalNumberOfRows) ||
totalNumberOfRows <= headerRowIndex
) {
throw new Error(
'Total number of rows must be an integer greater than header row index.'
);
}
if (!Number.isInteger(bodyRowCount) || bodyRowCount <= 0) {
throw new Error('Body row count must be a positive integer.');
}
if (typeof subjectColumn !== 'string' || !subjectColumn.trim()) {
throw new Error('Subject column must be a non-empty string.');
}
if (typeof stateColumn !== 'string' || !stateColumn.trim()) {
throw new Error('State column must be a non-empty string.');
}
if (
!Array.isArray(constants.FORMULA_TEMPLATES) ||
constants.FORMULA_TEMPLATES.length === 0
) {
throw new Error('FORMULA_TEMPLATES must be a non-empty array.');
}
if (
!Array.isArray(constants.FORMULA_KEYS) ||
constants.FORMULA_KEYS.length === 0
) {
throw new Error('FORMULA_KEYS must be a non-empty array.');
}
if (constants.FORMULA_TEMPLATES.length !== constants.FORMULA_KEYS.length) {
throw new Error(
'FORMULA_TEMPLATES and FORMULA_KEYS arrays must have the same length.'
);
}
// Function to replace placeholders with actual values
const fillTemplate = (template) =>
template
.replace(/{type}/g, type)
.replace(/{headerRowIndex}/g, headerRowIndex)
.replace(/{totalNumberOfRows}/g, totalNumberOfRows)
.replace(/{bodyRowCount}/g, bodyRowCount)
.replace(/{subjectColumn}/g, subjectColumn)
.replace(/{stateColumn}/g, stateColumn);
const templates = constants.FORMULA_TEMPLATES;
const keys = constants.FORMULA_KEYS;
const filledFormulas = templates.map(fillTemplate);
const combinedObject = keys.reduce((obj, key, index) => {
obj[key] = filledFormulas[index];
return obj;
}, {});
return combinedObject;
};
/**
* Helper function to create a regex pattern from an array of formula keys.
* @param {Function} getKeysPattern - Function to retrieve the keys pattern.
* @returns {RegExp} A RegExp to match formula keys in a string.
* @throws {Error} If the keys pattern is not a valid non-empty string.
*/
export const constructHeaderRegex = (getKeysPattern) => {
const keysPattern = getKeysPattern();
if (typeof keysPattern !== 'string' || !keysPattern.trim()) {
throw new Error('The keys pattern must be a non-empty string.');
}
return new RegExp(`(.+) (${keysPattern})$`);
};
/**
* Determines the column letter based on a descriptive type.
* @param {string} type - The type of the column to determine.
* @param {Array<string>} columnsAvailable - Array of available columns.
* @param {Function} numberToLetter - Function to convert a number to a letter.
* @param {Object} constants - An object containing the functions to get available categories and types.
* @param {Function} constants.TEST_CATEGORIES_AVAILABLE - Function to get available categories.
* @param {Function} constants.TEST_TYPES_AVAILABLE - Function to get available types.
* @returns {string} The letter representing the column in a spreadsheet.
* @throws {Error} If the type is not a string or columnsAvailable is not an array.
*/
export const determineSubjectColumn = (
type,
columnsAvailable,
numberToLetter,
constants = { TEST_CATEGORIES_AVAILABLE, TEST_TYPES_AVAILABLE }
) => {
if (typeof type !== 'string') {
throw new Error('Type must be a string.');
}
if (!Array.isArray(columnsAvailable)) {
throw new Error('columnsAvailable must be an array.');
}
if (typeof numberToLetter !== 'function') {
throw new Error('numberToLetter must be a function.');
}
if (
typeof constants.TEST_CATEGORIES_AVAILABLE !== 'function' ||
typeof constants.TEST_TYPES_AVAILABLE !== 'function'
) {
throw new Error(
'Constants must include TEST_CATEGORIES_AVAILABLE and TEST_TYPES_AVAILABLE functions.'
);
}
const typesAvailable = constants.TEST_TYPES_AVAILABLE();
const categoriesAvailable = constants.TEST_CATEGORIES_AVAILABLE();
if (
!Array.isArray(typesAvailable) ||
!typesAvailable.every((t) => typeof t === 'string')
) {
throw new Error('TEST_TYPES_AVAILABLE must return an array of strings.');
}
if (
!Array.isArray(categoriesAvailable) ||
!categoriesAvailable.every((c) => typeof c === 'string')
) {
throw new Error(
'TEST_CATEGORIES_AVAILABLE must return an array of strings.'
);
}
if (categoriesAvailable.includes(type)) {
return numberToLetter(columnsAvailable.indexOf('category'));
} else if (typesAvailable.includes(type)) {
return numberToLetter(columnsAvailable.indexOf('type'));
}
return numberToLetter(columnsAvailable.indexOf('team'));
};
/**
* Joins formula keys into a regex pattern for matching in strings.
* @param {Object} constants - An object containing the FORMULA_KEYS array.
* @param {Array<string>} constants.FORMULA_KEYS - An array of formula keys.
* @returns {string} A string pattern to use in regex matching.
* @throws {Error} If FORMULA_KEYS is not a valid array of strings.
*/
export const getKeysPattern = (constants = { FORMULA_KEYS }) => {
if (!Array.isArray(constants.FORMULA_KEYS)) {
throw new Error('FORMULA_KEYS must be an array.');
}
if (!constants.FORMULA_KEYS.every((key) => typeof key === 'string')) {
throw new Error('FORMULA_KEYS must be an array of strings.');
}
return constants.FORMULA_KEYS.map((key) => `(${key})`).join('|');
};
/**
* Creates a payload for batch updating merges in Google Sheets.
* Assumes that the array is the data of a Google Sheet starting from the first row and first column.
* @param {Array<Array<string>>} data - The 2D array representing the sheet data.
* @param {number} headerRowIndex - The index of the header row in the sheet.
* @param {number} sheetId - The ID of the sheet where merges are to be applied.
* @returns {Object} An object representing the payload for a batch update request to the Google Sheets API.
* @throws {Error} If the inputs are not valid.
*/
export const createMergeQueries = (data, headerRowIndex, sheetId) => {
if (
!Array.isArray(data) ||
!data.every(
(row) =>
Array.isArray(row) && row.every((cell) => typeof cell === 'string')
)
) {
throw new Error('Data must be a 2D array of strings.');
}
if (!Number.isInteger(headerRowIndex) || headerRowIndex < 0) {
throw new Error('Header row index must be a non-negative integer.');
}
if (!Number.isInteger(sheetId) || sheetId < 0) {
throw new Error('Sheet ID must be a non-negative integer.');
}
const createMergeQuery = (start, end, startColumn, endColumn) => ({
sheetId,
startRowIndex: start,
endRowIndex: end,
startColumnIndex: startColumn,
endColumnIndex: endColumn,
});
const processColumn = (columnIndex) => {
const merges = [];
let startMergeIndex = headerRowIndex;
for (let i = 0; i < data.length; i++) {
const isLastRow = i === data.length - 1;
const isDifferent =
!isLastRow && data[i][columnIndex] !== data[i + 1][columnIndex];
if (isLastRow || isDifferent) {
if (startMergeIndex < headerRowIndex + i) {
merges.push(
createMergeQuery(
startMergeIndex,
headerRowIndex + i + 1,
columnIndex,
columnIndex + 1
)
);
}
startMergeIndex = headerRowIndex + i + 1;
}
}
return merges;
};
const merges = [...processColumn(0), ...processColumn(1)];
const requestBody = {
requests: merges.map((merge) => ({
mergeCells: {
range: merge,
mergeType: 'MERGE_ALL',
},
})),
};
return requestBody;
};