fix: Bug - No android app icon
This commit is contained in:
107
e2e/tests/mobile/android-app-icon.spec.ts
Normal file
107
e2e/tests/mobile/android-app-icon.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import {
|
||||
BRAND_LAUNCHER_BACKGROUND_COLOR,
|
||||
findMissingLauncherResources,
|
||||
findStockCapacitorResources,
|
||||
isBrandLauncherBackgroundColor,
|
||||
readAdaptiveIconBackgroundColor,
|
||||
REQUIRED_LAUNCHER_ICON_FILES,
|
||||
REQUIRED_SPLASH_FILES
|
||||
} from '../../../toju-app/src/app/infrastructure/mobile/logic/mobile-android-launcher-icon.rules';
|
||||
|
||||
/**
|
||||
* Regression coverage for: "No android app icon" - the Capacitor shell shipped
|
||||
* the stock Ionic placeholder launcher icon instead of the Toju brand mark.
|
||||
*
|
||||
* A native launcher icon cannot be asserted through a running browser, so this
|
||||
* spec verifies the committed Android resources directly: every density is
|
||||
* present, none still match a stock Capacitor placeholder, the adaptive-icon
|
||||
* background is the brand purple, and the generated bitmaps actually contain the
|
||||
* brand mark (white cat on a purple disc). This is deterministic - no emulator,
|
||||
* no timing - so it stays reliable in CI.
|
||||
*/
|
||||
|
||||
const REPO_ROOT = join(__dirname, '..', '..', '..');
|
||||
const RES_DIR = join(REPO_ROOT, 'toju-app', 'android', 'app', 'src', 'main', 'res');
|
||||
const BRAND_PURPLE = { r: 0x4a, g: 0x21, b: 0x7a };
|
||||
const WHITE = { r: 255, g: 255, b: 255 };
|
||||
const COLOR_TOLERANCE = 24;
|
||||
|
||||
function sha256(resRelativePath: string): string {
|
||||
return createHash('sha256').update(readFileSync(join(RES_DIR, resRelativePath)))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function colorDistance(
|
||||
left: { r: number; g: number; b: number },
|
||||
right: { r: number; g: number; b: number }
|
||||
): number {
|
||||
return Math.max(Math.abs(left.r - right.r), Math.abs(left.g - right.g), Math.abs(left.b - right.b));
|
||||
}
|
||||
|
||||
async function samplePixel(
|
||||
resRelativePath: string,
|
||||
xRatio: number,
|
||||
yRatio: number
|
||||
): Promise<{ r: number; g: number; b: number }> {
|
||||
const { data, info } = await sharp(join(RES_DIR, resRelativePath)).raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
const x = Math.min(info.width - 1, Math.floor(info.width * xRatio));
|
||||
const y = Math.min(info.height - 1, Math.floor(info.height * yRatio));
|
||||
const offset = (y * info.width + x) * info.channels;
|
||||
|
||||
return { r: data[offset], g: data[offset + 1], b: data[offset + 2] };
|
||||
}
|
||||
|
||||
test.describe('Android brand app icon', () => {
|
||||
test('ships a launcher icon and splash for every required density', () => {
|
||||
const allRequired = [...REQUIRED_LAUNCHER_ICON_FILES, ...REQUIRED_SPLASH_FILES];
|
||||
const present = allRequired.filter((file) => existsSync(join(RES_DIR, file)));
|
||||
|
||||
expect(findMissingLauncherResources(present)).toEqual([]);
|
||||
});
|
||||
|
||||
test('replaces every stock Capacitor placeholder with the brand asset', () => {
|
||||
const allRequired = [...REQUIRED_LAUNCHER_ICON_FILES, ...REQUIRED_SPLASH_FILES];
|
||||
const hashByFile = Object.fromEntries(
|
||||
allRequired.filter((file) => existsSync(join(RES_DIR, file))).map((file) => [file, sha256(file)])
|
||||
);
|
||||
|
||||
expect(findStockCapacitorResources(hashByFile)).toEqual([]);
|
||||
});
|
||||
|
||||
test('uses the brand purple as the adaptive-icon background', () => {
|
||||
const valuesXml = readFileSync(join(RES_DIR, 'values', 'ic_launcher_background.xml'), 'utf8');
|
||||
const color = readAdaptiveIconBackgroundColor(valuesXml);
|
||||
|
||||
expect(color).not.toBe('#FFFFFF');
|
||||
expect(isBrandLauncherBackgroundColor(color)).toBe(true);
|
||||
expect(color?.toLowerCase()).toBe(BRAND_LAUNCHER_BACKGROUND_COLOR.toLowerCase());
|
||||
});
|
||||
|
||||
test('renders the brand mark (white cat on a purple disc) in the launcher bitmap', async () => {
|
||||
const launcher = 'mipmap-xxxhdpi/ic_launcher.png';
|
||||
const ringTop = await samplePixel(launcher, 0.5, 0.12);
|
||||
const ringLeft = await samplePixel(launcher, 0.12, 0.5);
|
||||
const faceCenter = await samplePixel(launcher, 0.5, 0.5);
|
||||
|
||||
expect(colorDistance(ringTop, BRAND_PURPLE)).toBeLessThanOrEqual(COLOR_TOLERANCE);
|
||||
expect(colorDistance(ringLeft, BRAND_PURPLE)).toBeLessThanOrEqual(COLOR_TOLERANCE);
|
||||
expect(colorDistance(faceCenter, WHITE)).toBeLessThanOrEqual(COLOR_TOLERANCE);
|
||||
});
|
||||
|
||||
test('renders the splash art as the brand mark centred on a purple field', async () => {
|
||||
const splash = 'drawable-port-xhdpi/splash.png';
|
||||
const corner = await samplePixel(splash, 0.04, 0.04);
|
||||
const center = await samplePixel(splash, 0.5, 0.5);
|
||||
|
||||
expect(colorDistance(corner, BRAND_PURPLE)).toBeLessThanOrEqual(COLOR_TOLERANCE);
|
||||
expect(colorDistance(center, WHITE)).toBeLessThanOrEqual(COLOR_TOLERANCE);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user