Test improvements: - Capture titles for all macOS windows, not just the frontmost. - Incorporate missing application log files. - Improve log clarity with enriched context. - Improve application termination on macOS by reducing grace period. - Ensure complete application termination on macOS. - Validate Vue application loading through an initial log. - Support ignoring environment-specific `stderr` errors. - Do not fail the test if working directory cannot be deleted. - Use retry pattern when installing dependencies due to network errors. Refactorings: - Migrate the test code to TypeScript. - Replace deprecated `rmdir` with `rm` for error-resistant directory removal. - Improve sanity checking by shifting from App.vue to Vue bootstrapper. - Centralize environment variable management with `EnvironmentVariables` construct. - Rename infrastructure/Environment to RuntimeEnvironment for clarity. - Isolate WindowVariables and SystemOperations from RuntimeEnvironment. - Inject logging via preloader. - Correct mislabeled RuntimeSanity tests. Configuration: - Introduce `npm run check:desktop` for simplified execution. - Omit `console.log` override due to `nodeIntegration` restrictions and reveal logging functionality using context-bridging.
182 lines
5.3 KiB
TypeScript
182 lines
5.3 KiB
TypeScript
import { splitTextIntoLines, indentText } from '../utils/text';
|
|
import { log, die } from '../utils/log';
|
|
import { readAppLogFile } from './app-logs';
|
|
import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns';
|
|
|
|
const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes
|
|
const LOG_ERROR_MARKER = '[error]'; // from electron-log
|
|
const EXPECTED_LOG_MARKERS = [
|
|
'[WINDOW_INIT]',
|
|
'[PRELOAD_INIT]',
|
|
'[APP_INIT]',
|
|
];
|
|
|
|
type ProcessType = 'main' | 'renderer';
|
|
|
|
export async function checkForErrors(
|
|
stderr: string,
|
|
windowTitles: readonly string[],
|
|
projectDir: string,
|
|
) {
|
|
if (!projectDir) { throw new Error('missing project directory'); }
|
|
const errors = await gatherErrors(stderr, windowTitles, projectDir);
|
|
if (errors.length) {
|
|
die(formatErrors(errors));
|
|
}
|
|
}
|
|
|
|
async function gatherErrors(
|
|
stderr: string,
|
|
windowTitles: readonly string[],
|
|
projectDir: string,
|
|
): Promise<ExecutionError[]> {
|
|
if (!projectDir) { throw new Error('missing project directory'); }
|
|
const { logFileContent: mainLogs, logFilePath: mainLogFile } = await readAppLogFile(projectDir, 'main');
|
|
const { logFileContent: rendererLogs, logFilePath: rendererLogFile } = await readAppLogFile(projectDir, 'renderer');
|
|
const allLogs = [mainLogs, rendererLogs, stderr].filter(Boolean).join('\n');
|
|
return [
|
|
verifyStdErr(stderr),
|
|
verifyApplicationLogsExist('main', mainLogs, mainLogFile),
|
|
verifyApplicationLogsExist('renderer', rendererLogs, rendererLogFile),
|
|
...EXPECTED_LOG_MARKERS.map(
|
|
(marker) => verifyLogMarkerExistsInLogs(allLogs, marker),
|
|
),
|
|
verifyWindowTitle(windowTitles),
|
|
verifyErrorsInLogs(allLogs),
|
|
].filter(Boolean);
|
|
}
|
|
|
|
interface ExecutionError {
|
|
readonly reason: string;
|
|
readonly description: string;
|
|
}
|
|
|
|
function formatErrors(errors: readonly ExecutionError[]): string {
|
|
if (!errors?.length) { throw new Error('missing errors'); }
|
|
return [
|
|
'Errors detected during execution:',
|
|
...errors.map(
|
|
(error) => formatError(error),
|
|
),
|
|
].join('\n---\n');
|
|
}
|
|
|
|
function formatError(error: ExecutionError): string {
|
|
if (!error) { throw new Error('missing error'); }
|
|
if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); }
|
|
let message = `Reason: ${indentText(error.reason, 1)}`;
|
|
if (error.description) {
|
|
message += `\nDescription:\n${indentText(error.description, 2)}`;
|
|
}
|
|
return message;
|
|
}
|
|
|
|
function verifyApplicationLogsExist(
|
|
processType: ProcessType,
|
|
logContent: string | undefined,
|
|
logFilePath: string,
|
|
): ExecutionError | undefined {
|
|
if (!logContent?.length) {
|
|
return describeError(
|
|
`Missing application (${processType}) logs`,
|
|
'Application logs are empty not were not found.'
|
|
+ `\nLog path: ${logFilePath}`,
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function verifyLogMarkerExistsInLogs(
|
|
logContent: string | undefined,
|
|
marker: string,
|
|
) : ExecutionError | undefined {
|
|
if (!marker) {
|
|
throw new Error('missing marker');
|
|
}
|
|
if (!logContent?.includes(marker)) {
|
|
return describeError(
|
|
'Incomplete application logs',
|
|
`Missing identifier "${marker}" in application logs.`,
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function verifyWindowTitle(
|
|
windowTitles: readonly string[],
|
|
) : ExecutionError | undefined {
|
|
const errorTitles = windowTitles.filter(
|
|
(title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE),
|
|
);
|
|
if (errorTitles.length) {
|
|
return describeError(
|
|
'Unexpected window title',
|
|
'One or more window titles suggest an error occurred in the application:'
|
|
+ `\nError Titles: ${errorTitles.join(', ')}`
|
|
+ `\nAll Titles: ${windowTitles.join(', ')}`,
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function verifyStdErr(
|
|
stderrOutput: string | undefined,
|
|
) : ExecutionError | undefined {
|
|
if (stderrOutput && stderrOutput.length > 0) {
|
|
const ignoredErrorLines = new Set();
|
|
const relevantErrors = getNonEmptyLines(stderrOutput)
|
|
.filter((line) => {
|
|
line = line.trim();
|
|
if (STDERR_IGNORE_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
ignoredErrorLines.add(line);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (ignoredErrorLines.size > 0) {
|
|
log(`Ignoring \`stderr\` lines:\n${indentText([...ignoredErrorLines].join('\n'), 1)}`);
|
|
}
|
|
if (relevantErrors.length === 0) {
|
|
return undefined;
|
|
}
|
|
return describeError(
|
|
'Standard error stream (`stderr`) is not empty.',
|
|
`Relevant errors (${relevantErrors.length}):\n${indentText(relevantErrors.map((error) => `- ${error}`).join('\n'), 1)}`
|
|
+ `\nFull \`stderr\` output:\n${indentText(stderrOutput, 1)}`,
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function verifyErrorsInLogs(
|
|
logContent: string | undefined,
|
|
) : ExecutionError | undefined {
|
|
if (!logContent?.length) {
|
|
return undefined;
|
|
}
|
|
const logLines = getNonEmptyLines(logContent)
|
|
.filter((line) => line.includes(LOG_ERROR_MARKER));
|
|
if (!logLines.length) {
|
|
return undefined;
|
|
}
|
|
return describeError(
|
|
'Application log file',
|
|
logLines.join('\n'),
|
|
);
|
|
}
|
|
|
|
function describeError(
|
|
reason: string,
|
|
description: string,
|
|
) : ExecutionError | undefined {
|
|
return {
|
|
reason,
|
|
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
|
|
};
|
|
}
|
|
|
|
function getNonEmptyLines(text: string) {
|
|
return splitTextIntoLines(text)
|
|
.filter((line) => line?.trim().length > 0);
|
|
}
|