Improve desktop runtime execution tests

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.
This commit is contained in:
undergroundwires
2023-08-29 16:30:00 +02:00
parent 35be05df20
commit ad0576a752
146 changed files with 2418 additions and 1186 deletions

View File

@@ -1,29 +1,33 @@
import { unlink } from 'fs/promises';
import { runCommand } from '../../utils/run-command.js';
import { log, LOG_LEVELS } from '../../utils/log.js';
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from '../../utils/platform.js';
import { exists } from '../../utils/io.js';
import { runCommand } from '../../utils/run-command';
import { log, LogLevel } from '../../utils/log';
import { CURRENT_PLATFORM, SupportedPlatform } from '../../utils/platform';
import { exists } from '../../utils/io';
export async function captureScreen(imagePath) {
export async function captureScreen(
imagePath: string,
): Promise<void> {
if (!imagePath) {
throw new Error('Path for screenshot not provided');
}
if (await exists(imagePath)) {
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LOG_LEVELS.WARN);
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LogLevel.Warn);
unlink(imagePath);
}
const platformCommands = {
[SUPPORTED_PLATFORMS.MAC]: `screencapture -x ${imagePath}`,
[SUPPORTED_PLATFORMS.LINUX]: `import -window root ${imagePath}`,
[SUPPORTED_PLATFORMS.WINDOWS]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
const platformCommands: {
readonly [K in SupportedPlatform]: string;
} = {
[SupportedPlatform.macOS]: `screencapture -x ${imagePath}`,
[SupportedPlatform.Linux]: `import -window root ${imagePath}`,
[SupportedPlatform.Windows]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
};
const commandForPlatform = platformCommands[CURRENT_PLATFORM];
if (!commandForPlatform) {
log(`Screenshot capture not supported on: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
log(`Screenshot capture not supported on: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn);
return;
}
@@ -31,13 +35,13 @@ export async function captureScreen(imagePath) {
const { error } = await runCommand(commandForPlatform);
if (error) {
log(`Failed to capture screenshot.\n${error}`, LOG_LEVELS.WARN);
log(`Failed to capture screenshot.\n${error}`, LogLevel.Warn);
return;
}
log(`Captured screenshot to ${imagePath}.`);
}
function getScreenshotPowershellScript(imagePath) {
function getScreenshotPowershellScript(imagePath: string): string {
return `
$ProgressPreference = 'SilentlyContinue' # Do not pollute stderr
Add-Type -AssemblyName System.Windows.Forms
@@ -53,7 +57,7 @@ function getScreenshotPowershellScript(imagePath) {
`;
}
function encodeForPowershell(script) {
const buffer = Buffer.from(script, 'utf-16le');
function encodeForPowershell(script: string): string {
const buffer = Buffer.from(script, 'utf16le');
return buffer.toString('base64');
}

View File

@@ -1,37 +1,40 @@
import { runCommand } from '../../utils/run-command.js';
import { log, LOG_LEVELS } from '../../utils/log.js';
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../../utils/platform.js';
import { runCommand } from '../../utils/run-command';
import { log, LogLevel } from '../../utils/log';
import { SupportedPlatform, CURRENT_PLATFORM } from '../../utils/platform';
export async function captureWindowTitles(processId) {
export async function captureWindowTitles(processId: number) {
if (!processId) { throw new Error('Missing process ID.'); }
const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM];
if (!captureFunction) {
log(`Cannot capture window title, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
log(`Cannot capture window title, unsupported OS: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn);
return undefined;
}
return captureFunction(processId);
}
const windowTitleCaptureFunctions = {
[SUPPORTED_PLATFORMS.MAC]: captureTitlesOnMac,
[SUPPORTED_PLATFORMS.LINUX]: captureTitlesOnLinux,
[SUPPORTED_PLATFORMS.WINDOWS]: captureTitlesOnWindows,
const windowTitleCaptureFunctions: {
readonly [K in SupportedPlatform]: (processId: number) => Promise<string[]>;
} = {
[SupportedPlatform.macOS]: (processId) => captureTitlesOnMac(processId),
[SupportedPlatform.Linux]: (processId) => captureTitlesOnLinux(processId),
[SupportedPlatform.Windows]: (processId) => captureTitlesOnWindows(processId),
};
async function captureTitlesOnWindows(processId) {
async function captureTitlesOnWindows(processId: number): Promise<string[]> {
if (!processId) { throw new Error('Missing process ID.'); }
const { stdout: tasklistOutput, error } = await runCommand(
`tasklist /FI "PID eq ${processId}" /fo list /v`,
);
if (error) {
log(`Failed to retrieve window title.\n${error}`, LOG_LEVELS.WARN);
log(`Failed to retrieve window title.\n${error}`, LogLevel.Warn);
return [];
}
const match = tasklistOutput.match(/Window Title:\s*(.*)/);
if (match && match[1]) {
const regex = /Window Title:\s*(.*)/;
const match = regex.exec(tasklistOutput);
if (match && match.length > 1 && match[1]) {
const title = match[1].trim();
if (title === 'N/A') {
return [];
@@ -41,7 +44,7 @@ async function captureTitlesOnWindows(processId) {
return [];
}
async function captureTitlesOnLinux(processId) {
async function captureTitlesOnLinux(processId: number): Promise<string[]> {
if (!processId) { throw new Error('Missing process ID.'); }
const { stdout: windowIdsOutput, error: windowIdError } = await runCommand(
@@ -49,7 +52,7 @@ async function captureTitlesOnLinux(processId) {
);
if (windowIdError || !windowIdsOutput) {
return undefined;
return [];
}
const windowIds = windowIdsOutput.trim().split('\n');
@@ -69,23 +72,24 @@ async function captureTitlesOnLinux(processId) {
let hasAssistiveAccessOnMac = true;
async function captureTitlesOnMac(processId) {
async function captureTitlesOnMac(processId: number): Promise<string[]> {
if (!processId) { throw new Error('Missing process ID.'); }
if (!hasAssistiveAccessOnMac) {
return [];
}
const script = `
tell application "System Events"
tell application "System Events"
try
set targetProcess to first process whose unix id is ${processId}
on error
return
end try
tell targetProcess
if (count of windows) > 0 then
set window_name to name of front window
return window_name
end if
set allWindowNames to {}
repeat with aWindow in windows
set end of allWindowNames to name of aWindow
end repeat
return allWindowNames
end tell
end tell
`;
@@ -102,7 +106,7 @@ async function captureTitlesOnMac(processId) {
hasAssistiveAccessOnMac = false;
}
errorMessage += error;
log(errorMessage, LOG_LEVELS.WARN);
log(errorMessage, LogLevel.Warn);
return [];
}
const title = titleOutput?.trim();