Skip to content

Commit

Permalink
html output processor
Browse files Browse the repository at this point in the history
  • Loading branch information
bvandercar-vt committed Nov 21, 2024
1 parent 57bccbc commit fbfd3fe
Show file tree
Hide file tree
Showing 28 changed files with 1,189 additions and 29 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ test/cypress/screenshots
test/cypress/videos
test/output/out.json
test/output/out.txt
test/output/not/existing/path/out.txt
test/output/out.html
test/output/out.cst
test/output/not/existing/path/out.txt
test/output_nested
test/cypress/reports
.idea
Expand Down
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
tsconfig.json
tsconfig.json
test/output*/**/*.html
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ setupNodeEvents(on, config) {
outputTarget: {
'out.txt': 'txt',
'out.json': 'json',
'out.html': 'html',
}
};

Expand All @@ -305,7 +306,7 @@ setupNodeEvents(on, config) {
The `outputTarget` needs to be an object where the key is the relative path of the
file from `outputRoot` and the value is the **type** of format to output.

Supported types: `txt`, `json`.
Supported types: `txt`, `json`, `html`.

### Log specs in separate files

Expand Down
3 changes: 1 addition & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
declare namespace Cypress {
import type {Log} from './src/installLogsCollector';
import type {BuiltinOutputProcessorsTypes} from './src/types';

interface Cypress {
TerminalReport: {
getLogs(format: BuiltinOutputProcessorsTypes): string | null;
getLogs(format: 'txt' | 'json'): string | null;
getLogs(format?: 'none' = 'none'): Log[] | null;
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/installLogsCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import LogCollectControlSimple from './collector/LogCollectControlSimple';
import logsTxtFormatter from './outputProcessor/logsTxtFormatter';
import CONSTANTS from './constants';
import type {ExtendedSupportOptions, SupportOptions} from './installLogsCollector.types';
import type {LogType, Log, Severity, BuiltinOutputProcessorsTypes} from './types';
import type {LogType, Log, Severity} from './types';
import utils from './utils';
import {validate} from 'superstruct';
import {InstallLogsCollectorSchema} from './installLogsCollector.schema';
Expand Down Expand Up @@ -75,7 +75,7 @@ function registerLogCollectorTypes(
function registerGlobalApi(logCollectorState: LogCollectorState) {
Cypress.TerminalReport = {
//@ts-ignore there is no error, this works correctly.
getLogs: (format: BuiltinOutputProcessorsTypes | 'none' = 'none') => {
getLogs: (format: 'txt' | 'json' | 'none' = 'none') => {
const logs = logCollectorState.getCurrentLogStack();

if (!logs) {
Expand Down
23 changes: 13 additions & 10 deletions src/installLogsPrinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import utils from './utils';
import consoleProcessor from './outputProcessor/consoleProcessor';
import {validate} from 'superstruct';
import {InstallLogsPrinterSchema} from './installLogsPrinter.schema';
import HtmlOutputProcessor from './outputProcessor/HtmlOutputProcessor';

const OUTPUT_PROCESSOR_TYPE: Record<
BuiltinOutputProcessorsTypes,
{new (file: string, options: PluginOptions): IOutputProcecessor}
> = {
json: JsonOutputProcessor,
txt: TextOutputProcessor,
html: HtmlOutputProcessor,
};

let writeToFileMessages: Record<string, Record<string, Log[]>> = {};
Expand Down Expand Up @@ -56,9 +58,10 @@ function installLogsPrinter(on: Cypress.PluginEvents, options: PluginOptions = {
printLogsToFile,
printLogsToConsole,
outputCompactLogs,
outputTarget,logToFilesOnAfterRun,
outputTarget,
logToFilesOnAfterRun,
includeSuccessfulHookLogs,
compactLogs: compactLogsOption,
compactLogs,
collectTestLogs,
} = resolvedOptions;

Expand All @@ -80,8 +83,8 @@ function installLogsPrinter(on: Cypress.PluginEvents, options: PluginOptions = {
let messages = data.messages;

const terminalMessages =
typeof compactLogsOption === 'number' && compactLogsOption >= 0
? compactLogs(messages, compactLogsOption, logDebug)
typeof compactLogs === 'number' && compactLogs >= 0
? getCompactLogs(messages, compactLogs, logDebug)
: messages;

const isHookAndShouldLog =
Expand All @@ -91,7 +94,7 @@ function installLogsPrinter(on: Cypress.PluginEvents, options: PluginOptions = {
if (data.state === 'failed' || printLogsToFile === 'always' || isHookAndShouldLog) {
let outputFileMessages =
typeof outputCompactLogs === 'number'
? compactLogs(messages, outputCompactLogs, logDebug)
? getCompactLogs(messages, outputCompactLogs, logDebug)
: outputCompactLogs === false
? messages
: terminalMessages;
Expand Down Expand Up @@ -176,8 +179,8 @@ function installOutputProcessors(on: Cypress.PluginEvents, options: PluginOption

const createProcessorFromType = (
file: string,
options: PluginOptions,
type: BuiltinOutputProcessorsTypes | CustomOutputProcessorCallback
type: BuiltinOutputProcessorsTypes | CustomOutputProcessorCallback,
options: PluginOptions
) => {
const filepath = path.join(options.outputRoot || '', file);

Expand Down Expand Up @@ -211,18 +214,18 @@ function installOutputProcessors(on: Cypress.PluginEvents, options: PluginOption
root,
options.specRoot || '',
ext,
(nestedFile: string) => createProcessorFromType(nestedFile, options, type)
(nestedFile: string) => createProcessorFromType(nestedFile, type, options)
)
);
} else {
outputProcessors.push(createProcessorFromType(file, options, type));
outputProcessors.push(createProcessorFromType(file, type, options));
}
});

outputProcessors.forEach((processor) => processor.initialize());
}

function compactLogs(logs: Log[], keepAroundCount: number, logDebug: (message: string) => void) {
function getCompactLogs(logs: Log[], keepAroundCount: number, logDebug: (message: string) => void) {
logDebug(`Compacting ${logs.length} logs.`);

const failingIndexes = logs
Expand Down
195 changes: 195 additions & 0 deletions src/outputProcessor/HtmlOutputProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import type {AllMessages, PluginOptions} from '../installLogsPrinter.types';
import BaseOutputProcessor from './BaseOutputProcessor';
import {Log, LogSymbols, LogType, ValueOf} from '../types';
import CONSTANTS from '../constants';
import utils from '../utils';

const {COLORS: BASE_COLORS, LOG_TYPES, LOG_SYMBOLS} = CONSTANTS;

export const TYPE_PADDING = Math.max(...Object.values(LOG_TYPES).map((l) => l.length)) + 3;

const COLORS = {
...BASE_COLORS,
DARK_CYAN: 'darkcyan',
LIGHT_GREY: 'lightgrey',
} as const;

type Colors = ValueOf<typeof COLORS>;

const MessageConfigMap: Record<
LogType,
(options: PluginOptions) => {
typeColor: Colors;
icon: LogSymbols;
/**
* default is no html color
*/
messageColor?: Colors;
/**
* default is no trim (full message length)
*/
trim?: number;
}
> = {
[LOG_TYPES.PLUGIN_LOG_TYPE]: () => ({typeColor: COLORS.DARK_CYAN, icon: LOG_SYMBOLS.INFO}),
[LOG_TYPES.BROWSER_CONSOLE_WARN]: () => ({typeColor: COLORS.YELLOW, icon: LOG_SYMBOLS.WARNING}),
[LOG_TYPES.BROWSER_CONSOLE_ERROR]: () => ({typeColor: COLORS.RED, icon: LOG_SYMBOLS.ERROR}),
[LOG_TYPES.BROWSER_CONSOLE_DEBUG]: () => ({typeColor: COLORS.BLUE, icon: LOG_SYMBOLS.DEBUG}),
[LOG_TYPES.BROWSER_CONSOLE_INFO]: () => ({typeColor: COLORS.DARK_CYAN, icon: LOG_SYMBOLS.INFO}),
[LOG_TYPES.BROWSER_CONSOLE_LOG]: () => ({typeColor: COLORS.DARK_CYAN, icon: LOG_SYMBOLS.INFO}),
[LOG_TYPES.CYPRESS_LOG]: () => ({typeColor: COLORS.DARK_CYAN, icon: LOG_SYMBOLS.INFO}),
[LOG_TYPES.CYPRESS_XHR]: (options) => ({
typeColor: COLORS.LIGHT_GREY,
icon: LOG_SYMBOLS.ROUTE,
messageColor: COLORS.LIGHT_GREY,
trim: options.routeTrimLength,
}),
[LOG_TYPES.CYPRESS_FETCH]: (options) => ({
typeColor: COLORS.GREEN,
icon: LOG_SYMBOLS.ROUTE,
trim: options.routeTrimLength,
messageColor: COLORS.GREY,
}),
[LOG_TYPES.CYPRESS_INTERCEPT]: (options) => ({
typeColor: COLORS.GREEN,
icon: LOG_SYMBOLS.ROUTE,
messageColor: COLORS.GREY,
trim: options.routeTrimLength,
}),
[LOG_TYPES.CYPRESS_REQUEST]: (options) => ({
typeColor: COLORS.GREEN,
icon: LOG_SYMBOLS.SUCCESS,
messageColor: COLORS.GREY,
trim: options.routeTrimLength,
}),
[LOG_TYPES.CYPRESS_COMMAND]: () => ({
typeColor: COLORS.GREEN,
icon: LOG_SYMBOLS.SUCCESS,
}),
};

// https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript
export function escapeHtml(html: string) {
return html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

/**
* Format an individual Cypress message for HTML logging:
* - Convert cy.log markup syntax to HTML (bold/italic).
* - Color message depending on message type and severity.
* - Trim long messages.
* - Apply proper spacing, newlines, and HTML syntax.
*/
export function formatMessage({type, message, severity}: Log, options: PluginOptions) {
const messageConfig = MessageConfigMap[type](options);

let processedMessage = message;
let {typeColor, icon, messageColor, trim = options.defaultTrimLength} = messageConfig;

if (severity === 'error') {
typeColor = COLORS.RED;
icon = LOG_SYMBOLS.ERROR;
} else if (severity === 'warning') {
typeColor = COLORS.YELLOW;
icon = LOG_SYMBOLS.WARNING;
}

const maybeTrimLength = (msg: string) =>
trim && msg.length > trim
? msg.substring(0, trim) + ' ...\n\n... remainder of log omitted ...\n'
: msg;

const processMessage = (msg: string) => escapeHtml(maybeTrimLength(msg));

if (type == 'cy:log') {
processedMessage = utils.applyMessageMarkdown(processedMessage, {
bold: (str) => `<b>${str}</b>`,
italic: (str) => `<i>${str}</i>`,
processContents: processMessage,
});
} else {
processedMessage = processMessage(processedMessage);
}

// If the message is multilined, align non-first lines with the "message column" that's
// to the right of the "type column"
processedMessage = processedMessage
.split('\n')
.map((line, index) => (index === 0 ? line : `${' '.repeat(TYPE_PADDING + 1)}${line}`))
.join('\n');

processedMessage = `<pre${messageColor ? ` style="color:${messageColor};"` : ''}>${processedMessage}</pre>`;

const typeString = `<pre style="color:${typeColor};">${
// pad to make "type column" spacing even
`${type}${icon}`.padStart(TYPE_PADDING, ' ')
}</pre>`;

return `<p>${typeString} ${processedMessage}</p>\n`;
}

/**
* Custom html output processor intended to be used with
* `cypress-terminal-report`. Generates detailed html log files that log
* every Cypress message in a test-- in other words, log exactly what is
* in the messages window when running `cypress open`)
*
* Example usage in config file:
* ```
* import installTerminalReporter from
* 'cypress-terminal-report/src/installLogsPrinter'
*
* installTerminalReporter(on, {
printLogsToFile: 'always',
outputRoot: config.reporterOptions.resultsDir,
specRoot: config.reporterOptions.testsDir,
outputTarget:()=>({ 'logs|html': detailedLogHtmlOutputProcessor }),
})
```
*/
export default class HtmlOutputProcessor extends BaseOutputProcessor {
private closingContent = `
</body>
</html>`;
private beforeClosingContentPos = -this.closingContent.length;

constructor(
protected file: string,
protected options: PluginOptions
) {
super(file, options);
this.initialContent = `<html>
<head>
<style>
body { font-family: monospace; }
p { margin: 0; padding: 0; }
pre { display: inline; margin: 0; }
h2 { margin: 0; font-size: 1.2em; }
</style>
</head>
<body>
${this.closingContent}`;
}

write(allMessages: AllMessages) {
for (const [spec, tests] of Object.entries(allMessages)) {
let html = `\n<h1>${escapeHtml(spec)}</h1>\n`;

for (const [test, messages] of Object.entries(tests)) {
html += `<h2>${escapeHtml(test)}</h2>\n`;
for (const message of messages) {
html += formatMessage(message, this.options);
}
html += '<br>\n';
}

this.writeSpecChunk(spec, html, this.beforeClosingContentPos);
}
}
}
17 changes: 9 additions & 8 deletions src/outputProcessor/consoleProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,15 @@ const getTypeString = (type: LogType, icon: LogSymbols, color: Colors, padding:
return TYPE_STRING_CACHE[key];
}

const typeString = padType(KNOWN_LOG_TYPES.includes(type) ? type : '[unknown]', padding);
const fullString = typeString + icon + ' ';
const coloredTypeString = BOLD_COLORS.includes(color)
? chalk[color].bold(fullString)
: chalk[color](fullString);

TYPE_STRING_CACHE[key] = coloredTypeString;
return coloredTypeString;
let typeString = padType(KNOWN_LOG_TYPES.includes(type) ? type : '[unknown]', padding);
typeString += icon + ' ';
typeString = chalk[color](typeString);
if (BOLD_COLORS.includes(color)) {
typeString = chalk.bold(typeString);
}

TYPE_STRING_CACHE[key] = typeString;
return typeString;
};

function consoleProcessor(messages: Log[], options: PluginOptions, data: MessageData) {
Expand Down
2 changes: 1 addition & 1 deletion src/outputProcessor/logsTxtFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import CONSTANTS from '../constants';
import type {Log, Severity} from '../types';

const PADDING = ' ';
const PADDING_LOGS = `${PADDING}`.repeat(6);
const PADDING_LOGS = PADDING.repeat(6);
const SEVERITY_ICON = {
[CONSTANTS.SEVERITY.ERROR]: 'X',
[CONSTANTS.SEVERITY.WARNING]: '!',
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type State = ValueOf<typeof CONSTANTS.STATE>;

export type CommandTimings = ValueOf<typeof CONSTANTS.COMMAND_TIMINGS>;

export type BuiltinOutputProcessorsTypes = 'txt' | 'json';
export type BuiltinOutputProcessorsTypes = 'txt' | 'json' | 'html';

// *****************************************************************************
// Objects
Expand Down
2 changes: 2 additions & 0 deletions test/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = defineConfig({
'not/existing/path/out.txt': 'txt',
'out.txt': 'txt',
'out.json': 'json',
'out.html': 'html',
'out.cst': function (allMessages) {
this.initialContent = 'Specs:\n';
this.chunkSeparator = '\n';
Expand All @@ -40,6 +41,7 @@ module.exports = defineConfig({
options.outputTarget = {
'txt|txt': 'txt',
'json|json': 'json',
'html|html': 'html',
'custom|cst': function (allMessages) {
this.initialContent = 'Specs:\n';
this.chunkSeparator = '\n';
Expand Down
Loading

0 comments on commit fbfd3fe

Please sign in to comment.