diff --git a/packages/wasm-sdk/.gitignore b/packages/wasm-sdk/.gitignore index 7cc3597a029..755421dbad0 100644 --- a/packages/wasm-sdk/.gitignore +++ b/packages/wasm-sdk/.gitignore @@ -1,3 +1,6 @@ playwright-report/ test-results/ test/test-report.html + +# Environment variables with sensitive test data +test/ui-automation/.env diff --git a/packages/wasm-sdk/api-definitions.json b/packages/wasm-sdk/api-definitions.json index 4e959b3c527..7bc120746b9 100644 --- a/packages/wasm-sdk/api-definitions.json +++ b/packages/wasm-sdk/api-definitions.json @@ -2079,7 +2079,7 @@ "required": true }, { - "name": "identityId", + "name": "frozenIdentityId", "type": "text", "label": "Identity ID whose frozen tokens to destroy", "required": true diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index a830f4dbadf..d01c288e0c3 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -3248,7 +3248,7 @@

Results

result = await sdk.tokenDestroyFrozen( values.contractId, Number(values.tokenPosition), - values.identityId, // identity whose frozen tokens to destroy + values.frozenIdentityId, // identity whose frozen tokens to destroy identityId, // destroyer ID privateKey, values.publicNote || null @@ -3343,7 +3343,7 @@

Results

values.documentType, values.documentId, identityId, - values.price || 0, // price in credits, 0 to remove price + BigInt(values.price || 0), // price in credits, 0 to remove price privateKey, 0 // key_id - using 0 as default ); @@ -3477,7 +3477,7 @@

Results

values.documentType, values.documentId, identityId, - values.price, + BigInt(values.price), privateKey, 0 // key_id - using 0 as default ); diff --git a/packages/wasm-sdk/src/state_transitions/documents/mod.rs b/packages/wasm-sdk/src/state_transitions/documents/mod.rs index 7bde0a1459a..8a186f2c8fa 100644 --- a/packages/wasm-sdk/src/state_transitions/documents/mod.rs +++ b/packages/wasm-sdk/src/state_transitions/documents/mod.rs @@ -185,6 +185,15 @@ impl WasmSdk { Ok(result_obj.into()) } + + /// Get the next revision for a document, handling errors for missing revisions and overflow + fn get_next_revision(document: &dash_sdk::platform::Document) -> Result { + let current_revision = document.revision() + .ok_or_else(|| JsValue::from_str("Document revision is missing"))?; + + current_revision.checked_add(1) + .ok_or_else(|| JsValue::from_str("Document revision overflow")) + } } #[wasm_bindgen] @@ -835,7 +844,8 @@ impl WasmSdk { .map_err(|e| JsValue::from_str(&format!("Failed to fetch document: {}", e)))? .ok_or_else(|| JsValue::from_str("Document not found"))?; - let current_revision = existing_doc.revision().unwrap_or(0); + let current_revision = existing_doc.revision() + .ok_or_else(|| JsValue::from_str("Document revision is missing"))?; // Fetch the identity to get the correct key let identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) @@ -965,6 +975,26 @@ impl WasmSdk { .map_err(|e| JsValue::from_str(&format!("Failed to fetch document: {}", e)))? .ok_or_else(|| JsValue::from_str("Document not found"))?; + // Get the current revision and increment it + let next_revision = Self::get_next_revision(&document)?; + + // Create a modified document with incremented revision for the transfer transition + let transfer_document = Document::V0(DocumentV0 { + id: document.id(), + owner_id: document.owner_id(), + properties: document.properties().clone(), + revision: Some(next_revision), + created_at: document.created_at(), + updated_at: document.updated_at(), + transferred_at: document.transferred_at(), + created_at_block_height: document.created_at_block_height(), + updated_at_block_height: document.updated_at_block_height(), + transferred_at_block_height: document.transferred_at_block_height(), + created_at_core_block_height: document.created_at_core_block_height(), + updated_at_core_block_height: document.updated_at_core_block_height(), + transferred_at_core_block_height: document.transferred_at_core_block_height(), + }); + // Fetch the identity to get the correct key let identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) .await @@ -983,7 +1013,7 @@ impl WasmSdk { // Create a transfer transition let transition = BatchTransition::new_document_transfer_transition_from_document( - document, + transfer_document, document_type_ref, recipient_identifier, matching_key, @@ -1089,6 +1119,26 @@ impl WasmSdk { ))); } + // Get the current revision and increment it + let next_revision = Self::get_next_revision(&document)?; + + // Create a modified document with incremented revision for the purchase transition + let purchase_document = Document::V0(DocumentV0 { + id: document.id(), + owner_id: document.owner_id(), + properties: document.properties().clone(), + revision: Some(next_revision), + created_at: document.created_at(), + updated_at: document.updated_at(), + transferred_at: document.transferred_at(), + created_at_block_height: document.created_at_block_height(), + updated_at_block_height: document.updated_at_block_height(), + transferred_at_block_height: document.transferred_at_block_height(), + created_at_core_block_height: document.created_at_core_block_height(), + updated_at_core_block_height: document.updated_at_core_block_height(), + transferred_at_core_block_height: document.transferred_at_core_block_height(), + }); + // Fetch buyer identity let buyer_identity = dash_sdk::platform::Identity::fetch(&sdk, buyer_identifier) .await @@ -1107,7 +1157,7 @@ impl WasmSdk { // Create document purchase transition let transition = BatchTransition::new_document_purchase_transition_from_document( - document.into(), + purchase_document, document_type_ref, buyer_identifier, price as Credits, @@ -1226,25 +1276,15 @@ impl WasmSdk { return Err(JsValue::from_str("Only the document owner can set its price")); } - // Get existing document properties and convert to mutable map - let mut properties = existing_doc.properties().clone(); - - // Update the price in the document properties - let price_value = if price > 0 { - PlatformValue::U64(price) - } else { - PlatformValue::Null - }; + // Get the current revision and increment it + let next_revision = Self::get_next_revision(&existing_doc)?; - properties.insert("$price".to_string(), price_value); - - // Create updated document with new properties - let new_revision = existing_doc.revision().unwrap_or(0) + 1; - let updated_doc = Document::V0(DocumentV0 { - id: doc_id, - owner_id: owner_identifier, - properties, - revision: Some(new_revision), + // Create a modified document with incremented revision for the price update transition + let price_update_document = Document::V0(DocumentV0 { + id: existing_doc.id(), + owner_id: existing_doc.owner_id(), + properties: existing_doc.properties().clone(), + revision: Some(next_revision), created_at: existing_doc.created_at(), updated_at: existing_doc.updated_at(), transferred_at: existing_doc.transferred_at(), @@ -1272,22 +1312,12 @@ impl WasmSdk { .await .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; - // Generate entropy for the state transition - let entropy_bytes = { - let mut entropy = [0u8; 32]; - if let Some(window) = web_sys::window() { - if let Ok(crypto) = window.crypto() { - let _ = crypto.get_random_values_with_u8_array(&mut entropy); - } - } - entropy - }; - - // Create the price update transition - let transition = BatchTransition::new_document_replacement_transition_from_document( - updated_doc, + // Create the price update transition using the dedicated method + let transition = BatchTransition::new_document_update_price_transition_from_document( + price_update_document, document_type_ref, - matching_key, + price, + &matching_key, identity_contract_nonce, UserFeeIncrease::default(), None, // token_payment_info @@ -1295,7 +1325,7 @@ impl WasmSdk { sdk.version(), None, // options ) - .map_err(|e| JsValue::from_str(&format!("Failed to create transition: {}", e)))?; + .map_err(|e| JsValue::from_str(&format!("Failed to create price update transition: {}", e)))?; // The transition is already signed, convert to StateTransition let state_transition: StateTransition = transition.into(); diff --git a/packages/wasm-sdk/test/ui-automation/.env.example b/packages/wasm-sdk/test/ui-automation/.env.example new file mode 100644 index 00000000000..ee0b0a1ea4e --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/.env.example @@ -0,0 +1,12 @@ +# Test Credentials for WASM SDK UI Tests +# Copy this file to .env and fill with real values + +# Private keys for state transitions (DON'T STORE in production) +TEST_PRIVATE_KEY_IDENTITY_1=YOUR_IDENTITY_PRIVATE_KEY_HERE +TEST_PRIVATE_KEY_TRANSFER=YOUR_TRANSFER_PRIVATE_KEY_HERE +TEST_PRIVATE_KEY_CONTRACT=YOUR_CONTRACT_PRIVATE_KEY_HERE +# Secondary private key (used by some document/token transitions) +TEST_PRIVATE_KEY_SECONDARY=YOUR_TEST_PRIVATE_KEY_SECONDARY + +# Seed phrases for identity creation (not implemented yet) +TEST_SEED_PHRASE_1="your seed phrase here" diff --git a/packages/wasm-sdk/test/ui-automation/fixtures/test-data.js b/packages/wasm-sdk/test/ui-automation/fixtures/test-data.js index f94999c18ad..b1b731e1e43 100644 --- a/packages/wasm-sdk/test/ui-automation/fixtures/test-data.js +++ b/packages/wasm-sdk/test/ui-automation/fixtures/test-data.js @@ -3,6 +3,9 @@ * Based on update_inputs.py and existing test files */ +// Load environment variables for sensitive test data +require('dotenv').config({ path: require('path').join(__dirname, '../.env') }); + const testData = { // Known testnet identity IDs for testing (from WASM SDK docs and tests) identityIds: { @@ -109,7 +112,7 @@ const testData = { ], contractId: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", documentTypeName: "domain", - keyRequestType: "all" + purposes: ["0", "3"] // Authentication and Transfer } ] }, @@ -493,6 +496,302 @@ const testData = { } }, + // State transition test parameters organized by category + stateTransitionParameters: { + identity: { + identityCreate: { + testnet: [ + { + seedPhrase: process.env.TEST_SEED_PHRASE_1 || "placeholder seed phrase", + identityIndex: 0, + keySelectionMode: "simple", + assetLockProof: "a914b7e904ce25ed97594e72f7af0e66f298031c175487", + privateKey: process.env.TEST_PRIVATE_KEY_IDENTITY_1 || "PLACEHOLDER_IDENTITY_KEY_1", + expectedKeys: 2, + description: "Test identity creation with standard seed phrase" + } + ] + }, + identityTopUp: { + testnet: [ + { + identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + assetLockProof: "a914b7e904ce25ed97594e72f7af0e66f298031c175487", + privateKey: process.env.TEST_PRIVATE_KEY_IDENTITY_1 || "PLACEHOLDER_IDENTITY_KEY_1", + description: "Top up existing identity with credits" + } + ] + }, + identityCreditTransfer: { + testnet: [ + { + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + recipientId: "HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG", + amount: 100000, // 0.000001 DASH in credits + privateKey: process.env.TEST_PRIVATE_KEY_TRANSFER || "PLACEHOLDER_TRANSFER_KEY", // Transfer key + description: "Transfer credits between identities" + } + ] + }, + identityCreditWithdrawal: { + testnet: [ + { + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + toAddress: "yQW6TmUFef5CDyhEYwjoN8aUTMmKLYYNDm", + amount: 190000, // 0.0000019 DASH in credits (minimum withdrawal amount) + privateKey: process.env.TEST_PRIVATE_KEY_TRANSFER || "PLACEHOLDER_TRANSFER_KEY", + description: "Withdraw credits to Dash address" + } + ] + } + }, + dataContract: { + dataContractCreate: { + testnet: [ + { + canBeDeleted: false, + readonly: false, + keepsHistory: false, + documentSchemas: '{"note": {"type": "object", "properties": {"message": {"type": "string", "position": 0}}, "additionalProperties": false}}', + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + description: "Create simple test data contract with document schema" + } + ] + }, + dataContractUpdate: { + testnet: [ + { + dataContractId: "5kMgvQ9foEQ9TzDhz5jvbJ9Lhv5qqBpUeYEezHNEa6Ti", // Sample contract ID + newDocumentSchemas: '{"note": {"type": "object", "properties": {"message": {"type": "string", "position": 0}, "author": {"type": "string", "position": 1}}, "additionalProperties": false}}', + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + description: "Update existing note document schema to add author field" + } + ] + } + }, + document: { + documentCreate: { + testnet: [ + { + contractId: "5kMgvQ9foEQ9TzDhz5jvbJ9Lhv5qqBpUeYEezHNEa6Ti", // Use simple note contract (will be created by dataContractCreate test) + documentType: "note", + documentFields: { + message: "Document created for WASM-SDK UI testing" + }, + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + description: "Create test note document with simple schema" + } + ] + }, + documentReplace: { + testnet: [ + { + contractId: "5kMgvQ9foEQ9TzDhz5jvbJ9Lhv5qqBpUeYEezHNEa6Ti", // Use simple note contract + documentType: "note", + documentId: "Dy19ZeYPpqbEDcpsPcLwkviY5GZqT7yJL2EY4YfxTYjn", // Persistent testnet document + documentFields: { + message: "Updated document message for automation testing" + }, + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + description: "Replace existing note document" + } + ] + }, + documentDelete: { + testnet: [ + { + contractId: "5kMgvQ9foEQ9TzDhz5jvbJ9Lhv5qqBpUeYEezHNEa6Ti", // Use simple note contract + documentType: "note", + documentId: "PLACEHOLDER_DOCUMENT_ID", // Will be set dynamically + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + description: "Delete existing note document" + } + ] + }, + documentTransfer: { + testnet: [ + { + identityId: "HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG", // Current owner + privateKey: process.env.TEST_PRIVATE_KEY_SECONDARY || "PLACEHOLDER_CONTRACT_KEY", + contractId: "HdRFTcxgwPSVgzdy6MTYutDLJdbpfLMXwuBaYLYKMVHv", // Use NFT contract + documentType: "card", + documentId: "EypPkQLgT6Jijht7NYs4jmK5TGzkNd1Z4WrQdH1hND59", // Existing trading card document + recipientId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", // Transfer recipient + description: "Transfer trading card ownership to secondary identity" + } + ] + }, + documentPurchase: { + testnet: [ + { + identityId: "HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG", // Buyer identity + privateKey: process.env.TEST_PRIVATE_KEY_SECONDARY || "PLACEHOLDER_SECONDARY_KEY", + contractId: "HdRFTcxgwPSVgzdy6MTYutDLJdbpfLMXwuBaYLYKMVHv", // Use NFT contract + documentType: "card", + documentId: "EypPkQLgT6Jijht7NYs4jmK5TGzkNd1Z4WrQdH1hND59", // Existing trading card document + price: 1000, // Price in credits + description: "Purchase a priced trading card with secondary identity" + } + ] + }, + documentSetPrice: { + testnet: [ + { + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", // Primary identity owns card after creation + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + contractId: "HdRFTcxgwPSVgzdy6MTYutDLJdbpfLMXwuBaYLYKMVHv", // Use NFT contract + documentType: "card", + documentId: "EypPkQLgT6Jijht7NYs4jmK5TGzkNd1Z4WrQdH1hND59", // Existing trading card document + price: 1000, // Price in credits + description: "Set price for a trading card" + } + ] + } + }, + token: { + tokenMint: { + testnet: [ + { + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + amount: "2", + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + // issuedToIdentityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + publicNote: "Token mint test", + description: "Mint new tokens (may fail without permissions)" + } + ] + }, + tokenTransfer: { + testnet: [ + { + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + amount: "1", + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + recipientId: "HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG", + publicNote: "Token transfer test", + description: "Transfer tokens between identities" + } + ] + }, + tokenBurn: { + testnet: [ + { + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + amount: "1", + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + publicNote: "Token burn test", + description: "Burn tokens from identity balance" + } + ] + }, + tokenFreeze: { + testnet: [ + { + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + identityToFreeze: "HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + publicNote: "Token freeze test", + description: "Freeze tokens for an identity" + } + ] + }, + tokenDestroyFrozen: { + testnet: [ + { + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + frozenIdentityId: "HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + publicNote: "Destroy frozen tokens test", + description: "Destroy frozen tokens from an identity" + } + ] + }, + tokenUnfreeze: { + testnet: [ + { + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + identityToUnfreeze: "HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + publicNote: "Token unfreeze test", + description: "Unfreeze tokens for an identity" + } + ] + }, + tokenClaim: { + testnet: [ + { + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + distributionType: "perpetual", + publicNote: "Token claim test", + description: "Claim tokens from distribution" + } + ] + }, + tokenSetPriceForDirectPurchase: { + testnet: [ + { + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + priceType: "single", + priceData: "10", + publicNote: "Set token price test", + description: "Set price for direct token purchases" + } + ] + }, + tokenDirectPurchase: { + testnet: [ + { + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + amount: "1", + totalAgreedPrice: "10", + keyId: 0, + description: "Direct purchase of tokens at configured price" + } + ] + }, + tokenConfigUpdate: { + testnet: [ + { + identityId: "7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC", + privateKey: process.env.TEST_PRIVATE_KEY_CONTRACT || "PLACEHOLDER_CONTRACT_KEY", + contractId: "Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i", + tokenPosition: 0, + configItemType: "max_supply", + configValue: "1000000", + publicNote: "Update max supply test", + description: "Update token configuration max supply" + } + ] + }, + } + }, + // Common where clauses for document queries whereClausesExamples: { dpnsDomain: [ @@ -543,8 +842,45 @@ function getAllTestParameters(category, queryType, network = 'testnet') { return queryData[network] || []; } +/** + * Get test parameters for a specific state transition + */ +function getStateTransitionParameters(category, transitionType, network = 'testnet') { + const categoryData = testData.stateTransitionParameters[category]; + if (!categoryData) { + throw new Error(`No state transition test data found for category: ${category}`); + } + + const transitionData = categoryData[transitionType]; + if (!transitionData) { + throw new Error(`No state transition test data found for transition: ${category}.${transitionType}`); + } + + const networkData = transitionData[network]; + if (!networkData || networkData.length === 0) { + throw new Error(`No state transition test data found for ${category}.${transitionType} on ${network}`); + } + + return networkData[0]; // Return first test case +} + +/** + * Get all test parameters for a state transition (for parameterized testing) + */ +function getAllStateTransitionParameters(category, transitionType, network = 'testnet') { + const categoryData = testData.stateTransitionParameters[category]; + if (!categoryData) return []; + + const transitionData = categoryData[transitionType]; + if (!transitionData) return []; + + return transitionData[network] || []; +} + module.exports = { testData, getTestParameters, - getAllTestParameters + getAllTestParameters, + getStateTransitionParameters, + getAllStateTransitionParameters }; diff --git a/packages/wasm-sdk/test/ui-automation/package-lock.json b/packages/wasm-sdk/test/ui-automation/package-lock.json index 7c1d8959081..86e0d8971d3 100644 --- a/packages/wasm-sdk/test/ui-automation/package-lock.json +++ b/packages/wasm-sdk/test/ui-automation/package-lock.json @@ -7,8 +7,11 @@ "": { "name": "wasm-sdk-ui-automation", "version": "1.0.0", + "dependencies": { + "dotenv": "^17.2.1" + }, "devDependencies": { - "@playwright/test": "^1.41.0" + "@playwright/test": "^1.54.1" }, "engines": { "node": ">=18" @@ -30,6 +33,18 @@ "node": ">=18" } }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", diff --git a/packages/wasm-sdk/test/ui-automation/package.json b/packages/wasm-sdk/test/ui-automation/package.json index 8785b6257c4..78bcefca2f5 100644 --- a/packages/wasm-sdk/test/ui-automation/package.json +++ b/packages/wasm-sdk/test/ui-automation/package.json @@ -24,5 +24,8 @@ }, "engines": { "node": ">=18" + }, + "dependencies": { + "dotenv": "^17.2.1" } } diff --git a/packages/wasm-sdk/test/ui-automation/playwright.config.js b/packages/wasm-sdk/test/ui-automation/playwright.config.js index 4e8c47a7400..5119dde4d90 100644 --- a/packages/wasm-sdk/test/ui-automation/playwright.config.js +++ b/packages/wasm-sdk/test/ui-automation/playwright.config.js @@ -6,14 +6,10 @@ const { defineConfig, devices } = require('@playwright/test'); */ module.exports = defineConfig({ testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['html', { outputFolder: 'playwright-report' }], @@ -41,10 +37,13 @@ module.exports = defineConfig({ navigationTimeout: 30000, }, - /* Configure projects for major browsers */ + /* Configure projects for different test execution modes */ projects: [ { - name: 'chromium', + name: 'parallel-tests', + testMatch: ['basic-smoke.spec.js', 'query-execution.spec.js', 'parameterized-queries.spec.js'], + fullyParallel: true, + workers: process.env.CI ? 1 : undefined, use: { ...devices['Desktop Chrome'], // Enable headless mode by default @@ -53,6 +52,21 @@ module.exports = defineConfig({ viewport: { width: 1920, height: 1080 } }, }, + // Skip state transitions tests in CI environments + // These are very slow-running due to https://github.com/dashpay/platform/issues/2736 + ...(process.env.CI ? [] : [{ + name: 'sequential-tests', + testMatch: ['state-transitions.spec.js'], + fullyParallel: false, // Tests in file run in order + workers: 1, // Single worker for sequential execution + use: { + ...devices['Desktop Chrome'], + // Enable headless mode by default + headless: true, + // Use a larger viewport for better testing + viewport: { width: 1920, height: 1080 } + }, + }]), ], /* Run your local dev server before starting the tests */ diff --git a/packages/wasm-sdk/test/ui-automation/tests/query-execution.spec.js b/packages/wasm-sdk/test/ui-automation/tests/query-execution.spec.js index cfa16105560..b375d8f5dde 100644 --- a/packages/wasm-sdk/test/ui-automation/tests/query-execution.spec.js +++ b/packages/wasm-sdk/test/ui-automation/tests/query-execution.spec.js @@ -195,6 +195,27 @@ function validateKeysResult(resultStr) { }); } +function validateIdentitiesContractKeysResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const contractKeysData = JSON.parse(resultStr); + expect(contractKeysData).toBeDefined(); + expect(Array.isArray(contractKeysData)).toBe(true); + + contractKeysData.forEach(identityResult => { + expect(identityResult).toHaveProperty('identityId'); + expect(identityResult).toHaveProperty('keys'); + expect(Array.isArray(identityResult.keys)).toBe(true); + + identityResult.keys.forEach(key => { + expect(key).toHaveProperty('keyId'); + expect(key).toHaveProperty('purpose'); + expect(key).toHaveProperty('keyType'); + expect(key).toHaveProperty('publicKeyData'); + expect(key).toHaveProperty('securityLevel'); + }); + }); +} + function validateIdentitiesResult(resultStr) { expect(() => JSON.parse(resultStr)).not.toThrow(); const identitiesData = JSON.parse(resultStr); @@ -1270,7 +1291,7 @@ test.describe('WASM SDK Query Execution Tests', () => { { name: 'getIdentityNonce', hasProofSupport: true, validateFn: (result) => validateNumericResult(result, 'nonce') }, { name: 'getIdentityContractNonce', hasProofSupport: true, validateFn: (result) => validateNumericResult(result, 'nonce') }, { name: 'getIdentityByPublicKeyHash', hasProofSupport: true, validateFn: validateIdentityResult }, - { name: 'getIdentitiesContractKeys', hasProofSupport: true, validateFn: validateKeysResult }, + { name: 'getIdentitiesContractKeys', hasProofSupport: true, validateFn: validateIdentitiesContractKeysResult }, { name: 'getIdentitiesBalances', hasProofSupport: true, validateFn: validateBalancesResult }, { name: 'getIdentityBalanceAndRevision', hasProofSupport: true, validateFn: validateBalanceAndRevisionResult }, { name: 'getIdentityByNonUniquePublicKeyHash', hasProofSupport: true, validateFn: validateIdentitiesResult }, diff --git a/packages/wasm-sdk/test/ui-automation/tests/state-transitions.spec.js b/packages/wasm-sdk/test/ui-automation/tests/state-transitions.spec.js new file mode 100644 index 00000000000..f0f80a0841a --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/tests/state-transitions.spec.js @@ -0,0 +1,1474 @@ +const { test, expect } = require('@playwright/test'); +const { WasmSdkPage } = require('../utils/wasm-sdk-page'); +const { ParameterInjector } = require('../utils/parameter-injector'); + +/** + * Helper function to execute a state transition + * @param {WasmSdkPage} wasmSdkPage - The page object instance + * @param {ParameterInjector} parameterInjector - The parameter injector instance + * @param {string} category - State transition category (e.g., 'identity', 'dataContract') + * @param {string} transitionType - Transition type (e.g., 'identityCreate') + * @param {string} network - Network to use ('testnet' or 'mainnet') + * @returns {Promise} - The transition result object + */ +async function executeStateTransition(wasmSdkPage, parameterInjector, category, transitionType, network = 'testnet') { + await wasmSdkPage.setupStateTransition(category, transitionType); + + const success = await parameterInjector.injectStateTransitionParameters(category, transitionType, network); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + return result; +} + +/** + * Helper function to validate basic state transition result properties + * @param {Object} result - The state transition result object + */ +function validateBasicStateTransitionResult(result) { + // Check for withdrawal-specific minimum amount error + if (!result.success && result.result && result.result.includes('Missing response message')) { + console.error('⚠️ Withdrawal may have failed due to insufficient amount. Minimum withdrawal is ~190,000 credits.'); + console.error('Full error:', result.result); + } + + expect(result.success).toBe(true); + expect(result.result).toBeDefined(); + expect(result.hasError).toBe(false); + expect(result.result).not.toContain('Error executing'); + expect(result.result).not.toContain('invalid'); + expect(result.result).not.toContain('failed'); +} + +/** + * Filter out placeholder options from dropdown arrays + * @param {string[]} options - Array of dropdown options + * @returns {string[]} - Filtered array without placeholders + */ +function filterPlaceholderOptions(options) { + return options.filter(option => + !option.toLowerCase().includes('select') && + option.trim() !== '' + ); +} + +/** + * Parse and validate JSON response structure + * @param {string} resultStr - The raw result string + * @returns {Object} - The parsed contract data + */ +function parseContractResponse(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const contractData = JSON.parse(resultStr); + expect(contractData).toBeDefined(); + expect(contractData).toBeInstanceOf(Object); + expect(contractData.status).toBe('success'); + expect(contractData.contractId).toBeDefined(); + expect(contractData.version).toBeDefined(); + expect(typeof contractData.version).toBe('number'); + expect(contractData.message).toBeDefined(); + return contractData; +} + +/** + * Helper function to validate data contract result (both create and update) + * @param {string} resultStr - The raw result string from data contract operation + * @param {boolean} isUpdate - Whether this is an update operation (default: false for create) + * @returns {Object} - The parsed contract data for further use + */ +function validateDataContractResult(resultStr, isUpdate = false) { + const contractData = parseContractResponse(resultStr); + + // Conditional validations based on operation type + if (isUpdate) { + // Update: only has version and message specifics + expect(contractData.version).toBeGreaterThan(1); // Updates should increment version + expect(contractData.message).toContain('updated successfully'); + } else { + // Create: has additional fields that updates don't have + expect(contractData.ownerId).toBeDefined(); + expect(contractData.documentTypes).toBeDefined(); + expect(Array.isArray(contractData.documentTypes)).toBe(true); + expect(contractData.version).toBe(1); // Creates start at version 1 + expect(contractData.message).toContain('created successfully'); + } + + return contractData; +} + +/** + * Helper function to validate document creation result + * @param {string} resultStr - The raw result string from document creation + */ +function validateDocumentCreateResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const documentResponse = JSON.parse(resultStr); + expect(documentResponse).toBeDefined(); + expect(documentResponse).toBeInstanceOf(Object); + + // Validate the response structure for document creation + expect(documentResponse.type).toBe('DocumentCreated'); + expect(documentResponse.documentId).toBeDefined(); + expect(typeof documentResponse.documentId).toBe('string'); + expect(documentResponse.documentId.length).toBeGreaterThan(0); + + // Validate the document object + expect(documentResponse.document).toBeDefined(); + expect(documentResponse.document.id).toBe(documentResponse.documentId); + expect(documentResponse.document.ownerId).toBeDefined(); + expect(documentResponse.document.dataContractId).toBeDefined(); + expect(documentResponse.document.documentType).toBeDefined(); + expect(documentResponse.document.revision).toBe(1); // New documents start at revision 1 + expect(documentResponse.document.data).toBeDefined(); + expect(typeof documentResponse.document.data).toBe('object'); + + return documentResponse; +} + +/** + * Helper function to validate document replace result + * @param {string} resultStr - The raw result string from document replacement + * @param {string} expectedDocumentId - Expected document ID to validate against + * @param {number} expectedMinRevision - Minimum expected revision (should be > 1) + */ +function validateDocumentReplaceResult(resultStr, expectedDocumentId, expectedMinRevision = 2) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const replaceResponse = JSON.parse(resultStr); + expect(replaceResponse).toBeDefined(); + expect(replaceResponse).toBeInstanceOf(Object); + + // Validate the response structure for document replacement + expect(replaceResponse.type).toBe('DocumentReplaced'); + expect(replaceResponse.documentId).toBe(expectedDocumentId); + expect(replaceResponse.document).toBeDefined(); + + // Validate the document object matches the expected structure + expect(replaceResponse.document.id).toBe(expectedDocumentId); + expect(replaceResponse.document.ownerId).toBeDefined(); + expect(replaceResponse.document.dataContractId).toBeDefined(); + expect(replaceResponse.document.documentType).toBeDefined(); + expect(replaceResponse.document.revision).toBeGreaterThanOrEqual(expectedMinRevision); + expect(replaceResponse.document.data).toBeDefined(); + expect(typeof replaceResponse.document.data).toBe('object'); + + console.log(`✅ Confirmed replacement of document: ${expectedDocumentId} (revision: ${replaceResponse.document.revision})`); + + return replaceResponse; +} + +/** + * Helper function to validate document deletion result + * @param {string} resultStr - The raw result string from document deletion + * @param {string} expectedDocumentId - Optional expected document ID to validate against + */ +function validateDocumentDeleteResult(resultStr, expectedDocumentId = null) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const deleteResponse = JSON.parse(resultStr); + expect(deleteResponse).toBeDefined(); + expect(deleteResponse).toBeInstanceOf(Object); + + // Validate the response structure for document deletion + expect(deleteResponse.type).toBe('DocumentDeleted'); + expect(deleteResponse.documentId).toBeDefined(); + expect(typeof deleteResponse.documentId).toBe('string'); + expect(deleteResponse.documentId.length).toBeGreaterThan(0); + expect(deleteResponse.deleted).toBe(true); + + // If expectedDocumentId is provided, verify it matches the response + if (expectedDocumentId) { + expect(deleteResponse.documentId).toBe(expectedDocumentId); + console.log(`Confirmed deletion of correct document: ${expectedDocumentId}`); + } + + return deleteResponse; +} + +/** + * Helper function to validate identity credit transfer result + * @param {string} resultStr - The raw result string from identity credit transfer + * @param {string} expectedSenderId - Expected sender identity ID + * @param {string} expectedRecipientId - Expected recipient identity ID + * @param {number} expectedAmount - Expected transfer amount + */ +function validateIdentityCreditTransferResult(resultStr, expectedSenderId, expectedRecipientId, expectedAmount) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const transferResponse = JSON.parse(resultStr); + expect(transferResponse).toBeDefined(); + expect(transferResponse).toBeInstanceOf(Object); + + // Validate the response structure for identity credit transfer + expect(transferResponse.status).toBe('success'); + expect(transferResponse.senderId).toBe(expectedSenderId); + expect(transferResponse.recipientId).toBe(expectedRecipientId); + expect(transferResponse.amount).toBe(expectedAmount); + expect(transferResponse.message).toBeDefined(); + + console.log(`✅ Confirmed credit transfer: ${expectedAmount} credits from ${expectedSenderId} to ${expectedRecipientId}`); + + return transferResponse; +} + +/** + * Helper function to validate identity credit withdrawal result + * @param {string} resultStr - The raw result string from identity credit withdrawal + * @param {string} expectedIdentityId - Expected identity ID + * @param {string} expectedWithdrawalAddress - Expected withdrawal address + * @param {number} expectedAmount - Expected withdrawal amount + */ +function validateIdentityCreditWithdrawalResult(resultStr, expectedIdentityId, expectedWithdrawalAddress, expectedAmount) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const withdrawalResponse = JSON.parse(resultStr); + expect(withdrawalResponse).toBeDefined(); + expect(withdrawalResponse).toBeInstanceOf(Object); + + // Validate the response structure for identity credit withdrawal + expect(withdrawalResponse.status).toBe('success'); + expect(withdrawalResponse.identityId).toBe(expectedIdentityId); + expect(withdrawalResponse.toAddress).toBe(expectedWithdrawalAddress); + expect(withdrawalResponse.amount).toBeDefined(); // Amount might be different due to fees + expect(withdrawalResponse.remainingBalance).toBeDefined(); + expect(withdrawalResponse.message).toContain('withdrawn successfully'); + + console.log(`✅ Confirmed credit withdrawal: ${withdrawalResponse.amount} credits from ${expectedIdentityId} to ${expectedWithdrawalAddress}`); + + return withdrawalResponse; +} + +/** + * Helper function to validate token mint result + * @param {string} resultStr - The raw result string from token mint + * @param {string} expectedIdentityId - Expected identity ID + * @param {string} expectedAmount - Expected mint amount + */ +function validateTokenMintResult(resultStr, expectedIdentityId, expectedAmount) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const mintResponse = JSON.parse(resultStr); + expect(mintResponse).toBeDefined(); + expect(mintResponse).toBeInstanceOf(Object); + + // Token mint returns an empty object {} on success + // This indicates the transaction was submitted successfully + console.log(`✅ Token mint transaction submitted successfully: ${expectedAmount} tokens to ${expectedIdentityId}`); + + return mintResponse; +} + +/** + * Helper function to validate token transfer result + * @param {string} resultStr - The raw result string from token transfer + * @param {string} expectedSenderId - Expected sender identity ID + * @param {string} expectedRecipientId - Expected recipient identity ID + * @param {string} expectedAmount - Expected transfer amount + */ +function validateTokenTransferResult(resultStr, expectedSenderId, expectedRecipientId, expectedAmount) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const transferResponse = JSON.parse(resultStr); + expect(transferResponse).toBeDefined(); + expect(transferResponse).toBeInstanceOf(Object); + + // Token transfer returns an empty object {} on success + // This indicates the transaction was submitted successfully + console.log(`✅ Token transfer transaction submitted successfully: ${expectedAmount} tokens from ${expectedSenderId} to ${expectedRecipientId}`); + + return transferResponse; +} + +/** + * Helper function to validate token burn result + * @param {string} resultStr - The raw result string from token burn + * @param {string} expectedIdentityId - Expected identity ID + * @param {string} expectedAmount - Expected burn amount + */ +function validateTokenBurnResult(resultStr, expectedIdentityId, expectedAmount) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const burnResponse = JSON.parse(resultStr); + expect(burnResponse).toBeDefined(); + expect(burnResponse).toBeInstanceOf(Object); + + // Token burn returns an empty object {} on success + // This indicates the transaction was submitted successfully + console.log(`✅ Token burn transaction submitted successfully: ${expectedAmount} tokens burned from ${expectedIdentityId}`); + + return burnResponse; +} + +/** + * Helper function to validate token freeze result + * @param {string} resultStr - The raw result string from token freeze + * @param {string} expectedIdentityId - Expected identity ID to freeze + */ +function validateTokenFreezeResult(resultStr, expectedIdentityId) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const freezeResponse = JSON.parse(resultStr); + expect(freezeResponse).toBeDefined(); + expect(freezeResponse).toBeInstanceOf(Object); + + // Token freeze returns an empty object {} on success + console.log(`✅ Token freeze transaction submitted successfully for identity: ${expectedIdentityId}`); + + return freezeResponse; +} + +/** + * Helper function to validate token destroy frozen result + * @param {string} resultStr - The raw result string from token destroy frozen + * @param {string} expectedIdentityId - Expected identity ID with frozen tokens + */ +function validateTokenDestroyFrozenResult(resultStr, expectedIdentityId) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const destroyResponse = JSON.parse(resultStr); + expect(destroyResponse).toBeDefined(); + expect(destroyResponse).toBeInstanceOf(Object); + + // Token destroy frozen returns an empty object {} on success + console.log(`✅ Token destroy frozen transaction submitted successfully: destroyed all frozen tokens from ${expectedIdentityId}`); + + return destroyResponse; +} + +/** + * Helper function to validate token unfreeze result + * @param {string} resultStr - The raw result string from token unfreeze + * @param {string} expectedIdentityId - Expected identity ID to unfreeze + */ +function validateTokenUnfreezeResult(resultStr, expectedIdentityId) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const unfreezeResponse = JSON.parse(resultStr); + expect(unfreezeResponse).toBeDefined(); + expect(unfreezeResponse).toBeInstanceOf(Object); + + // Token unfreeze returns an empty object {} on success + console.log(`✅ Token unfreeze transaction submitted successfully for identity: ${expectedIdentityId}`); + + return unfreezeResponse; +} + +function validateTokenClaimResult(resultStr, expectedDistributionType) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const claimResponse = JSON.parse(resultStr); + expect(claimResponse).toBeDefined(); + expect(claimResponse).toBeInstanceOf(Object); + + // Token claim returns an empty object {} on success + console.log(`✅ Token claim transaction submitted successfully for distribution type: ${expectedDistributionType}`); + + return claimResponse; +} + +function validateTokenSetPriceResult(resultStr, expectedPriceType, expectedPriceData) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const setPriceResponse = JSON.parse(resultStr); + expect(setPriceResponse).toBeDefined(); + expect(setPriceResponse).toBeInstanceOf(Object); + + // Token set price returns an empty object {} on success + console.log(`✅ Token set price transaction submitted successfully - Type: ${expectedPriceType}, Price: ${expectedPriceData}`); + + return setPriceResponse; +} + +function validateTokenDirectPurchaseResult(resultStr, expectedAmount, expectedTotalPrice) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const purchaseResponse = JSON.parse(resultStr); + expect(purchaseResponse).toBeDefined(); + expect(purchaseResponse).toBeInstanceOf(Object); + + // Token direct purchase returns an empty object {} on success + console.log(`✅ Token direct purchase transaction submitted successfully - Amount: ${expectedAmount} tokens, Total price: ${expectedTotalPrice} credits`); + + return purchaseResponse; +} + +function validateTokenConfigUpdateResult(resultStr, expectedConfigType, expectedConfigValue) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const configUpdateResponse = JSON.parse(resultStr); + expect(configUpdateResponse).toBeDefined(); + expect(configUpdateResponse).toBeInstanceOf(Object); + + // Token config update returns an empty object {} on success + console.log(`✅ Token config update transaction submitted successfully - Type: ${expectedConfigType}, Value: ${expectedConfigValue}`); + + return configUpdateResponse; +} + +/** + * Helper function to validate document transfer result + * @param {string} resultStr - The raw result string from document transfer + * @param {string} expectedDocumentId - Expected document ID to validate against + * @param {string} expectedRecipientId - Expected recipient identity ID + */ +function validateDocumentTransferResult(resultStr, expectedDocumentId, expectedRecipientId) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const transferResponse = JSON.parse(resultStr); + expect(transferResponse).toBeDefined(); + expect(transferResponse).toBeInstanceOf(Object); + + // Validate the response structure for document transfer + expect(transferResponse.type).toBe('DocumentTransferred'); + expect(transferResponse.documentId).toBe(expectedDocumentId); + expect(transferResponse.newOwnerId).toBe(expectedRecipientId); + expect(transferResponse.transferred).toBe(true); + + console.log(`✅ Confirmed transfer of document: ${expectedDocumentId} to ${expectedRecipientId}`); + + return transferResponse; +} + +/** + * Helper function to validate document set price result + * @param {string} resultStr - The raw result string from document set price + * @param {string} expectedDocumentId - Expected document ID to validate against + * @param {number} expectedPrice - Expected price that was set + */ +function validateDocumentSetPriceResult(resultStr, expectedDocumentId, expectedPrice) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const setPriceResponse = JSON.parse(resultStr); + expect(setPriceResponse).toBeDefined(); + expect(setPriceResponse).toBeInstanceOf(Object); + + // Validate the response structure for document set price + expect(setPriceResponse.type).toBe('DocumentPriceSet'); + expect(setPriceResponse.documentId).toBe(expectedDocumentId); + expect(setPriceResponse.price).toBe(expectedPrice); + expect(setPriceResponse.priceSet).toBe(true); + + console.log(`✅ Confirmed price set for document: ${expectedDocumentId} at ${expectedPrice} credits`); + + return setPriceResponse; +} + +/** + * Helper function to validate document purchase result + * @param {string} resultStr - The raw result string from document purchase + * @param {string} expectedDocumentId - Expected document ID to validate against + * @param {string} expectedBuyerId - Expected buyer identity ID + * @param {number} expectedPrice - Expected purchase price + */ +function validateDocumentPurchaseResult(resultStr, expectedDocumentId, expectedBuyerId, expectedPrice) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const purchaseResponse = JSON.parse(resultStr); + expect(purchaseResponse).toBeDefined(); + expect(purchaseResponse).toBeInstanceOf(Object); + + // Validate the response structure for document purchase + expect(purchaseResponse.type).toBe('DocumentPurchased'); + expect(purchaseResponse.documentId).toBe(expectedDocumentId); + expect(purchaseResponse.status).toBe('success'); + expect(purchaseResponse.newOwnerId).toBe(expectedBuyerId); + expect(purchaseResponse.pricePaid).toBe(expectedPrice); + expect(purchaseResponse.message).toBe('Document purchased successfully'); + expect(purchaseResponse.documentUpdated).toBe(true); + expect(purchaseResponse.revision).toBeDefined(); + expect(typeof purchaseResponse.revision).toBe('number'); + + console.log(`✅ Confirmed purchase of document: ${expectedDocumentId} by ${expectedBuyerId} for ${expectedPrice} credits`); + + return purchaseResponse; +} + +/** + * Execute a state transition with custom parameters + * @param {WasmSdkPage} wasmSdkPage - The page object instance + * @param {ParameterInjector} parameterInjector - The parameter injector instance + * @param {string} category - State transition category + * @param {string} transitionType - Transition type + * @param {string} network - Network to use + * @param {Object} customParams - Custom parameters to override test data + * @returns {Promise} - The transition result object + */ +async function executeStateTransitionWithCustomParams(wasmSdkPage, parameterInjector, category, transitionType, network = 'testnet', customParams = {}) { + await wasmSdkPage.setupStateTransition(category, transitionType); + + const success = await parameterInjector.injectStateTransitionParameters(category, transitionType, network, customParams); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + return result; +} + +test.describe('WASM SDK State Transition Tests', () => { + let wasmSdkPage; + let parameterInjector; + + test.beforeEach(async ({ page }) => { + wasmSdkPage = new WasmSdkPage(page); + parameterInjector = new ParameterInjector(wasmSdkPage); + await wasmSdkPage.initialize('testnet'); + }); + + test.describe('Data Contract State Transitions', () => { + test.skip('should execute data contract create transition', async () => { + // Execute the data contract create transition + const result = await executeStateTransition( + wasmSdkPage, + parameterInjector, + 'dataContract', + 'dataContractCreate', + 'testnet' + ); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Validate data contract creation specific result + validateDataContractResult(result.result, false); + + console.log('✅ Data contract create state transition completed successfully'); + }); + + test.skip('should execute data contract update transition', async () => { + // Execute the data contract update transition + const result = await executeStateTransition( + wasmSdkPage, + parameterInjector, + 'dataContract', + 'dataContractUpdate', + 'testnet' + ); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Validate data contract update specific result + validateDataContractResult(result.result, true); + + console.log('✅ Data contract update state transition completed successfully'); + }); + + test('should create data contract and then update it with author field', async () => { + // Set extended timeout for combined create+update operation + test.setTimeout(180000); + + let contractId; + + // Step 1: Create contract (reported separately) + await test.step('Create data contract', async () => { + console.log('Creating new data contract...'); + const createResult = await executeStateTransition( + wasmSdkPage, + parameterInjector, + 'dataContract', + 'dataContractCreate', + 'testnet' + ); + + // Validate create result + validateBasicStateTransitionResult(createResult); + validateDataContractResult(createResult.result, false); + + // Get the contract ID from create result + contractId = JSON.parse(createResult.result).contractId; + console.log('✅ Data contract created with ID:', contractId); + }); + + // Step 2: Update contract (reported separately) + // This test is now flaky for some reason and frequently fails + await test.step('Update data contract with author field', async () => { + console.log('🔄 Updating data contract to add author field...'); + const updateResult = await executeStateTransitionWithCustomParams( + wasmSdkPage, + parameterInjector, + 'dataContract', + 'dataContractUpdate', + 'testnet', + { dataContractId: contractId } // Override with dynamic contract ID + ); + + // Validate update result + validateBasicStateTransitionResult(updateResult); + validateDataContractResult(updateResult.result, true); + + console.log('✅ Data contract updated successfully with author field'); + }); + }); + + test('should show authentication inputs for data contract transitions', async () => { + await wasmSdkPage.setupStateTransition('dataContract', 'dataContractCreate'); + + // Check that authentication inputs are visible + const hasAuthInputs = await wasmSdkPage.hasAuthenticationInputs(); + expect(hasAuthInputs).toBe(true); + + console.log('✅ Data contract state transition authentication inputs are visible'); + }); + }); + + test.describe('Document State Transitions', () => { + test('should execute document create transition', async () => { + // Set up the document create transition manually due to special schema handling + await wasmSdkPage.setupStateTransition('document', 'documentCreate'); + + // Inject basic parameters (contractId, documentType, identityId, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('document', 'documentCreate', 'testnet'); + expect(success).toBe(true); + + // Step 1: Fetch document schema to generate dynamic fields + await test.step('Fetch document schema', async () => { + await wasmSdkPage.fetchDocumentSchema(); + console.log('✅ Document schema fetched and fields generated'); + }); + + // Step 2: Fill document fields + await test.step('Fill document fields', async () => { + // Get document fields from test data + const testParams = parameterInjector.testData.stateTransitionParameters.document.documentCreate.testnet[0]; + await wasmSdkPage.fillDocumentFields(testParams.documentFields); + console.log('✅ Document fields filled'); + }); + + // Step 3: Execute the transition + await test.step('Execute document create', async () => { + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Validate document creation specific result + validateDocumentCreateResult(result.result); + + console.log('✅ Document create state transition completed successfully'); + }); + }); + + test('should execute document replace transition', async () => { + // Set up the document replace transition + await wasmSdkPage.setupStateTransition('document', 'documentReplace'); + + // Inject basic parameters (contractId, documentType, documentId, identityId, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('document', 'documentReplace', 'testnet'); + expect(success).toBe(true); + + // Load the existing document to get revision and populate fields + await wasmSdkPage.loadExistingDocument(); + + // Create updated message with timestamp + const testParams = parameterInjector.testData.stateTransitionParameters.document.documentReplace.testnet[0]; + const baseMessage = testParams.documentFields.message; + const timestamp = new Date().toISOString(); + const updatedFields = { + message: `${baseMessage} - Updated at ${timestamp}` + }; + + // Fill updated document fields + await wasmSdkPage.fillDocumentFields(updatedFields); + + // Execute the replace transition + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Validate document replace specific result with expected document ID + const expectedDocumentId = testParams.documentId; + validateDocumentReplaceResult(result.result, expectedDocumentId); + + console.log('✅ Document replace state transition completed successfully'); + }); + + test('should set price, purchase, and transfer a trading card document', async () => { + // Set extended timeout for complete marketplace workflow + test.setTimeout(275000); + + let documentId; + // Step 1: Set price on the card (by owner - primary identity) + await test.step('Set price on trading card', async () => { + console.log('Setting price on trading card...'); + + // Get the configured price from test data + const setPriceParams = parameterInjector.testData.stateTransitionParameters.document.documentSetPrice.testnet[0]; + const configuredPrice = setPriceParams.price; + + // Execute the set price transition + const setPriceResult = await executeStateTransitionWithCustomParams( + wasmSdkPage, + parameterInjector, + 'document', + 'documentSetPrice', + 'testnet', + {} + ); + + // Validate basic result structure + validateBasicStateTransitionResult(setPriceResult); + + // Get document ID from test data for validation + documentId = setPriceParams.documentId; + + // Validate document set price specific result + validateDocumentSetPriceResult( + setPriceResult.result, + documentId, + configuredPrice + ); + + console.log('✅ Card price set successfully'); + }); + + // Step 2: Purchase the card with secondary identity (tests purchase flow) + await test.step('Purchase trading card with secondary identity', async () => { + console.log('Purchasing trading card with secondary identity...'); + + // Get the configured price from test data + const purchaseParams = parameterInjector.testData.stateTransitionParameters.document.documentPurchase.testnet[0]; + const purchaseConfiguredPrice = purchaseParams.price; + + // Log if the purchase price differs from what was set + const setPriceParams = parameterInjector.testData.stateTransitionParameters.document.documentSetPrice.testnet[0]; + if (purchaseConfiguredPrice !== setPriceParams.price) { + console.log(`⚠️ Note: documentPurchase uses price ${purchaseConfiguredPrice}, but documentSetPrice set it to ${setPriceParams.price}`); + } + + // Execute the purchase transition + const purchaseResult = await executeStateTransitionWithCustomParams( + wasmSdkPage, + parameterInjector, + 'document', + 'documentPurchase', + 'testnet', + {} + ); + + // Validate basic result structure + validateBasicStateTransitionResult(purchaseResult); + + // Get test parameters for validation (secondary identity is the buyer) + const testParams = parameterInjector.testData.stateTransitionParameters.document.documentPurchase.testnet[0]; + + // Validate document purchase specific result + validateDocumentPurchaseResult( + purchaseResult.result, + documentId, + testParams.identityId, // Secondary identity as buyer + purchaseConfiguredPrice // Use the actual price from test-data.js + ); + + console.log('✅ Card purchased by secondary identity successfully'); + }); + + // Step 3: Transfer the card back to primary identity (tests transfer flow) + await test.step('Transfer card back to primary identity', async () => { + console.log('Transferring card back to primary identity...'); + + // Get primary identity ID from test data + const primaryIdentityId = parameterInjector.testData.stateTransitionParameters.dataContract.dataContractCreate.testnet[0].identityId; + + // Execute the transfer transition + const transferResult = await executeStateTransitionWithCustomParams( + wasmSdkPage, + parameterInjector, + 'document', + 'documentTransfer', + 'testnet', + { + recipientId: primaryIdentityId // Transfer back to primary identity + } + ); + + // Validate basic result structure + validateBasicStateTransitionResult(transferResult); + + // Validate document transfer specific result + validateDocumentTransferResult( + transferResult.result, + documentId, + primaryIdentityId // Primary identity as recipient + ); + + console.log('✅ Complete marketplace workflow completed: Create → Set Price → Purchase → Transfer'); + }); + }); + + test('should create, replace, and delete a document', async () => { + // Set extended timeout for combined create+replace+delete operation + test.setTimeout(260000); + + let documentId; + + // Step 1: Create document (reported separately) + await test.step('Create document', async () => { + console.log('Creating new document...'); + + // Set up the document create transition + await wasmSdkPage.setupStateTransition('document', 'documentCreate'); + + // Inject basic parameters (contractId, documentType, identityId, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('document', 'documentCreate', 'testnet'); + expect(success).toBe(true); + + // Fetch document schema to generate dynamic fields + await wasmSdkPage.fetchDocumentSchema(); + + // Fill document fields + const testParams = parameterInjector.testData.stateTransitionParameters.document.documentCreate.testnet[0]; + await wasmSdkPage.fillDocumentFields(testParams.documentFields); + + // Execute the transition + const createResult = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate create result + validateBasicStateTransitionResult(createResult); + const documentResponse = validateDocumentCreateResult(createResult.result); + + // Get the document ID from create result + documentId = documentResponse.documentId; + console.log('✅ Document created with ID:', documentId); + }); + + // Step 2: Replace the document (reported separately) + await test.step('Replace document', async () => { + console.log('Replacing the created document...'); + + // Set up document replace transition + await wasmSdkPage.setupStateTransition('document', 'documentReplace'); + + // Inject parameters with the created document ID + const success = await parameterInjector.injectStateTransitionParameters( + 'document', + 'documentReplace', + 'testnet', + { documentId } // Override with the created document ID + ); + expect(success).toBe(true); + + // Load the existing document to get revision + await wasmSdkPage.loadExistingDocument(); + + // Create updated message with timestamp + const originalTestParams = parameterInjector.testData.stateTransitionParameters.document.documentCreate.testnet[0]; + const originalMessage = originalTestParams.documentFields.message; + const timestamp = new Date().toISOString(); + const updatedFields = { + message: `${originalMessage} - Updated at ${timestamp}` + }; + + // Fill updated document fields + await wasmSdkPage.fillDocumentFields(updatedFields); + + // Execute the replace transition + const replaceResult = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate replace result + validateBasicStateTransitionResult(replaceResult); + validateDocumentReplaceResult(replaceResult.result, documentId); + + console.log('✅ Document replaced successfully'); + }); + + // Step 3: Delete the document (reported separately) + await test.step('Delete document', async () => { + console.log('Deleting the created document...'); + + // Set up document delete transition with the created document ID + await wasmSdkPage.setupStateTransition('document', 'documentDelete'); + + // Inject parameters with the dynamic document ID + const success = await parameterInjector.injectStateTransitionParameters( + 'document', + 'documentDelete', + 'testnet', + { documentId } // Override with the created document ID + ); + expect(success).toBe(true); + + // Execute the delete transition + const deleteResult = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate delete result with expected document ID + validateBasicStateTransitionResult(deleteResult); + validateDocumentDeleteResult(deleteResult.result, documentId); + + console.log('✅ Document deleted successfully'); + }); + }); + + test('should show authentication inputs for document transitions', async () => { + await wasmSdkPage.setupStateTransition('document', 'documentCreate'); + + // Check that authentication inputs are visible + const hasAuthInputs = await wasmSdkPage.hasAuthenticationInputs(); + expect(hasAuthInputs).toBe(true); + + console.log('✅ Document state transition authentication inputs are visible'); + }); + }); + + test.describe('Identity State Transitions', () => { + test('should execute identity credit transfer transition', async () => { + // Set up the identity credit transfer transition + await wasmSdkPage.setupStateTransition('identity', 'identityCreditTransfer'); + + // Inject parameters (senderId, recipientId, amount, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('identity', 'identityCreditTransfer', 'testnet'); + expect(success).toBe(true); + + // Execute the transfer + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.identity.identityCreditTransfer.testnet[0]; + + // Validate identity credit transfer specific result + validateIdentityCreditTransferResult( + result.result, + testParams.identityId, // Sender is the identityId field + testParams.recipientId, + testParams.amount + ); + + console.log('✅ Identity credit transfer state transition completed successfully'); + }); + + test('should execute identity credit withdrawal transition', async () => { + // Get test parameters to check withdrawal amount upfront + const testParams = parameterInjector.testData.stateTransitionParameters.identity.identityCreditWithdrawal.testnet[0]; + + // Skip test if withdrawal amount is below minimum threshold + if (testParams.amount < 190000) { + test.skip(true, `Withdrawal amount ${testParams.amount} credits is below minimum threshold (~190,000 credits)`); + } + + // Set up the identity credit withdrawal transition + await wasmSdkPage.setupStateTransition('identity', 'identityCreditWithdrawal'); + + // Inject parameters (identityId, withdrawalAddress, amount, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('identity', 'identityCreditWithdrawal', 'testnet'); + expect(success).toBe(true); + + // Execute the withdrawal + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Validate identity credit withdrawal specific result + validateIdentityCreditWithdrawalResult( + result.result, + testParams.identityId, + testParams.toAddress, + testParams.amount + ); + + console.log('✅ Identity credit withdrawal state transition completed successfully'); + }); + + test('should show authentication inputs for identity transitions', async () => { + await wasmSdkPage.setupStateTransition('identity', 'identityCreditTransfer'); + + // Check that authentication inputs are visible + const hasAuthInputs = await wasmSdkPage.hasAuthenticationInputs(); + expect(hasAuthInputs).toBe(true); + + console.log('✅ Identity state transition authentication inputs are visible'); + }); + }); + + test.describe('Token State Transitions', () => { + test('should execute token mint transition', async () => { + // Set up the token mint transition + await wasmSdkPage.setupStateTransition('token', 'tokenMint'); + + // Inject parameters (contractId, tokenId, tokenPosition, amount, issuedToIdentityId, identityId, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenMint', 'testnet'); + expect(success).toBe(true); + + // Execute the mint + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenMint.testnet[0]; + + // Validate token mint specific result + validateTokenMintResult( + result.result, + testParams.identityId, + testParams.amount + ); + + console.log('✅ Token mint state transition completed successfully'); + }); + + test('should execute token transfer transition', async () => { + // Set up the token transfer transition + await wasmSdkPage.setupStateTransition('token', 'tokenTransfer'); + + // Inject parameters (contractId, tokenId, tokenPosition, amount, recipientId, identityId, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenTransfer', 'testnet'); + expect(success).toBe(true); + + // Execute the transfer + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenTransfer.testnet[0]; + + // Validate token transfer specific result + validateTokenTransferResult( + result.result, + testParams.identityId, + testParams.recipientId, + testParams.amount + ); + + console.log('✅ Token transfer state transition completed successfully'); + }); + + test('should execute token burn transition', async () => { + // Set up the token burn transition + await wasmSdkPage.setupStateTransition('token', 'tokenBurn'); + + // Inject parameters (contractId, tokenId, tokenPosition, amount, identityId, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenBurn', 'testnet'); + expect(success).toBe(true); + + // Execute the burn + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenBurn.testnet[0]; + + // Validate token burn specific result + validateTokenBurnResult( + result.result, + testParams.identityId, + testParams.amount + ); + + console.log('✅ Token burn state transition completed successfully'); + }); + + test('should execute token freeze transition', async () => { + // Set up the token freeze transition + await wasmSdkPage.setupStateTransition('token', 'tokenFreeze'); + + // Inject parameters (contractId, tokenPosition, identityId, identityToFreeze, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenFreeze', 'testnet'); + expect(success).toBe(true); + + // Execute the freeze + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenFreeze.testnet[0]; + + // Validate token freeze specific result + validateTokenFreezeResult(result.result, testParams.identityToFreeze); + + console.log('✅ Token freeze state transition completed successfully'); + }); + + test('should execute token destroy frozen transition', async () => { + // Set up the token destroy frozen transition + await wasmSdkPage.setupStateTransition('token', 'tokenDestroyFrozen'); + + // Inject parameters (contractId, tokenPosition, identityId, destroyFromIdentityId, amount, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenDestroyFrozen', 'testnet'); + expect(success).toBe(true); + + // Execute the destroy frozen + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenDestroyFrozen.testnet[0]; + + // Validate token destroy frozen specific result + validateTokenDestroyFrozenResult(result.result, testParams.frozenIdentityId); + + console.log('✅ Token destroy frozen state transition completed successfully'); + }); + + test('should execute token unfreeze transition', async () => { + // Set up the token unfreeze transition + await wasmSdkPage.setupStateTransition('token', 'tokenUnfreeze'); + + // Inject parameters (contractId, tokenPosition, identityId, identityToUnfreeze, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenUnfreeze', 'testnet'); + expect(success).toBe(true); + + // Execute the unfreeze + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenUnfreeze.testnet[0]; + + // Validate token unfreeze specific result + validateTokenUnfreezeResult(result.result, testParams.identityToUnfreeze); + + console.log('✅ Token unfreeze state transition completed successfully'); + }); + + test('should execute token claim transition', async () => { + // Set up the token claim transition + await wasmSdkPage.setupStateTransition('token', 'tokenClaim'); + + // Inject parameters (contractId, tokenPosition, distributionType, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenClaim', 'testnet'); + expect(success).toBe(true); + + // Execute the claim + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Check for expected platform responses indicating no tokens available + if (!result.success && result.result && result.result.includes('Missing response message')) { + // Skip the test with a descriptive reason + test.skip(true, 'Platform returned "Missing response message". Probably no tokens available to claim.'); + } + + // Validate normal success case + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenClaim.testnet[0]; + + // Validate token claim specific result + validateTokenClaimResult(result.result, testParams.distributionType); + + console.log('✅ Token claim state transition completed successfully'); + }); + + test('should execute token set price transition', async () => { + // Set up the token set price transition + await wasmSdkPage.setupStateTransition('token', 'tokenSetPriceForDirectPurchase'); + + // Inject parameters (contractId, tokenPosition, priceType, priceData, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenSetPriceForDirectPurchase', 'testnet'); + expect(success).toBe(true); + + // Execute the set price + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenSetPriceForDirectPurchase.testnet[0]; + + // Validate token set price specific result + validateTokenSetPriceResult(result.result, testParams.priceType, testParams.priceData); + + console.log('✅ Token set price state transition completed successfully'); + }); + + test('should execute token direct purchase transition', async () => { + // Set up the token direct purchase transition + await wasmSdkPage.setupStateTransition('token', 'tokenDirectPurchase'); + + // Inject parameters (contractId, tokenPosition, amount, totalAgreedPrice, keyId, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenDirectPurchase', 'testnet'); + expect(success).toBe(true); + + // Execute the purchase + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Check for expected platform responses indicating issues + if (!result.success && result.result && result.result.includes('Missing response message')) { + // Skip the test with a descriptive reason + test.skip(true, 'Platform returned "Missing response message". Possibly insufficient credits or tokens not available for purchase.'); + } + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenDirectPurchase.testnet[0]; + + // Validate token direct purchase specific result + validateTokenDirectPurchaseResult(result.result, testParams.amount, testParams.totalAgreedPrice); + + console.log('✅ Token direct purchase state transition completed successfully'); + }); + + test('should execute token config update transition', async () => { + // Set up the token config update transition + await wasmSdkPage.setupStateTransition('token', 'tokenConfigUpdate'); + + // Inject parameters (contractId, tokenPosition, configItemType, configValue, privateKey) + const success = await parameterInjector.injectStateTransitionParameters('token', 'tokenConfigUpdate', 'testnet'); + expect(success).toBe(true); + + // Execute the config update + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Validate basic result structure + validateBasicStateTransitionResult(result); + + // Get test parameters for validation + const testParams = parameterInjector.testData.stateTransitionParameters.token.tokenConfigUpdate.testnet[0]; + + // Validate token config update specific result + validateTokenConfigUpdateResult(result.result, testParams.configItemType, testParams.configValue); + + console.log('✅ Token config update state transition completed successfully'); + }); + + test('should show authentication inputs for token transitions', async () => { + await wasmSdkPage.setupStateTransition('token', 'tokenTransfer'); + + // Check that authentication inputs are visible + const hasAuthInputs = await wasmSdkPage.hasAuthenticationInputs(); + expect(hasAuthInputs).toBe(true); + + console.log('✅ Token state transition authentication inputs are visible'); + }); + }); + + test.describe('Error Handling for State Transitions', () => { + test('should handle invalid JSON schema gracefully', async () => { + await wasmSdkPage.setupStateTransition('dataContract', 'dataContractCreate'); + + // Fill with invalid JSON schema + const invalidParams = { + canBeDeleted: false, + readonly: false, + keepsHistory: false, + documentSchemas: 'invalid_json_here', + identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + privateKey: "11111111111111111111111111111111111111111111111111" + }; + + await wasmSdkPage.fillStateTransitionParameters(invalidParams); + + // Execute the transition + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Should show error + expect(result.hasError).toBe(true); + expect(result.statusText.toLowerCase()).toMatch(/error|invalid|failed/); + + console.log('✅ Invalid JSON schema error handling works correctly'); + }); + + test('should handle missing required fields', async () => { + await wasmSdkPage.setupStateTransition('dataContract', 'dataContractCreate'); + + // Don't fill any parameters, try to execute + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Should show error or validation message + expect(result.hasError).toBe(true); + expect(result.statusText.toLowerCase()).toMatch(/error|required|missing/); + + console.log('✅ Missing required fields error handling works correctly'); + }); + + test('should handle invalid private key gracefully', async () => { + await wasmSdkPage.setupStateTransition('dataContract', 'dataContractCreate'); + + // Fill with invalid private key + const invalidParams = { + canBeDeleted: false, + readonly: false, + keepsHistory: false, + documentSchemas: '{"note": {"type": "object", "properties": {"message": {"type": "string", "position": 0}}, "additionalProperties": false}}', + identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + privateKey: "invalid_private_key_here" + }; + + await wasmSdkPage.fillStateTransitionParameters(invalidParams); + + // Execute the transition + const result = await wasmSdkPage.executeStateTransitionAndGetResult(); + + // Should show error + expect(result.hasError).toBe(true); + expect(result.statusText.toLowerCase()).toMatch(/error|invalid|failed/); + + console.log('✅ Invalid private key error handling works correctly'); + }); + }); + + test.describe('UI State and Navigation', () => { + test('should switch to state transitions operation type correctly', async () => { + // Start with queries, then switch to transitions + await wasmSdkPage.setOperationType('queries'); + await wasmSdkPage.page.waitForTimeout(500); + + await wasmSdkPage.setOperationType('transitions'); + + // Verify the operation type is set correctly + const operationType = await wasmSdkPage.page.locator('#operationType').inputValue(); + expect(operationType).toBe('transitions'); + + console.log('✅ Successfully switched to state transitions operation type'); + }); + + test('should populate transition categories correctly', async () => { + await wasmSdkPage.setOperationType('transitions'); + + // Get available categories and filter out placeholders + const allCategories = await wasmSdkPage.getAvailableQueryCategories(); + const categories = filterPlaceholderOptions(allCategories); + + // Define expected state transition categories + const expectedCategories = [ + 'Identity Transitions', + 'Data Contract Transitions', + 'Document Transitions', + 'Token Transitions', + 'Voting Transitions' + ]; + + // Verify exact match - contains all expected and no unexpected ones + expect(categories).toHaveLength(expectedCategories.length); + expectedCategories.forEach(expectedCategory => { + expect(categories).toContain(expectedCategory); + }); + + console.log('✅ State transition categories populated correctly:', categories); + }); + + test('should populate identity transition types correctly', async () => { + await wasmSdkPage.setOperationType('transitions'); + await wasmSdkPage.setQueryCategory('identity'); + + // Get available transition types and filter out placeholders + const allTransitionTypes = await wasmSdkPage.getAvailableQueryTypes(); + const transitionTypes = filterPlaceholderOptions(allTransitionTypes); + + // Define expected identity transition types + const expectedTransitionTypes = [ + 'Identity Create', + 'Identity Top Up', + 'Identity Update', + 'Identity Credit Transfer', + 'Identity Credit Withdrawal' + ]; + + // Verify exact match - contains all expected and no unexpected ones + expect(transitionTypes).toHaveLength(expectedTransitionTypes.length); + expectedTransitionTypes.forEach(expectedType => { + expect(transitionTypes).toContain(expectedType); + }); + + console.log('✅ Identity transition types populated correctly:', transitionTypes); + }); + + test('should populate data contract transition types correctly', async () => { + await wasmSdkPage.setOperationType('transitions'); + await wasmSdkPage.setQueryCategory('dataContract'); + + // Get available transition types and filter out placeholders + const allTransitionTypes = await wasmSdkPage.getAvailableQueryTypes(); + const transitionTypes = filterPlaceholderOptions(allTransitionTypes); + + // Define expected data contract transition types + const expectedTransitionTypes = [ + 'Data Contract Create', + 'Data Contract Update' + ]; + + // Verify exact match - contains all expected and no unexpected ones + expect(transitionTypes).toHaveLength(expectedTransitionTypes.length); + expectedTransitionTypes.forEach(expectedType => { + expect(transitionTypes).toContain(expectedType); + }); + + console.log('✅ Data contract transition types populated correctly:', transitionTypes); + }); + + test('should populate document transition types correctly', async () => { + await wasmSdkPage.setOperationType('transitions'); + await wasmSdkPage.setQueryCategory('document'); + + // Get available transition types and filter out placeholders + const allTransitionTypes = await wasmSdkPage.getAvailableQueryTypes(); + const transitionTypes = filterPlaceholderOptions(allTransitionTypes); + + // Define expected document transition types + const expectedTransitionTypes = [ + 'Document Create', + 'Document Replace', + 'Document Delete', + 'Document Transfer', + 'Document Purchase', + 'Document Set Price', + 'DPNS Register Name' + ]; + + // Verify exact match - contains all expected and no unexpected ones + expect(transitionTypes).toHaveLength(expectedTransitionTypes.length); + expectedTransitionTypes.forEach(expectedType => { + expect(transitionTypes).toContain(expectedType); + }); + + console.log('✅ Document transition types populated correctly:', transitionTypes); + }); + + test('should populate token transition types correctly', async () => { + await wasmSdkPage.setOperationType('transitions'); + await wasmSdkPage.setQueryCategory('token'); + + // Get available transition types and filter out placeholders + const allTransitionTypes = await wasmSdkPage.getAvailableQueryTypes(); + const transitionTypes = filterPlaceholderOptions(allTransitionTypes); + + // Define expected token transition types (based on docs.html) + const expectedTransitionTypes = [ + 'Token Burn', + 'Token Mint', + 'Token Claim', + 'Token Set Price', + 'Token Direct Purchase', + 'Token Config Update', + 'Token Transfer', + 'Token Freeze', + 'Token Unfreeze', + 'Token Destroy Frozen' + ]; + + // Verify exact match - contains all expected and no unexpected ones + expect(transitionTypes).toHaveLength(expectedTransitionTypes.length); + expectedTransitionTypes.forEach(expectedType => { + expect(transitionTypes).toContain(expectedType); + }); + + console.log('✅ Token transition types populated correctly:', transitionTypes); + }); + + test('should populate voting transition types correctly', async () => { + await wasmSdkPage.setOperationType('transitions'); + await wasmSdkPage.setQueryCategory('voting'); + + // Get available transition types and filter out placeholders + const allTransitionTypes = await wasmSdkPage.getAvailableQueryTypes(); + const transitionTypes = filterPlaceholderOptions(allTransitionTypes); + + // Define expected voting transition types + const expectedTransitionTypes = [ + 'DPNS Username', + 'Contested Resource' + ]; + + // Verify exact match - contains all expected and no unexpected ones + expect(transitionTypes).toHaveLength(expectedTransitionTypes.length); + expectedTransitionTypes.forEach(expectedType => { + expect(transitionTypes).toContain(expectedType); + }); + + console.log('✅ Voting transition types populated correctly:', transitionTypes); + }); + }); + +}); diff --git a/packages/wasm-sdk/test/ui-automation/utils/base-test.js b/packages/wasm-sdk/test/ui-automation/utils/base-test.js index 23034fb2694..1f98c954ecb 100644 --- a/packages/wasm-sdk/test/ui-automation/utils/base-test.js +++ b/packages/wasm-sdk/test/ui-automation/utils/base-test.js @@ -222,7 +222,8 @@ class BaseTest { await this.page.locator('#statusBanner.loading').waitFor({ state: 'visible', timeout: 5000 }); // Wait for loading to complete (either success or error) - await this.page.locator('#statusBanner.loading').waitFor({ state: 'hidden', timeout: 30000 }); + // State transitions can take longer than queries, so use longer timeout + await this.page.locator('#statusBanner.loading').waitFor({ state: 'hidden', timeout: 85000 }); } catch (error) { // Some queries execute so quickly they never show loading state // Check if the query already completed successfully or with an error diff --git a/packages/wasm-sdk/test/ui-automation/utils/parameter-injector.js b/packages/wasm-sdk/test/ui-automation/utils/parameter-injector.js index 4b97e6fafea..35c5a8a67f6 100644 --- a/packages/wasm-sdk/test/ui-automation/utils/parameter-injector.js +++ b/packages/wasm-sdk/test/ui-automation/utils/parameter-injector.js @@ -1,4 +1,4 @@ -const { getTestParameters, getAllTestParameters } = require('../fixtures/test-data'); +const { testData, getTestParameters, getAllTestParameters, getStateTransitionParameters, getAllStateTransitionParameters } = require('../fixtures/test-data'); /** * Parameter injection system for WASM SDK UI tests @@ -7,6 +7,7 @@ const { getTestParameters, getAllTestParameters } = require('../fixtures/test-da class ParameterInjector { constructor(wasmSdkPage) { this.page = wasmSdkPage; + this.testData = testData; } /** @@ -22,7 +23,7 @@ class ParameterInjector { } const parameters = allParameters[parameterSetIndex] || allParameters[0]; - console.log(`📝 Injecting parameters for ${category}.${queryType}:`, parameters); + console.log(`📝 Injecting parameters for ${category}.${queryType}`); await this.page.fillQueryParameters(parameters); return true; @@ -32,6 +33,34 @@ class ParameterInjector { } } + /** + * Inject parameters for a specific state transition based on test data + */ + async injectStateTransitionParameters(category, transitionType, network = 'testnet', customParams = {}) { + try { + // Get base parameters from test data + const allParameters = getAllStateTransitionParameters(category, transitionType, network); + + if (allParameters.length === 0) { + console.warn(`⚠️ No state transition test parameters found for ${category}.${transitionType} on ${network}`); + return false; + } + + const baseParameters = allParameters[0]; + + // Merge base parameters with custom overrides + const parameters = { ...baseParameters, ...customParams }; + + console.log(`📝 Injecting state transition parameters for ${category}.${transitionType}`); + + await this.page.fillStateTransitionParameters(parameters); + return true; + } catch (error) { + console.error(`❌ Failed to inject state transition parameters for ${category}.${transitionType}:`, error.message); + return false; + } + } + /** * Get parameter mapping for manual field filling * Maps parameter names to likely field selectors diff --git a/packages/wasm-sdk/test/ui-automation/utils/wasm-sdk-page.js b/packages/wasm-sdk/test/ui-automation/utils/wasm-sdk-page.js index 0bddb309823..4f876793368 100644 --- a/packages/wasm-sdk/test/ui-automation/utils/wasm-sdk-page.js +++ b/packages/wasm-sdk/test/ui-automation/utils/wasm-sdk-page.js @@ -120,6 +120,18 @@ class WasmSdkPage extends BaseTest { * Fill a specific parameter by name */ async fillParameterByName(paramName, value) { + // Special handling for multiselect checkboxes (like purposes) + if (paramName === 'purposes' && Array.isArray(value)) { + for (const purposeValue of value) { + const checkboxSelector = `input[name="purposes_${purposeValue}"][type="checkbox"]`; + const checkbox = this.page.locator(checkboxSelector); + if (await checkbox.count() > 0) { + await checkbox.check(); + } + } + return; + } + // Special handling for array parameters that use dynamic input fields if (DYNAMIC_ARRAY_PARAMETERS[paramName]) { const enterValueInput = this.page.locator('input[placeholder="Enter value"]').first(); @@ -143,7 +155,12 @@ class WasmSdkPage extends BaseTest { `[placeholder*="${paramName}"]`, `label:has-text("${paramName}") + input`, `label:has-text("${paramName}") + select`, - `label:has-text("${paramName}") + textarea` + `label:has-text("${paramName}") + textarea`, + // Special cases for contract and document fields + `input[placeholder*="Contract ID"]`, + `input[placeholder*="Document Type"]`, + `textarea[placeholder*="JSON"]`, + `textarea[placeholder*="Schema"]` ]; let found = false; @@ -157,7 +174,27 @@ class WasmSdkPage extends BaseTest { } if (!found) { - console.warn(`⚠️ Could not find input for parameter: ${paramName}`); + console.warn(`⚠️ Could not find input for parameter: ${paramName}. Trying by label text...`); + + // Try finding by label text as last resort + const labelSelectors = [ + `label:text-is("${paramName}") + input`, + `label:text-is("${paramName}") + textarea`, + `label:text-is("${paramName}") + select` + ]; + + for (const selector of labelSelectors) { + const labelInput = this.page.locator(selector).first(); + if (await labelInput.count() > 0) { + await this.fillInputByType(labelInput, value); + found = true; + break; + } + } + + if (!found) { + console.warn(`⚠️ Could not find input for parameter: ${paramName} - skipping`); + } } } else { await this.fillInputByType(input, value); @@ -516,6 +553,243 @@ class WasmSdkPage extends BaseTest { const options = await queryTypeSelect.locator('option').allTextContents(); return options.filter(option => option.trim() !== '' && option !== 'Select Query Type'); } + + /** + * Set up a state transition test scenario + */ + async setupStateTransition(category, transitionType, parameters = {}) { + // Set operation type to transitions + await this.setOperationType('transitions'); + + // Set category and transition type + await this.setQueryCategory(category); + await this.setQueryType(transitionType); + + // Fill in parameters + if (Object.keys(parameters).length > 0) { + await this.fillStateTransitionParameters(parameters); + } + + return this; + } + + /** + * Fill state transition parameters + */ + async fillStateTransitionParameters(parameters) { + // Handle state transition specific parameters + for (const [key, value] of Object.entries(parameters)) { + if (key === 'assetLockProof') { + await this.fillAssetLockProof(value); + } else if (key === 'privateKey') { + await this.fillPrivateKey(value); + } else if (key === 'identityId') { + await this.fillIdentityId(value); + } else if (key === 'seedPhrase') { + await this.fillSeedPhrase(value); + } else if (key === 'identityIndex') { + await this.fillIdentityIndex(value); + } else if (key === 'keySelectionMode') { + // Skip keySelectionMode for now - only needed for identity create + console.log('Skipping keySelectionMode field (identity create only)'); + } else if (key === 'documentFields') { + // Handle document fields - these need to be filled after schema fetch + console.log('Document fields will be handled after schema fetch'); + } else if (key === 'description') { + // Skip description field - it's just for documentation + console.log('Skipping description field (documentation only)'); + } else { + // Use the general parameter filling method for other parameters + await this.fillParameterByName(key, value); + } + } + } + + /** + * Fill asset lock proof field + */ + async fillAssetLockProof(assetLockProof) { + await this.fillInput(this.selectors.assetLockProof, assetLockProof); + console.log('Asset lock proof filled'); + } + + /** + * Fill private key field + */ + async fillPrivateKey(privateKey) { + await this.fillInput(this.selectors.privateKey, privateKey); + console.log('Private key filled'); + } + + /** + * Fill identity ID field (for top-up transitions) + */ + async fillIdentityId(identityId) { + await this.fillInput(this.selectors.identityId, identityId); + console.log('Identity ID filled'); + } + + /** + * Fill seed phrase field + */ + async fillSeedPhrase(seedPhrase) { + const seedPhraseInput = this.page.locator('textarea[name="seedPhrase"]'); + await seedPhraseInput.fill(seedPhrase); + console.log('Seed phrase filled'); + } + + /** + * Fill identity index field + */ + async fillIdentityIndex(identityIndex) { + const identityIndexInput = this.page.locator('input[name="identityIndex"]'); + await identityIndexInput.fill(identityIndex.toString()); + console.log('Identity index filled'); + } + + /** + * Set key selection mode (simple/advanced) + */ + async setKeySelectionMode(mode) { + const keySelectionSelect = this.page.locator('select[name="keySelectionMode"]'); + await keySelectionSelect.selectOption(mode); + console.log(`Key selection mode set to: ${mode}`); + } + + /** + * Execute state transition and get result (similar to executeQueryAndGetResult) + */ + async executeStateTransitionAndGetResult() { + const success = await this.executeQuery(); // Same execute button works for transitions + const result = await this.getResultContent(); + const hasError = await this.hasErrorResult(); + + return { + success, + result, + hasError, + statusText: await this.getStatusBannerText() + }; + } + + /** + * Check if state transition authentication inputs are visible + */ + async hasStateTransitionAuthInputs() { + const authInputs = this.page.locator(this.selectors.authenticationInputs); + const assetLockProofGroup = this.page.locator('#assetLockProofGroup'); + + const authVisible = await authInputs.isVisible(); + const assetLockVisible = await assetLockProofGroup.isVisible(); + + return authVisible && assetLockVisible; + } + + /** + * Fetch document schema and generate dynamic fields for document transitions + */ + async fetchDocumentSchema() { + console.log('Attempting to fetch document schema...'); + + // First check if the function exists and call it directly + try { + await this.page.evaluate(() => { + if (typeof window.fetchDocumentSchema === 'function') { + return window.fetchDocumentSchema(); + } else { + throw new Error('fetchDocumentSchema function not found'); + } + }); + console.log('Called fetchDocumentSchema function directly'); + } catch (error) { + console.error('Error calling fetchDocumentSchema:', error); + throw error; + } + + // Wait for schema to load and fields to be generated + await this.page.waitForTimeout(3000); + + // Check if dynamic fields container is visible + const dynamicFieldsContainer = this.page.locator('#dynamic_documentFields'); + await dynamicFieldsContainer.waitFor({ state: 'visible', timeout: 15000 }); + + console.log('Document schema fetched and fields generated'); + } + + /** + * Fill a specific document field by name + */ + async fillDocumentField(fieldName, value) { + const fieldInput = this.page.locator(`#dynamic_documentFields input[data-field-name="${fieldName}"], #dynamic_documentFields textarea[data-field-name="${fieldName}"]`); + + // Convert value to string based on type + let stringValue = ''; + if (value === null || value === undefined) { + stringValue = ''; + } else if (typeof value === 'object') { + stringValue = JSON.stringify(value); + } else { + stringValue = value.toString(); + } + + await fieldInput.fill(stringValue); + console.log(`Document field '${fieldName}' filled with value: ${stringValue}`); + } + + /** + * Fill multiple document fields + */ + async fillDocumentFields(fields) { + for (const [fieldName, value] of Object.entries(fields)) { + await this.fillDocumentField(fieldName, value); + } + console.log('All document fields filled'); + } + + /** + * Load existing document for replacement (gets revision and populates fields) + */ + async loadExistingDocument() { + console.log('Loading existing document for replacement...'); + + // Call the loadExistingDocument function directly via page.evaluate + try { + await this.page.evaluate(() => { + if (typeof window.loadExistingDocument === 'function') { + return window.loadExistingDocument(); + } else { + throw new Error('loadExistingDocument function not found'); + } + }); + console.log('Existing document loaded successfully'); + } catch (error) { + console.error('Error loading existing document:', error); + throw error; + } + + // Wait for the document to be loaded and fields to be populated + await this.page.waitForTimeout(3000); + + console.log('Document loaded and fields populated'); + } + + /** + * Fill complete state transition authentication (asset lock proof + private key) + */ + async fillStateTransitionAuthentication(assetLockProof, privateKey, identityId = null) { + if (await this.hasStateTransitionAuthInputs()) { + if (assetLockProof) { + await this.fillAssetLockProof(assetLockProof); + } + if (privateKey) { + await this.fillPrivateKey(privateKey); + } + if (identityId) { + await this.fillIdentityId(identityId); + } + console.log('State transition authentication filled'); + } + } } module.exports = { WasmSdkPage };