Browse Source

feat(ios-testing): Enhance deeplink testing and error handling

- Improve test data generation and validation
  - Add detailed logging of generated test data
  - Implement robust validation of required fields
  - Use ts-node script for test data generation
  - Add fallback data generation with validation

- Enhance deeplink testing UX
  - Add interactive prompts between tests
  - Display detailed test progress and next steps
  - Improve error handling and test skip logic
  - Add comprehensive logging throughout test execution

- Improve DeepLinkErrorView
  - Add detailed error information display
  - Show debug information for parameters and queries
  - Enhance UI with better styling and layout
  - Add safe area spacing for iOS

- Refactor deeplink handling
  - Standardize route definitions
  - Improve parameter validation
  - Add better error logging
app_id_fix
Matthew Raymer 1 week ago
parent
commit
8be8de5f1f
  1. 1280
      package-lock.json
  2. 478
      scripts/test-ios.js
  3. 7
      src/services/deepLinks.ts
  4. 171
      src/views/DeepLinkErrorView.vue

1280
package-lock.json

File diff suppressed because it is too large

478
scripts/test-ios.js

@ -40,7 +40,21 @@
const { execSync } = require('child_process');
const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } = require('fs');
const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, readdirSync, statSync, accessSync } = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const { constants } = require('fs');
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
// Make sure to close readline at the end
process.on('SIGINT', () => {
rl.close();
process.exit();
});
// Format date as YYYY-MM-DD-HHMMSS
const getLogFileName = () => {
@ -316,122 +330,84 @@ const verifyXcodeInstallation = (log) => {
// Generate test data using generate_data.ts
const generateTestData = async (log) => {
log('🔄 Generating test data...');
log('\n🔍 DEBUG: Starting test data generation...');
// Check if test-scripts directory exists
if (!existsSync('test-scripts')) {
log('⚠️ test-scripts directory not found');
log('⚠️ Current directory: ' + process.cwd());
// List directories to help debug
const { readdirSync } = require('fs');
log('📂 Directories in current path:');
try {
const files = readdirSync('.');
files.forEach(file => {
const isDir = existsSync(file) && require('fs').statSync(file).isDirectory();
log(`${isDir ? '📁' : '📄'} ${file}`);
});
} catch (err) {
log(`⚠️ Error listing directory: ${err.message}`);
}
} else {
log('✅ Found test-scripts directory');
// Check if generate_data.ts exists
if (existsSync('test-scripts/generate_data.ts')) {
log('✅ Found generate_data.ts');
} else {
log('⚠️ generate_data.ts not found in test-scripts directory');
// List files in test-scripts to help debug
const { readdirSync } = require('fs');
log('📂 Files in test-scripts:');
try {
const files = readdirSync('test-scripts');
files.forEach(file => {
log(`📄 ${file}`);
});
} catch (err) {
log(`⚠️ Error listing test-scripts: ${err.message}`);
}
}
}
// Check directory structure
log('📁 Current directory:', process.cwd());
log('📁 Directory contents:', require('fs').readdirSync('.'));
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) {
log('📁 Creating .generated directory');
mkdirSync('.generated', { recursive: true });
}
try {
// Try to generate test data using the script
log('🔄 Running test data generation script...');
log('🔄 Attempting to run generate_data.ts...');
execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' });
log('✅ Test data generation script completed');
log('✅ Test data generation completed');
// Verify the generated files exist
// Verify and log generated files content
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying generated files:');
log('\n📝 Verifying generated files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} was not generated`);
throw new Error(`Required file ${file} was not generated`);
log(`❌ Missing file: ${file}`);
} else {
log(`${file} exists`);
const content = readFileSync(file, 'utf8');
log(`\n📄 Content of ${file}:`);
log(content);
try {
const parsed = JSON.parse(content);
if (file.includes('test-env.json')) {
log('🔑 CONTACT1_DID in test-env:', parsed.CONTACT1_DID);
}
if (file.includes('contacts.json')) {
log('👥 First contact DID:', parsed[0]?.did);
}
} catch (e) {
log(`❌ Error parsing ${file}:`, e);
}
}
}
} catch (error) {
log(`⚠️ Failed to generate test data: ${error.message}`);
log(`\n⚠️ Test data generation failed: ${error.message}`);
log('⚠️ Creating fallback test data...');
// Create minimal fallback test data
// Create fallback data with detailed logging
const fallbackTestEnv = {
"CONTACT1_DID": "did:example:123456789",
"CONTACT1_DID": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B",
"APP_URL": "https://app.timesafari.example"
};
const fallbackClaimDetails = {
"claim_id": "claim_12345",
"title": "Test Claim",
"description": "This is a test claim"
};
const fallbackContacts = [
{
"id": "contact1",
"name": "Test Contact",
"did": "did:example:123456789"
"did": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B"
}
];
// Use writeFileSync to overwrite any existing files
const { writeFileSync } = require('fs');
log('\n📝 Writing fallback data:');
log('TestEnv:', JSON.stringify(fallbackTestEnv, null, 2));
log('Contacts:', JSON.stringify(fallbackContacts, null, 2));
writeFileSync('.generated/test-env.json', JSON.stringify(fallbackTestEnv, null, 2));
writeFileSync('.generated/claim_details.json', JSON.stringify(fallbackClaimDetails, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2));
log('✅ Fallback test data created');
// Verify files were created
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying fallback files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Failed to create ${file}`);
} else {
log(`✅ Created ${file}`);
}
// Verify fallback data was written
log('\n🔍 Verifying fallback data:');
try {
const writtenTestEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8'));
const writtenContacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
log('Written TestEnv:', writtenTestEnv);
log('Written Contacts:', writtenContacts);
} catch (e) {
log('❌ Error verifying fallback data:', e);
}
}
};
@ -535,6 +511,96 @@ const runIosApp = async (log, simulator) => {
log('✅ App launched successfully');
};
const validateTestData = (log) => {
log('\n=== VALIDATING TEST DATA ===');
const generateFreshTestData = () => {
log('\n🔄 Generating fresh test data...');
try {
// Ensure .generated directory exists
if (!existsSync('.generated')) {
mkdirSync('.generated', { recursive: true });
}
// Execute the generate_data.ts script synchronously
log('Running generate_data.ts...');
execSync('npx ts-node test-scripts/generate_data.ts', {
stdio: 'inherit',
encoding: 'utf8'
});
// Read and validate the generated files
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
throw new Error('Generated files not found after running generate_data.ts');
}
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CONTACT1_DID) {
throw new Error('CONTACT1_DID missing from generated test data');
}
log('Generated test data:', {
testEnv: testEnv,
contacts: contacts
});
return { testEnv, contacts };
} catch (error) {
log('❌ Test data generation failed:', error);
throw error;
}
};
try {
// Try to read existing data or generate fresh data
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
let testData;
// If either file is missing or invalid, generate fresh data
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
testData = generateFreshTestData();
} else {
try {
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CLAIM_ID || !testEnv.CONTACT1_DID) {
log('⚠️ Existing test data missing required fields, regenerating...');
testData = generateFreshTestData();
} else {
testData = { testEnv, contacts };
}
} catch (error) {
log('⚠️ Error reading existing test data, regenerating...');
testData = generateFreshTestData();
}
}
// Final validation of data
if (!testData.testEnv.CLAIM_ID || !testData.testEnv.CONTACT1_DID) {
throw new Error('Test data validation failed even after generation');
}
log('✅ Test data validated successfully');
log('📄 Test Environment:', JSON.stringify(testData.testEnv, null, 2));
return testData;
} catch (error) {
log(`❌ Test data validation failed: ${error.message}`);
throw error;
}
};
/**
* Run deeplink tests
* Optionally tests deeplinks if the test data is available
@ -543,76 +609,65 @@ const runIosApp = async (log, simulator) => {
* @returns {Promise<void>}
*/
const runDeeplinkTests = async (log) => {
log('🔗 Starting deeplink tests...');
// Import readline module for user input
const readline = require('readline');
// Create readline interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Promisify the question method
const question = (query) => new Promise(resolve => rl.question(query, resolve));
// Register URL scheme if needed
checkAndRegisterUrlScheme(log);
// Check if test data files exist first
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('\n=== Starting Deeplink Tests ===');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} does not exist`);
log('⚠️ Skipping deeplink tests');
rl.close();
return;
}
// Validate test data before proceeding
let testEnv, contacts;
try {
({ testEnv, contacts } = validateTestData(log));
} catch (error) {
log('❌ Cannot proceed with tests due to invalid test data');
log(`Error: ${error.message}`);
log('Please ensure test data is properly generated before running tests');
process.exit(1); // Exit with error code
}
// Check if our app is actually running in the simulator
log('🔍 Checking if app is currently running in simulator...');
try {
const runningApps = execSync('xcrun simctl listapps booted').toString();
const appIdentifier = getAppIdentifier();
if (!runningApps.includes(appIdentifier)) {
log('⚠️ The app does not appear to be running in the simulator.');
const shouldLaunch = await question('Would you like to launch the app now? (y/n): ');
if (shouldLaunch.toLowerCase() === 'y' || shouldLaunch.toLowerCase() === 'yes') {
// Try launching the app again
log('🚀 Launching app in simulator...');
const simulatorInfo = JSON.parse(execSync('xcrun simctl list -j devices booted').toString());
const booted = Object.values(simulatorInfo.devices)
.flat()
.find(device => device.state === 'Booted');
if (booted) {
execSync(`npx cap run ios --target="${booted.udid}"`, { stdio: 'inherit' });
log('✅ App launched');
} else {
log('⚠️ No booted simulator found');
// Now we can safely create the deeplink tests knowing we have valid data
const deeplinkTests = [
{
url: `timesafari://claim/${testEnv.CLAIM_ID}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${testEnv.CERT_ID || testEnv.CLAIM_ID}`,
description: 'Claim certificate view'
},
{
url: `timesafari://claim-add-raw/${testEnv.RAW_CLAIM_ID || testEnv.CLAIM_ID}`,
description: 'Raw claim addition'
},
{
url: 'timesafari://did/test',
description: 'DID view with test identifier'
},
{
url: `timesafari://did/${testEnv.CONTACT1_DID}`,
description: 'DID view with contact DID'
},
{
url: (() => {
if (!testEnv?.CONTACT1_DID) {
throw new Error('Cannot construct contact-edit URL: CONTACT1_DID is missing');
}
} else {
log('⚠️ Deeplink tests require the app to be running');
log('⚠️ Please launch the app manually and restart the tests');
rl.close();
return;
}
} else {
log('✅ App is running in simulator');
const url = `timesafari://contact-edit/${testEnv.CONTACT1_DID}`;
log('Created contact-edit URL:', url);
return url;
})(),
description: 'Contact editing'
},
{
url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`,
description: 'Contacts import'
}
} catch (error) {
log(`⚠️ Unable to check if app is running: ${error.message}`);
log('⚠️ Proceeding with deeplink tests, but they may fail if app is not running');
}
];
// Log the final test configuration
log('\n5. Final Test Configuration:');
deeplinkTests.forEach((test, i) => {
log(`\nTest ${i + 1}:`);
log(`Description: ${test.description}`);
log(`URL: ${test.url}`);
});
// Show instructions for iOS security dialogs
log('\n📱 IMPORTANT: iOS Security Dialog Instructions:');
@ -627,110 +682,41 @@ const runDeeplinkTests = async (log) => {
await question('Press Enter when the app is visible and in the foreground...');
try {
// Load test data
log('📂 Loading test data from .generated directory');
let testEnv, claimDetails, contacts;
try {
const testEnvContent = readFileSync('.generated/test-env.json', 'utf8');
testEnv = JSON.parse(testEnvContent);
log('✅ Loaded test-env.json');
} catch (error) {
log(`⚠️ Failed to load test-env.json: ${error.message}`);
rl.close();
return;
}
try {
const claimDetailsContent = readFileSync('.generated/claim_details.json', 'utf8');
claimDetails = JSON.parse(claimDetailsContent);
log('✅ Loaded claim_details.json');
} catch (error) {
log(`⚠️ Failed to load claim_details.json: ${error.message}`);
rl.close();
return;
}
try {
const contactsContent = readFileSync('.generated/contacts.json', 'utf8');
contacts = JSON.parse(contactsContent);
log('✅ Loaded contacts.json');
} catch (error) {
log(`⚠️ Failed to load contacts.json: ${error.message}`);
rl.close();
return;
}
// Check if the app URL scheme is registered in the simulator
log('🔍 Checking if URL scheme is registered in simulator...');
try {
// Attempt to open a simple URL with the scheme
log('⚠️ A security dialog will appear - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "timesafari://test"`, { stdio: 'pipe' });
log('✅ URL scheme is registered and working');
} catch (error) {
const errorMessage = error.message || '';
// Check for the specific error code that indicates an unregistered URL scheme
if (errorMessage.includes('OSStatus error -10814') || errorMessage.includes('NSOSStatusErrorDomain, code=-10814')) {
log('⚠️ URL scheme "timesafari://" is not registered in the app or app is not running');
log('⚠️ The scheme was added to Info.plist but the app may need to be rebuilt');
log('⚠️ Trying to continue with tests, but they may fail');
}
}
// Wait for user confirmation before proceeding
await question('Press Enter to continue with the tests...');
// Test URLs
const deeplinkTests = [
{
url: `timesafari://claim/${claimDetails.claim_id}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${claimDetails.claim_id}`,
description: 'Claim certificate view'
},
{
url: `timesafari://claim-add-raw/${claimDetails.claim_id}`,
description: 'Raw claim addition'
},
{
url: 'timesafari://did/test',
description: 'DID view with test identifier'
},
{
url: `timesafari://did/${testEnv.CONTACT1_DID}`,
description: 'DID view with contact DID'
},
{
url: `timesafari://contact-edit/${testEnv.CONTACT1_DID}`,
description: 'Contact editing'
},
{
url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`,
description: 'Contacts import'
}
];
// Execute each test
let testsCompleted = 0;
let testsSkipped = 0;
for (const test of deeplinkTests) {
// Show upcoming test info before execution
log('\n📱 NEXT TEST:');
log('------------------------');
log(`Description: ${test.description}`);
log(`URL to test: ${test.url}`);
log('------------------------');
// Clear prompt for user action
await question('\n⏎ Press Enter to execute this test (or Ctrl+C to quit)...');
try {
log(`\n🔗 Testing deeplink: ${test.description}`);
log(`URL: ${test.url}`);
log('🚀 Executing deeplink test...');
log('⚠️ iOS SECURITY DIALOG WILL APPEAR - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "${test.url}"`, { stdio: 'pipe' });
log(`✅ Successfully executed: ${test.description}`);
testsCompleted++;
// Wait for user to press Enter before continuing to next test
// Show progress
log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`);
// If there are more tests, show the next one
if (testsCompleted < deeplinkTests.length) {
await question('Press Enter to continue to the next test...');
const nextTest = deeplinkTests[testsCompleted];
log('\n⏭️ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
} catch (deeplinkError) {
const errorMessage = deeplinkError.message || '';
@ -745,14 +731,22 @@ const runDeeplinkTests = async (log) => {
}
log('⚠️ Continuing with next test...');
// Wait for user to press Enter before continuing to next test
// Show next test info after error handling
if (testsCompleted + testsSkipped < deeplinkTests.length) {
await question('Press Enter to continue to the next test...');
const nextTest = deeplinkTests[testsCompleted + testsSkipped];
log('\n⏭️ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
}
}
log(`✅ Deeplink tests completed: ${testsCompleted} successful, ${testsSkipped} skipped`);
log('\n🎉 All deeplink tests completed!');
log(`✅ Successful: ${testsCompleted}`);
log(`⚠️ Skipped: ${testsSkipped}`);
if (testsSkipped > 0) {
log('\n📝 Note about skipped tests:');
@ -762,15 +756,9 @@ const runDeeplinkTests = async (log) => {
log('4. iOS security dialogs must be manually approved for each deeplink test');
log('5. If these conditions are met and tests still fail, check URL handling in the app code');
}
// Close readline interface
rl.close();
} catch (error) {
log(`❌ Deeplink tests setup failed: ${error.message}`);
log('⚠️ Deeplink tests might be unavailable or test data is missing');
// Close readline interface
rl.close();
// Don't rethrow the error to prevent halting the process
}
};

7
src/services/deepLinks.ts

@ -33,6 +33,7 @@ import {
deepLinkSchemas,
baseUrlSchema,
routeSchema,
DeepLinkRoute,
} from "../types/deepLinks";
import { logConsoleAndDb } from "../db";
import type { DeepLinkError } from "../interfaces/deepLinks";
@ -120,12 +121,12 @@ export class DeepLinkHandler {
"invite-one-accept": "invite-one-accept",
"contact-import": "contact-import",
"confirm-gift": "confirm-gift",
claim: "claim",
"claim": "claim",
"claim-cert": "claim-cert",
"claim-add-raw": "claim-add-raw",
"contact-edit": "contact-edit",
contacts: "contacts",
did: "did",
"contacts": "contacts",
"did": "did",
};
// First try to validate the route path

171
src/views/DeepLinkErrorView.vue

@ -1,10 +1,24 @@
<template>
<div class="deep-link-error">
<div class="safe-area-spacer"></div>
<h1>Invalid Deep Link</h1>
<div class="error-details">
<p>{{ errorMessage }}</p>
<div class="error-message">
<h3>Error Details</h3>
<p>{{ errorMessage }}</p>
<div v-if="errorCode" class="error-code">
Error Code: <span>{{ errorCode }}</span>
</div>
</div>
<div v-if="originalPath" class="original-link">
<strong>Link attempted:</strong> timesafari://{{ originalPath }}
<h3>Attempted Link</h3>
<code>timesafari://{{ formattedPath }}</code>
<div class="debug-info">
<h4>Parameters:</h4>
<pre>{{ JSON.stringify(route.params, null, 2) }}</pre>
<h4>Query:</h4>
<pre>{{ JSON.stringify(route.query, null, 2) }}</pre>
</div>
</div>
</div>
<div class="actions">
@ -17,7 +31,7 @@
<h2>Supported Deep Links</h2>
<ul>
<li v-for="route in validRoutes" :key="route">
timesafari://{{ route }}/:id
<code>timesafari://{{ route }}/:id</code>
</li>
</ul>
</div>
@ -45,6 +59,19 @@ const errorMessage = computed(
const originalPath = computed(() => route.query.originalPath as string);
const validRoutes = VALID_DEEP_LINK_ROUTES;
// Format the path and include any parameters
const formattedPath = computed(() => {
if (!originalPath.value) return '';
let path = originalPath.value.replace(/^\/+/, '');
// Log for debugging
console.log('Original Path:', originalPath.value);
console.log('Route Params:', route.params);
console.log('Route Query:', route.query);
return path;
});
// Navigation methods
const goHome = () => router.replace({ name: "home" });
const reportIssue = () => {
@ -60,8 +87,144 @@ const reportIssue = () => {
// Log the error for analytics
onMounted(() => {
logConsoleAndDb(
`[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}`,
`[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}`,
true,
);
});
</script>
<style scoped>
.deep-link-error {
padding-top: 60px;
padding-left: 20px;
padding-right: 20px;
max-width: 600px;
margin: 0 auto;
}
.safe-area-spacer {
height: env(safe-area-inset-top);
}
h1 {
color: #ff4444;
margin-bottom: 24px;
}
h2, h3 {
color: #333;
margin-bottom: 12px;
}
.error-details {
background-color: #f8f8f8;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.error-message {
margin-bottom: 20px;
}
.error-message p {
color: #666;
line-height: 1.5;
margin-bottom: 12px;
}
.error-code {
font-family: monospace;
color: #666;
margin-top: 8px;
}
.error-code span {
background-color: #eee;
padding: 2px 6px;
border-radius: 4px;
}
.original-link {
padding: 12px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 6px;
}
.original-link code {
color: #0066cc;
font-family: monospace;
word-break: break-all;
}
.actions {
margin: 24px 0;
display: flex;
gap: 12px;
}
.actions button {
padding: 10px 20px;
border-radius: 6px;
border: none;
font-weight: 500;
cursor: pointer;
}
.primary-button {
background-color: #007aff;
color: white;
}
.secondary-button {
background-color: #f2f2f2;
color: #333;
}
.supported-links {
margin-top: 32px;
}
.supported-links ul {
list-style: none;
padding: 0;
}
.supported-links li {
padding: 8px 12px;
background-color: #f8f8f8;
border-radius: 4px;
margin-bottom: 8px;
}
.supported-links code {
font-family: monospace;
color: #0066cc;
}
.debug-info {
margin-top: 16px;
padding: 12px;
background-color: #f0f0f0;
border-radius: 4px;
}
.debug-info h4 {
margin: 8px 0;
color: #666;
font-size: 14px;
}
.debug-info pre {
white-space: pre-wrap;
word-break: break-all;
font-family: monospace;
font-size: 12px;
color: #333;
background-color: #fff;
padding: 8px;
border-radius: 4px;
margin: 4px 0;
}
</style>

Loading…
Cancel
Save