Files
pleno-vue/tests/unit/app-store-product-readiness.spec.js
T
Jeppe B 32418b42a0 fix(ios): add required location purpose string (#223)
## What changed
- add `NSLocationAlwaysAndWhenInUseUsageDescription` to the iOS app
- localize the purpose string in Danish and English
- enforce the key in mobile permission validation and App Store
readiness tests

## Why
App Store Connect accepted builds 1 and 2 but emitted `ITMS-90683`,
stating that the final app references APIs requiring this purpose
string. Shipping a corrected binary avoids submitting a candidate with a
known Apple delivery warning.

## Validation
- `node scripts/mobile/check-permissions.mjs`
- `node scripts/mobile/validate-app-store.mjs --strict`
- `vitest run tests/unit/app-store-product-readiness.spec.js` (5 tests)
- Prettier check for changed JS files
- `git diff --check`
2026-07-23 16:36:35 +00:00

94 lines
4.5 KiB
JavaScript

import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { readI18nRuntimeMessages } from "./helpers/readI18nRuntimeMessages";
const root = process.cwd();
const read = (relativePath) => readFileSync(join(root, relativePath));
const readText = (relativePath) => read(relativePath).toString("utf8");
const sha256 = (relativePath) => createHash("sha256").update(read(relativePath)).digest("hex");
const pngMetadata = (relativePath) => {
const png = read(relativePath);
return {
width: png.readUInt32BE(16),
height: png.readUInt32BE(20),
colorType: png.readUInt8(25),
};
};
describe("App Store product readiness", () => {
it("ships Danish and English privacy and account-deletion disclosures", () => {
const da = readI18nRuntimeMessages("da");
const en = readI18nRuntimeMessages("en");
expect(da.privacy_policy.title).toBe("Privatlivspolitik");
expect(en.privacy_policy.title).toBe("Privacy policy");
expect(da.privacy_policy.deletion_body).toContain("Profil → Slet konto");
expect(en.privacy_policy.deletion_body).toContain("Profile → Delete account");
expect(da.user_dashboard.profile.deletion.title).toBe("Slet konto");
expect(en.user_dashboard.profile.deletion.title).toBe("Delete account");
expect(da.support.help_deletion).toContain("kontosletning");
expect(en.support.help_deletion).toContain("account deletion");
});
it("declares localized camera and location reasons plus exempt encryption use", () => {
const infoPlist = readText("ios/App/App/Info.plist");
const danishStrings = readText("ios/App/App/da.lproj/InfoPlist.strings");
const englishStrings = readText("ios/App/App/en.lproj/InfoPlist.strings");
expect(infoPlist).toContain("<key>ITSAppUsesNonExemptEncryption</key>");
expect(infoPlist).toMatch(/<key>ITSAppUsesNonExemptEncryption<\/key>\s*<false\/>/u);
expect(danishStrings).toContain('"NSCameraUsageDescription"');
expect(danishStrings).toContain('"NSLocationAlwaysAndWhenInUseUsageDescription"');
expect(danishStrings).toContain('"NSLocationWhenInUseUsageDescription"');
expect(englishStrings).toContain('"NSCameraUsageDescription"');
expect(englishStrings).toContain('"NSLocationAlwaysAndWhenInUseUsageDescription"');
expect(englishStrings).toContain('"NSLocationWhenInUseUsageDescription"');
});
it("declares linked app-functionality data without tracking", () => {
const privacyManifest = readText("ios/App/App/PrivacyInfo.xcprivacy");
for (const type of [
"NSPrivacyCollectedDataTypeName",
"NSPrivacyCollectedDataTypeEmailAddress",
"NSPrivacyCollectedDataTypePhoneNumber",
"NSPrivacyCollectedDataTypePhysicalAddress",
"NSPrivacyCollectedDataTypeUserID",
"NSPrivacyCollectedDataTypePurchaseHistory",
"NSPrivacyCollectedDataTypePaymentInfo",
"NSPrivacyCollectedDataTypePhotosorVideos",
"NSPrivacyCollectedDataTypeCustomerSupport",
"NSPrivacyCollectedDataTypeOtherUserContent",
"NSPrivacyCollectedDataTypeOtherDiagnosticData",
]) {
expect(privacyManifest).toContain(`<string>${type}</string>`);
}
expect(privacyManifest).not.toContain("NSPrivacyCollectedDataTypePreciseLocation");
expect(privacyManifest).toMatch(/<key>NSPrivacyTracking<\/key>\s*<false\/>/u);
expect(privacyManifest).not.toContain("NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising");
});
it("replaces Capacitor artwork with opaque, correctly sized Truck Wash assets", () => {
const iconPath = "ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png";
const splashPath = "ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png";
expect(pngMetadata(iconPath)).toEqual({ width: 1024, height: 1024, colorType: 2 });
expect(pngMetadata(splashPath)).toEqual({ width: 2732, height: 2732, colorType: 2 });
expect(sha256(iconPath)).not.toBe("29e4777e319de3ee5a52c3a8004ec19d0568414004257e36d7c94a077d71c93b");
expect(sha256(splashPath)).not.toBe("1b5002b74a5500e697298ced06ca2811ac33f2771f236f3c720ff23243890530");
});
it("keeps the fixed mobile header below the native iOS status area", () => {
const mobileHeader = readText("src/components/viewport/page/headers/MobileHeader.vue");
expect(mobileHeader).toContain("padding-top: var(--pleno-mobile-safe-top)");
expect(mobileHeader).toContain("height: calc(80px + var(--pleno-mobile-safe-top))");
expect(mobileHeader).toContain("min-height: calc(80px + var(--pleno-mobile-safe-top))");
});
});