mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat: add in-app update flow and native e2e verification (#1112)
* feat(app-update): add in-app update flow with native e2e coverage * docs(ai-workflow): infer closed-issue labels automatically * fix(app-update): address native updater review findings * fix(android): validate updater redirect hosts * fix(electron): harden updater file names
This commit is contained in:
@@ -173,6 +173,78 @@ describe('app-update', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers a matching mac dmg over an incompatible zip fallback', async () => {
|
||||
window.electronApi = {
|
||||
isElectron: true,
|
||||
getPlatform: () => testState.electronGetPlatformMock(),
|
||||
downloadAndInstallUpdate: (options) => testState.electronDownloadAndInstallUpdateMock(options),
|
||||
copyToClipboard: vi.fn(),
|
||||
automateUploadMedia: vi.fn(),
|
||||
} as Window['electronApi'];
|
||||
testState.electronGetPlatformMock.mockResolvedValue({
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
version: 'v20.0.0',
|
||||
});
|
||||
testState.fetchMock.mockResolvedValueOnce(
|
||||
createFetchResponse({
|
||||
tag_name: 'v9.9.9',
|
||||
assets: [
|
||||
{
|
||||
name: '5chan-9.9.9-x64.zip',
|
||||
browser_download_url: 'https://github.com/bitsocialnet/5chan/releases/download/v9.9.9/5chan-9.9.9-x64.zip',
|
||||
},
|
||||
{
|
||||
name: '5chan-9.9.9-arm64.dmg',
|
||||
browser_download_url: 'https://github.com/bitsocialnet/5chan/releases/download/v9.9.9/5chan-9.9.9-arm64.dmg',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const { resolveAvailableAppUpdate } = await loadModule();
|
||||
const result = await resolveAvailableAppUpdate();
|
||||
|
||||
expect(result).toEqual({
|
||||
runtime: 'electron',
|
||||
targetVersion: '9.9.9',
|
||||
assetName: '5chan-9.9.9-arm64.dmg',
|
||||
downloadUrl: 'https://github.com/bitsocialnet/5chan/releases/download/v9.9.9/5chan-9.9.9-arm64.dmg',
|
||||
releaseUrl: 'https://github.com/bitsocialnet/5chan/releases/tag/v9.9.9',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no mac update when the only zip asset targets the wrong architecture', async () => {
|
||||
window.electronApi = {
|
||||
isElectron: true,
|
||||
getPlatform: () => testState.electronGetPlatformMock(),
|
||||
downloadAndInstallUpdate: (options) => testState.electronDownloadAndInstallUpdateMock(options),
|
||||
copyToClipboard: vi.fn(),
|
||||
automateUploadMedia: vi.fn(),
|
||||
} as Window['electronApi'];
|
||||
testState.electronGetPlatformMock.mockResolvedValue({
|
||||
platform: 'darwin',
|
||||
arch: 'x64',
|
||||
version: 'v20.0.0',
|
||||
});
|
||||
testState.fetchMock.mockResolvedValueOnce(
|
||||
createFetchResponse({
|
||||
tag_name: 'v9.9.9',
|
||||
assets: [
|
||||
{
|
||||
name: '5chan-9.9.9-arm64.zip',
|
||||
browser_download_url: 'https://github.com/bitsocialnet/5chan/releases/download/v9.9.9/5chan-9.9.9-arm64.zip',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const { resolveAvailableAppUpdate } = await loadModule();
|
||||
const result = await resolveAvailableAppUpdate();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('selects the latest android apk release asset', async () => {
|
||||
testState.capacitorPlatform = 'android';
|
||||
testState.fetchMock.mockResolvedValueOnce(
|
||||
|
||||
+24
-19
@@ -103,7 +103,25 @@ const hasArmArchitecture = (value: string): boolean => {
|
||||
return normalized.includes('arm64') || normalized.includes('aarch64');
|
||||
};
|
||||
|
||||
const hasX64Architecture = (value: string): boolean => !hasArmArchitecture(value);
|
||||
const hasX64Architecture = (value: string): boolean => {
|
||||
const normalized = value.toLowerCase();
|
||||
return normalized.includes('x64') || normalized.includes('x86_64') || normalized.includes('amd64');
|
||||
};
|
||||
|
||||
const isArchitectureAgnostic = (value: string): boolean => !hasArmArchitecture(value) && !hasX64Architecture(value);
|
||||
|
||||
const matchesPreferredArchitecture = (value: string, prefersArm: boolean, prefersX64: boolean): boolean =>
|
||||
(prefersArm && hasArmArchitecture(value)) || (prefersX64 && hasX64Architecture(value));
|
||||
|
||||
const findCompatibleAsset = (
|
||||
assets: GitHubReleaseAsset[],
|
||||
predicate: (asset: GitHubReleaseAsset) => boolean,
|
||||
prefersArm: boolean,
|
||||
prefersX64: boolean,
|
||||
): GitHubReleaseAsset | null =>
|
||||
assets.find((asset) => predicate(asset) && matchesPreferredArchitecture(asset.name, prefersArm, prefersX64)) ||
|
||||
assets.find((asset) => predicate(asset) && isArchitectureAgnostic(asset.name)) ||
|
||||
null;
|
||||
|
||||
const getReleaseUrl = (version: string): string => getDefaultReleaseUrl(normalizeVersion(version));
|
||||
|
||||
@@ -118,30 +136,17 @@ const findMatchingElectronAsset = async (assets: GitHubReleaseAsset[]): Promise<
|
||||
const prefersX64 = hasX64Architecture(arch);
|
||||
|
||||
if (platform === 'darwin') {
|
||||
return (
|
||||
assets.find((asset) => asset.name.endsWith('.zip') && ((prefersArm && hasArmArchitecture(asset.name)) || (prefersX64 && hasX64Architecture(asset.name)))) ||
|
||||
assets.find((asset) => asset.name.endsWith('.zip')) ||
|
||||
assets.find((asset) => asset.name.endsWith('.dmg') && ((prefersArm && hasArmArchitecture(asset.name)) || (prefersX64 && hasX64Architecture(asset.name)))) ||
|
||||
null
|
||||
);
|
||||
const matchingZip = findCompatibleAsset(assets, (asset) => asset.name.endsWith('.zip'), prefersArm, prefersX64);
|
||||
const matchingDmg = findCompatibleAsset(assets, (asset) => asset.name.endsWith('.dmg'), prefersArm, prefersX64);
|
||||
return matchingZip || matchingDmg;
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
return (
|
||||
assets.find((asset) => asset.name.endsWith('.AppImage') && ((prefersArm && hasArmArchitecture(asset.name)) || (prefersX64 && hasX64Architecture(asset.name)))) ||
|
||||
null
|
||||
);
|
||||
return findCompatibleAsset(assets, (asset) => asset.name.endsWith('.AppImage'), prefersArm, prefersX64);
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
return (
|
||||
assets.find(
|
||||
(asset) =>
|
||||
asset.name.toLowerCase().endsWith('.exe') &&
|
||||
asset.name.toLowerCase().includes('setup') &&
|
||||
((prefersArm && hasArmArchitecture(asset.name)) || (prefersX64 && hasX64Architecture(asset.name))),
|
||||
) || null
|
||||
);
|
||||
return findCompatibleAsset(assets, (asset) => asset.name.toLowerCase().endsWith('.exe') && asset.name.toLowerCase().includes('setup'), prefersArm, prefersX64);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user