mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(app-update): add in-app update flow with native e2e coverage
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDirectory, '..');
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const createTempWorkspace = async (prefix) => fs.promises.mkdtemp(path.join(os.tmpdir(), `${prefix}-`));
|
||||
|
||||
const logStep = (message) => {
|
||||
console.log(`[app-update-e2e] ${message}`);
|
||||
};
|
||||
|
||||
const runCommand = (command, args, options = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
logStep(`$ ${command} ${args.join(' ')}`);
|
||||
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd || repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
...options.env,
|
||||
},
|
||||
stdio: options.captureOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (options.captureOutput) {
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
}
|
||||
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({
|
||||
stdout,
|
||||
stderr,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(stderr.trim() || `${command} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
|
||||
const waitFor = async (predicate, { timeoutMs = 120000, intervalMs = 1000, description = 'condition' } = {}) => {
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const result = await predicate();
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for ${description}`);
|
||||
};
|
||||
|
||||
const findFirstMatchingPath = async (rootPath, matcher) => {
|
||||
const entries = await fs.promises.readdir(rootPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(rootPath, entry.name);
|
||||
if (matcher(entryPath, entry)) {
|
||||
return entryPath;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const nestedMatch = await findFirstMatchingPath(entryPath, matcher);
|
||||
if (nestedMatch) {
|
||||
return nestedMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const copyPath = async (sourcePath, targetPath) => {
|
||||
await fs.promises.rm(targetPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
await fs.promises.mkdir(path.dirname(targetPath), {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin' && sourcePath.endsWith('.app')) {
|
||||
await runCommand('/usr/bin/ditto', [sourcePath, targetPath]);
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.promises.cp(sourcePath, targetPath, {
|
||||
recursive: true,
|
||||
});
|
||||
};
|
||||
|
||||
const startFixtureServer = async () => {
|
||||
const state = {
|
||||
version: null,
|
||||
assets: [],
|
||||
};
|
||||
|
||||
const server = http.createServer(async (request, response) => {
|
||||
const requestUrl = new URL(request.url || '/', `http://${request.headers.host || '127.0.0.1'}`);
|
||||
|
||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
||||
response.setHeader('Access-Control-Allow-Headers', '*');
|
||||
response.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS');
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestUrl.pathname === '/releases/latest') {
|
||||
if (!state.version || state.assets.length === 0) {
|
||||
response.writeHead(503, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
});
|
||||
response.end(JSON.stringify({ error: 'fixture release not configured' }));
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
tag_name: `v${state.version}`,
|
||||
html_url: `${requestUrl.origin}/releases/v${state.version}`,
|
||||
assets: state.assets.map((asset) => ({
|
||||
name: asset.name,
|
||||
browser_download_url: `${requestUrl.origin}/assets/${asset.name}`,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestUrl.pathname.startsWith('/releases/')) {
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
});
|
||||
response.end('<!doctype html><title>Fixture Release</title><p>Fixture release page</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestUrl.pathname.startsWith('/assets/')) {
|
||||
const assetName = decodeURIComponent(requestUrl.pathname.slice('/assets/'.length));
|
||||
const matchedAsset = state.assets.find((asset) => asset.name === assetName);
|
||||
|
||||
if (!matchedAsset) {
|
||||
response.writeHead(404, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
});
|
||||
response.end('asset not found');
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
fs.createReadStream(matchedAsset.filePath).pipe(response);
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(404, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
});
|
||||
response.end('not found');
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.on('error', reject);
|
||||
server.listen(0, '0.0.0.0', resolve);
|
||||
});
|
||||
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Could not resolve fixture server port');
|
||||
}
|
||||
|
||||
return {
|
||||
port: address.port,
|
||||
setRelease(version, assets) {
|
||||
state.version = version;
|
||||
state.assets = assets;
|
||||
logStep(`fixture server release set to v${version} with assets: ${assets.map((asset) => asset.name).join(', ')}`);
|
||||
},
|
||||
async close() {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { copyPath, createTempWorkspace, findFirstMatchingPath, logStep, repoRoot, runCommand, sleep, startFixtureServer, waitFor };
|
||||
@@ -0,0 +1,239 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { createTempWorkspace, logStep, repoRoot, runCommand, sleep, startFixtureServer, waitFor } from './app-update-e2e-helpers.mjs';
|
||||
|
||||
const OLD_VERSION = '0.7.1';
|
||||
const NEW_VERSION = '0.7.3';
|
||||
const OLD_VERSION_CODE = '701';
|
||||
const NEW_VERSION_CODE = '703';
|
||||
const PACKAGE_NAME = 'fivechan.android';
|
||||
const ACTIVITY_NAME = `${PACKAGE_NAME}/.MainActivity`;
|
||||
const AVD_NAME = 'fivechan-test-api35';
|
||||
const SETTINGS_HASH = '#/all/settings#interface-settings';
|
||||
|
||||
const getAndroidSdkRoot = () => process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
||||
|
||||
const getSdkToolPath = (relativePath) => {
|
||||
const sdkRoot = getAndroidSdkRoot();
|
||||
if (!sdkRoot) {
|
||||
throw new Error('ANDROID_HOME or ANDROID_SDK_ROOT must be set');
|
||||
}
|
||||
|
||||
return path.join(sdkRoot, relativePath);
|
||||
};
|
||||
|
||||
const adb = async (args, options = {}) => runCommand(getSdkToolPath('platform-tools/adb'), args, options);
|
||||
|
||||
const buildAndroidDebugApk = async ({ version, versionCode, fixturePort }) => {
|
||||
await runCommand('corepack', ['yarn', 'build'], {
|
||||
env: {
|
||||
VITE_APP_VERSION: version,
|
||||
VITE_APP_UPDATE_ALLOWED_DOWNLOAD_HOSTS: '10.0.2.2',
|
||||
VITE_APP_UPDATE_RELEASE_API_URL: `http://10.0.2.2:${fixturePort}/releases/latest`,
|
||||
VITE_E2E_START_HASH: SETTINGS_HASH,
|
||||
},
|
||||
});
|
||||
await runCommand('npx', ['cap', 'sync', 'android']);
|
||||
await runCommand('./gradlew', ['assembleDebug', `-PAPP_VERSION_CODE=${versionCode}`, `-PAPP_VERSION_NAME=${version}`], {
|
||||
cwd: path.join(repoRoot, 'android'),
|
||||
});
|
||||
|
||||
return path.join(repoRoot, 'android', 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
|
||||
};
|
||||
|
||||
const ensureEmulatorRunning = async () => {
|
||||
const devicesOutput = await adb(['devices'], { captureOutput: true });
|
||||
const existingEmulator = devicesOutput.stdout
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('emulator-') && line.endsWith('\tdevice'));
|
||||
|
||||
if (existingEmulator) {
|
||||
return existingEmulator.split('\t')[0];
|
||||
}
|
||||
|
||||
const avdManagerPath = getSdkToolPath('cmdline-tools/latest/bin/avdmanager');
|
||||
const emulatorPath = getSdkToolPath('emulator/emulator');
|
||||
const avdList = await runCommand(avdManagerPath, ['list', 'avd'], {
|
||||
captureOutput: true,
|
||||
});
|
||||
|
||||
if (!avdList.stdout.includes(`Name: ${AVD_NAME}`)) {
|
||||
await runCommand('/bin/sh', ['-lc', `echo "no" | "${avdManagerPath}" create avd --name "${AVD_NAME}" --package "system-images;android-35;google_apis;arm64-v8a" --device pixel_6 --force`]);
|
||||
}
|
||||
|
||||
const emulatorLogPath = path.join(os.tmpdir(), `fivechan-emulator-${Date.now()}.log`);
|
||||
logStep(`starting emulator, log: ${emulatorLogPath}`);
|
||||
runCommand('/bin/sh', ['-lc', `"${emulatorPath}" -avd "${AVD_NAME}" -no-boot-anim -no-snapshot-save -netdelay none -netspeed full > "${emulatorLogPath}" 2>&1 &`]);
|
||||
|
||||
await adb(['wait-for-device']);
|
||||
await waitFor(
|
||||
async () => {
|
||||
const result = await adb(['shell', 'getprop', 'sys.boot_completed'], {
|
||||
captureOutput: true,
|
||||
});
|
||||
return result.stdout.replace(/\s+/g, '') === '1';
|
||||
},
|
||||
{
|
||||
timeoutMs: 180000,
|
||||
intervalMs: 2000,
|
||||
description: 'the Android emulator to finish booting',
|
||||
},
|
||||
);
|
||||
|
||||
await adb(['shell', 'settings', 'put', 'global', 'window_animation_scale', '0']);
|
||||
await adb(['shell', 'settings', 'put', 'global', 'transition_animation_scale', '0']);
|
||||
await adb(['shell', 'settings', 'put', 'global', 'animator_duration_scale', '0']);
|
||||
|
||||
const refreshedDevicesOutput = await adb(['devices'], { captureOutput: true });
|
||||
const emulatorLine = refreshedDevicesOutput.stdout
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('emulator-') && line.endsWith('\tdevice'));
|
||||
|
||||
if (!emulatorLine) {
|
||||
throw new Error('Could not determine the running emulator serial');
|
||||
}
|
||||
|
||||
return emulatorLine.split('\t')[0];
|
||||
};
|
||||
|
||||
const readUiDump = async (serial) => {
|
||||
await adb(['-s', serial, 'shell', 'uiautomator', 'dump', '/sdcard/window_dump.xml']);
|
||||
const dumpResult = await adb(['-s', serial, 'shell', 'cat', '/sdcard/window_dump.xml'], {
|
||||
captureOutput: true,
|
||||
});
|
||||
return dumpResult.stdout;
|
||||
};
|
||||
|
||||
const findNodeBounds = (dumpXml, matcher) => {
|
||||
const nodePattern = /<node\b([^>]+?)\/>/g;
|
||||
|
||||
for (const match of dumpXml.matchAll(nodePattern)) {
|
||||
const attributes = match[1];
|
||||
const textMatch = attributes.match(/\btext="([^"]*)"/);
|
||||
const contentDescMatch = attributes.match(/\bcontent-desc="([^"]*)"/);
|
||||
const boundsMatch = attributes.match(/\bbounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
|
||||
const clickableMatch = attributes.match(/\bclickable="([^"]*)"/);
|
||||
const text = textMatch?.[1] || contentDescMatch?.[1] || '';
|
||||
|
||||
if (!boundsMatch || !matcher(text, clickableMatch?.[1] === 'true')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, left, top, right, bottom] = boundsMatch.map(Number);
|
||||
return {
|
||||
x: Math.floor((left + right) / 2),
|
||||
y: Math.floor((top + bottom) / 2),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const tapText = async (serial, { exactText, pattern, timeoutMs = 120000 }) => {
|
||||
const bounds = await waitFor(
|
||||
async () => {
|
||||
const dumpXml = await readUiDump(serial);
|
||||
return findNodeBounds(dumpXml, (text, clickable) => clickable && (exactText ? text === exactText : pattern.test(text)));
|
||||
},
|
||||
{
|
||||
timeoutMs,
|
||||
intervalMs: 2000,
|
||||
description: `UI text ${exactText || pattern}`,
|
||||
},
|
||||
);
|
||||
|
||||
await adb(['-s', serial, 'shell', 'input', 'tap', `${bounds.x}`, `${bounds.y}`]);
|
||||
};
|
||||
|
||||
const waitForText = async (serial, matcher, description) =>
|
||||
waitFor(
|
||||
async () => {
|
||||
const dumpXml = await readUiDump(serial);
|
||||
if (typeof matcher === 'string') {
|
||||
return dumpXml.includes(`text="${matcher}"`) || dumpXml.includes(`content-desc="${matcher}"`);
|
||||
}
|
||||
|
||||
return matcher.test(dumpXml);
|
||||
},
|
||||
{
|
||||
timeoutMs: 120000,
|
||||
intervalMs: 2000,
|
||||
description,
|
||||
},
|
||||
);
|
||||
|
||||
const waitForInstalledVersion = async (serial, version) =>
|
||||
waitFor(
|
||||
async () => {
|
||||
const packageInfo = await adb(['-s', serial, 'shell', 'dumpsys', 'package', PACKAGE_NAME], {
|
||||
captureOutput: true,
|
||||
});
|
||||
return packageInfo.stdout.includes(`versionName=${version}`);
|
||||
},
|
||||
{
|
||||
timeoutMs: 180000,
|
||||
intervalMs: 3000,
|
||||
description: `installed package version ${version}`,
|
||||
},
|
||||
);
|
||||
|
||||
const main = async () => {
|
||||
const workspace = await createTempWorkspace('android-update-e2e');
|
||||
const fixtureServer = await startFixtureServer();
|
||||
|
||||
try {
|
||||
const oldApkSourcePath = await buildAndroidDebugApk({
|
||||
version: OLD_VERSION,
|
||||
versionCode: OLD_VERSION_CODE,
|
||||
fixturePort: fixtureServer.port,
|
||||
});
|
||||
const oldApkPath = path.join(workspace, `5chan-v${OLD_VERSION}.apk`);
|
||||
await fs.promises.copyFile(oldApkSourcePath, oldApkPath);
|
||||
|
||||
const newApkSourcePath = await buildAndroidDebugApk({
|
||||
version: NEW_VERSION,
|
||||
versionCode: NEW_VERSION_CODE,
|
||||
fixturePort: fixtureServer.port,
|
||||
});
|
||||
const newApkPath = path.join(workspace, `5chan-v${NEW_VERSION}.apk`);
|
||||
await fs.promises.copyFile(newApkSourcePath, newApkPath);
|
||||
|
||||
fixtureServer.setRelease(NEW_VERSION, [
|
||||
{
|
||||
name: path.basename(newApkPath),
|
||||
filePath: newApkPath,
|
||||
},
|
||||
]);
|
||||
|
||||
const serial = await ensureEmulatorRunning();
|
||||
await adb(['-s', serial, 'uninstall', PACKAGE_NAME]).catch(() => undefined);
|
||||
await adb(['-s', serial, 'install', '-r', oldApkPath]);
|
||||
await adb(['-s', serial, 'shell', 'appops', 'set', PACKAGE_NAME, 'REQUEST_INSTALL_PACKAGES', 'allow']);
|
||||
await adb(['-s', serial, 'shell', 'am', 'start', '-n', ACTIVITY_NAME]);
|
||||
|
||||
await waitForText(serial, 'Check', 'the settings update check button');
|
||||
await tapText(serial, { exactText: 'Check' });
|
||||
await waitForText(serial, 'Download', 'the download button after update detection');
|
||||
await waitForText(serial, `v${NEW_VERSION}`, 'the release version link text');
|
||||
await tapText(serial, { exactText: 'Download' });
|
||||
await tapText(serial, {
|
||||
pattern: /^(Install|Update)$/i,
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
await waitForInstalledVersion(serial, NEW_VERSION);
|
||||
|
||||
await adb(['-s', serial, 'shell', 'am', 'force-stop', PACKAGE_NAME]);
|
||||
await adb(['-s', serial, 'shell', 'am', 'start', '-n', ACTIVITY_NAME]);
|
||||
await waitForText(serial, `v${NEW_VERSION}`, 'the relaunched app version text');
|
||||
|
||||
logStep(`android update e2e passed on ${serial}: package ${PACKAGE_NAME} updated to ${NEW_VERSION}`);
|
||||
} finally {
|
||||
await sleep(1000);
|
||||
await fixtureServer.close();
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,136 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { _electron as electron } from 'playwright';
|
||||
import { copyPath, createTempWorkspace, findFirstMatchingPath, logStep, repoRoot, runCommand, sleep, startFixtureServer, waitFor } from './app-update-e2e-helpers.mjs';
|
||||
|
||||
const OLD_VERSION = '0.7.1';
|
||||
const NEW_VERSION = '0.7.3';
|
||||
const SETTINGS_HASH = '#/all/settings#interface-settings';
|
||||
|
||||
const findPackagedMacApp = async () => {
|
||||
const outDirectory = path.join(repoRoot, 'out');
|
||||
return findFirstMatchingPath(outDirectory, (entryPath, entry) => entry.isDirectory() && entryPath.endsWith('.app'));
|
||||
};
|
||||
|
||||
const buildPackagedApp = async ({ version, fixturePort }) => {
|
||||
await runCommand('corepack', ['yarn', 'electron:package'], {
|
||||
env: {
|
||||
VITE_APP_VERSION: version,
|
||||
VITE_APP_UPDATE_ALLOWED_DOWNLOAD_HOSTS: '127.0.0.1',
|
||||
VITE_APP_UPDATE_RELEASE_API_URL: `http://127.0.0.1:${fixturePort}/releases/latest`,
|
||||
VITE_E2E_START_HASH: SETTINGS_HASH,
|
||||
},
|
||||
});
|
||||
|
||||
const packagedAppPath = await findPackagedMacApp();
|
||||
if (!packagedAppPath) {
|
||||
throw new Error('Could not find the packaged macOS app');
|
||||
}
|
||||
|
||||
return packagedAppPath;
|
||||
};
|
||||
|
||||
const zipMacApp = async (appPath, zipPath) => {
|
||||
await fs.promises.rm(zipPath, { force: true });
|
||||
await runCommand('/usr/bin/ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', appPath, zipPath]);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
throw new Error('Electron update e2e is only implemented for macOS in this harness');
|
||||
}
|
||||
|
||||
const workspace = await createTempWorkspace('electron-update-e2e');
|
||||
const fixtureServer = await startFixtureServer();
|
||||
|
||||
try {
|
||||
const packagedOldAppPath = await buildPackagedApp({
|
||||
version: OLD_VERSION,
|
||||
fixturePort: fixtureServer.port,
|
||||
});
|
||||
|
||||
const installedAppPath = path.join(workspace, 'installed', '5chan.app');
|
||||
await copyPath(packagedOldAppPath, installedAppPath);
|
||||
|
||||
const packagedNewAppPath = await buildPackagedApp({
|
||||
version: NEW_VERSION,
|
||||
fixturePort: fixtureServer.port,
|
||||
});
|
||||
|
||||
const zippedNewAppPath = path.join(workspace, `5chan-darwin-arm64-v${NEW_VERSION}.zip`);
|
||||
await zipMacApp(packagedNewAppPath, zippedNewAppPath);
|
||||
|
||||
fixtureServer.setRelease(NEW_VERSION, [
|
||||
{
|
||||
name: path.basename(zippedNewAppPath),
|
||||
filePath: zippedNewAppPath,
|
||||
},
|
||||
]);
|
||||
|
||||
const versionMetadataPath = path.join(installedAppPath, 'Contents', 'Resources', 'app', 'build', 'version.json');
|
||||
const sandboxHome = path.join(workspace, 'home');
|
||||
const plebbitDataPath = path.join(sandboxHome, 'Library', 'Application Support', 'plebbit');
|
||||
await fs.promises.mkdir(plebbitDataPath, { recursive: true });
|
||||
await fs.promises.writeFile(path.join(plebbitDataPath, 'auth-key'), 'e2e-auth-key', 'utf8');
|
||||
|
||||
const electronApp = await electron.launch({
|
||||
executablePath: path.join(installedAppPath, 'Contents', 'MacOS', '5chan'),
|
||||
env: {
|
||||
...process.env,
|
||||
APP_UPDATE_ALLOWED_DOWNLOAD_HOSTS: '127.0.0.1',
|
||||
HOME: sandboxHome,
|
||||
},
|
||||
});
|
||||
|
||||
const firstWindow = await electronApp.firstWindow();
|
||||
await firstWindow.waitForLoadState('domcontentloaded');
|
||||
await firstWindow.getByRole('button', { name: 'Check' }).waitFor({
|
||||
timeout: 120000,
|
||||
});
|
||||
await firstWindow.getByRole('button', { name: 'Check' }).click();
|
||||
await firstWindow.getByRole('button', { name: 'Download' }).waitFor({
|
||||
timeout: 120000,
|
||||
});
|
||||
await firstWindow.getByText(`v${NEW_VERSION}`).waitFor({
|
||||
timeout: 120000,
|
||||
});
|
||||
await firstWindow.getByRole('button', { name: 'Download' }).click();
|
||||
|
||||
await waitFor(
|
||||
async () => {
|
||||
try {
|
||||
const payload = JSON.parse(await fs.promises.readFile(versionMetadataPath, 'utf8'));
|
||||
return payload.version === NEW_VERSION;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
timeoutMs: 180000,
|
||||
intervalMs: 2000,
|
||||
description: `the installed app bundle to be replaced with v${NEW_VERSION}`,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
async () => {
|
||||
const result = await runCommand('/usr/bin/pgrep', ['-f', path.join(installedAppPath, 'Contents', 'MacOS', '5chan')], {
|
||||
captureOutput: true,
|
||||
}).catch(() => null);
|
||||
return result?.stdout?.trim()?.length ? result.stdout.trim() : false;
|
||||
},
|
||||
{
|
||||
timeoutMs: 120000,
|
||||
intervalMs: 2000,
|
||||
description: 'the updated macOS app to relaunch',
|
||||
},
|
||||
);
|
||||
|
||||
logStep(`electron update e2e passed: ${installedAppPath} now contains version ${NEW_VERSION}`);
|
||||
} finally {
|
||||
await sleep(2000);
|
||||
await fixtureServer.close();
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user