diff --git a/CHANGELOG.md b/CHANGELOG.md index c846ad2..ff4a92f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.14.6] - 2025-10-01 + +### Enhanced +- **Webhook Error Messages**: Replaced generic "Please try again later or contact support" messages with actionable guidance + - Error messages now extract execution ID and workflow ID from failed webhook triggers + - Guide users to use `n8n_get_execution({id: executionId, mode: 'preview'})` for efficient debugging + - Format: "Workflow {workflowId} execution {executionId} failed. Use n8n_get_execution({id: '{executionId}', mode: 'preview'}) to investigate the error." + - When no execution ID available: "Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate." + +### Added +- New error formatting functions in `n8n-errors.ts`: + - `formatExecutionError()` - Creates execution-specific error messages with debugging guidance + - `formatNoExecutionError()` - Provides guidance when execution context unavailable +- Enhanced `McpToolResponse` type with optional `executionId` and `workflowId` fields +- Error handling documentation in `n8n-trigger-webhook-workflow` tool docs +- 30 new comprehensive tests for error message formatting and webhook error handling + +### Changed +- `handleTriggerWebhookWorkflow` now extracts execution context from error responses +- `getUserFriendlyErrorMessage` returns actual server error messages instead of generic text +- Tool documentation type enhanced with optional `errorHandling` field + +### Fixed +- Test expectations updated to match new error message format (handlers-workflow-diff.test.ts) + +### Benefits +- **Fast debugging**: Preview mode executes in <50ms (vs seconds for full data) +- **Efficient**: Uses ~500 tokens (vs 50K+ tokens for full execution data) +- **Safe**: No timeout or token limit risks +- **Actionable**: Clear next steps for users to investigate failures + +### Impact +- Eliminates unhelpful "contact support" messages +- Provides specific, actionable debugging guidance +- Reduces debugging time by directing users to efficient tools +- 100% backward compatible - only improves error messages + ## [2.14.5] - 2025-09-30 ### Added diff --git a/data/nodes.db b/data/nodes.db index 0c9fc84..e61ec2d 100644 Binary files a/data/nodes.db and b/data/nodes.db differ diff --git a/package.json b/package.json index 1fd326c..e153046 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.14.5", + "version": "2.14.6", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "bin": { diff --git a/package.runtime.json b/package.runtime.json index 623b77f..bcec423 100644 --- a/package.runtime.json +++ b/package.runtime.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp-runtime", - "version": "2.14.4", + "version": "2.14.5", "description": "n8n MCP Server Runtime Dependencies Only", "private": true, "dependencies": { diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index 20a5121..48b4202 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -18,7 +18,9 @@ import { import { N8nApiError, N8nNotFoundError, - getUserFriendlyErrorMessage + getUserFriendlyErrorMessage, + formatExecutionError, + formatNoExecutionError } from '../utils/n8n-errors'; import { logger } from '../utils/logger'; import { z } from 'zod'; @@ -942,7 +944,7 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst try { const client = ensureApiConfigured(context); const input = triggerWebhookSchema.parse(args); - + const webhookRequest: WebhookRequest = { webhookUrl: input.webhookUrl, httpMethod: input.httpMethod || 'POST', @@ -950,9 +952,9 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst headers: input.headers, waitForResponse: input.waitForResponse ?? true }; - + const response = await client.triggerWebhook(webhookRequest); - + return { success: true, data: response, @@ -966,8 +968,35 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst details: { errors: error.errors } }; } - + if (error instanceof N8nApiError) { + // Try to extract execution context from error response + const errorData = error.details as any; + const executionId = errorData?.executionId || errorData?.id || errorData?.execution?.id; + const workflowId = errorData?.workflowId || errorData?.workflow?.id; + + // If we have execution ID, provide specific guidance with n8n_get_execution + if (executionId) { + return { + success: false, + error: formatExecutionError(executionId, workflowId), + code: error.code, + executionId, + workflowId: workflowId || undefined + }; + } + + // No execution ID available - workflow likely didn't start + // Provide guidance to check recent executions + if (error.code === 'SERVER_ERROR' || error.statusCode && error.statusCode >= 500) { + return { + success: false, + error: formatNoExecutionError(), + code: error.code + }; + } + + // For other errors (auth, validation, etc), use standard message return { success: false, error: getUserFriendlyErrorMessage(error), @@ -975,7 +1004,7 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst details: error.details as Record | undefined }; } - + return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' diff --git a/src/mcp/tool-docs/types.ts b/src/mcp/tool-docs/types.ts index 9ddc9d2..0c5da3e 100644 --- a/src/mcp/tool-docs/types.ts +++ b/src/mcp/tool-docs/types.ts @@ -22,6 +22,7 @@ export interface ToolDocumentation { examples: string[]; useCases: string[]; performance: string; + errorHandling?: string; // Optional: Documentation on error handling and debugging bestPractices: string[]; pitfalls: string[]; modeComparison?: string; // Optional: Comparison of different modes for tools with multiple modes diff --git a/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts b/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts index 772b255..decf7e0 100644 --- a/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts +++ b/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts @@ -59,19 +59,59 @@ export const n8nTriggerWebhookWorkflowDoc: ToolDocumentation = { 'Implement event-driven architectures with n8n' ], performance: `Performance varies based on workflow complexity and waitForResponse setting. Synchronous calls (waitForResponse: true) block until workflow completes. For long-running workflows, use async mode (waitForResponse: false) and monitor execution separately.`, + errorHandling: `**Enhanced Error Messages with Execution Guidance** + +When a webhook trigger fails, the error response now includes specific guidance to help debug the issue: + +**Error with Execution ID** (workflow started but failed): +- Format: "Workflow {workflowId} execution {executionId} failed. Use n8n_get_execution({id: '{executionId}', mode: 'preview'}) to investigate the error." +- Response includes: executionId and workflowId fields for direct access +- Recommended action: Use n8n_get_execution with mode='preview' for fast, efficient error inspection + +**Error without Execution ID** (workflow didn't start): +- Format: "Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate." +- Recommended action: Check recent executions with n8n_list_executions + +**Why mode='preview'?** +- Fast: <50ms response time +- Efficient: ~500 tokens (vs 50K+ for full mode) +- Safe: No timeout or token limit risks +- Informative: Shows structure, counts, and error details +- Provides recommendations for fetching more data if needed + +**Example Error Responses**: +\`\`\`json +{ + "success": false, + "error": "Workflow wf_123 execution exec_456 failed. Use n8n_get_execution({id: 'exec_456', mode: 'preview'}) to investigate the error.", + "executionId": "exec_456", + "workflowId": "wf_123", + "code": "SERVER_ERROR" +} +\`\`\` + +**Investigation Workflow**: +1. Trigger returns error with execution ID +2. Call n8n_get_execution({id: executionId, mode: 'preview'}) to see structure and error +3. Based on preview recommendation, fetch more data if needed +4. Fix issues in workflow and retry`, bestPractices: [ 'Always verify workflow is active before attempting webhook triggers', 'Match HTTP method exactly with webhook node configuration', 'Use async mode (waitForResponse: false) for long-running workflows', 'Include authentication headers when webhook requires them', - 'Test webhook URL manually first to ensure it works' + 'Test webhook URL manually first to ensure it works', + 'When errors occur, use n8n_get_execution with mode="preview" first for efficient debugging', + 'Store execution IDs from error responses for later investigation' ], pitfalls: [ 'Workflow must be ACTIVE - inactive workflows cannot be triggered', 'HTTP method mismatch returns 404 even if URL is correct', 'Webhook node must be the trigger node in the workflow', 'Timeout errors occur with long workflows in sync mode', - 'Data format must match webhook node expectations' + 'Data format must match webhook node expectations', + 'Error messages always include n8n_get_execution guidance - follow the suggested steps for efficient debugging', + 'Execution IDs in error responses are crucial for debugging - always check for and use them' ], relatedTools: ['n8n_get_execution', 'n8n_list_executions', 'n8n_get_workflow', 'n8n_create_workflow'] } diff --git a/src/types/n8n-api.ts b/src/types/n8n-api.ts index 328e6e5..bde1d42 100644 --- a/src/types/n8n-api.ts +++ b/src/types/n8n-api.ts @@ -290,6 +290,8 @@ export interface McpToolResponse { message?: string; code?: string; details?: Record; + executionId?: string; + workflowId?: string; } // Execution Filtering Types diff --git a/src/utils/n8n-errors.ts b/src/utils/n8n-errors.ts index 7b6a27d..d8a7c5b 100644 --- a/src/utils/n8n-errors.ts +++ b/src/utils/n8n-errors.ts @@ -95,6 +95,25 @@ export function handleN8nApiError(error: unknown): N8nApiError { return new N8nApiError('Unknown error occurred', undefined, 'UNKNOWN_ERROR', error); } +/** + * Format execution error message with guidance to use n8n_get_execution + * @param executionId - The execution ID from the failed execution + * @param workflowId - Optional workflow ID + * @returns Formatted error message with n8n_get_execution guidance + */ +export function formatExecutionError(executionId: string, workflowId?: string): string { + const workflowPrefix = workflowId ? `Workflow ${workflowId} execution ` : 'Execution '; + return `${workflowPrefix}${executionId} failed. Use n8n_get_execution({id: '${executionId}', mode: 'preview'}) to investigate the error.`; +} + +/** + * Format error message when no execution ID is available + * @returns Generic guidance to check executions + */ +export function formatNoExecutionError(): string { + return "Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate."; +} + // Utility to extract user-friendly error messages export function getUserFriendlyErrorMessage(error: N8nApiError): string { switch (error.code) { @@ -109,7 +128,9 @@ export function getUserFriendlyErrorMessage(error: N8nApiError): string { case 'NO_RESPONSE': return 'Unable to connect to n8n. Please check the server URL and ensure n8n is running.'; case 'SERVER_ERROR': - return 'n8n server error. Please try again later or contact support.'; + // For server errors, we should not show generic message + // Callers should check for execution context and use formatExecutionError instead + return error.message || 'n8n server error occurred'; default: return error.message || 'An unexpected error occurred'; } diff --git a/tests/unit/mcp/handlers-n8n-manager.test.ts b/tests/unit/mcp/handlers-n8n-manager.test.ts index 78db946..ab96a84 100644 --- a/tests/unit/mcp/handlers-n8n-manager.test.ts +++ b/tests/unit/mcp/handlers-n8n-manager.test.ts @@ -542,7 +542,7 @@ describe('handlers-n8n-manager', () => { expect(result).toEqual({ success: false, - error: 'n8n server error. Please try again later or contact support.', + error: 'Service unavailable', code: 'SERVER_ERROR', details: { apiUrl: 'https://n8n.test.com', @@ -642,4 +642,179 @@ describe('handlers-n8n-manager', () => { }); }); }); + + describe('handleTriggerWebhookWorkflow', () => { + it('should trigger webhook successfully', async () => { + const webhookResponse = { + status: 200, + statusText: 'OK', + data: { result: 'success' }, + headers: {} + }; + + mockApiClient.triggerWebhook.mockResolvedValue(webhookResponse); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test-123', + httpMethod: 'POST', + data: { test: 'data' } + }); + + expect(result).toEqual({ + success: true, + data: webhookResponse, + message: 'Webhook triggered successfully' + }); + }); + + it('should extract execution ID from webhook error response', async () => { + const apiError = new N8nServerError('Workflow execution failed'); + apiError.details = { + executionId: 'exec_abc123', + workflowId: 'wf_xyz789' + }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test-123', + httpMethod: 'POST' + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Workflow wf_xyz789 execution exec_abc123 failed'); + expect(result.error).toContain('n8n_get_execution'); + expect(result.error).toContain("mode: 'preview'"); + expect(result.executionId).toBe('exec_abc123'); + expect(result.workflowId).toBe('wf_xyz789'); + }); + + it('should extract execution ID without workflow ID', async () => { + const apiError = new N8nServerError('Execution failed'); + apiError.details = { + executionId: 'exec_only_123' + }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test-123', + httpMethod: 'GET' + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Execution exec_only_123 failed'); + expect(result.error).toContain('n8n_get_execution'); + expect(result.error).toContain("mode: 'preview'"); + expect(result.executionId).toBe('exec_only_123'); + expect(result.workflowId).toBeUndefined(); + }); + + it('should handle execution ID as "id" field', async () => { + const apiError = new N8nServerError('Error'); + apiError.details = { + id: 'exec_from_id_field', + workflowId: 'wf_test' + }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error).toContain('exec_from_id_field'); + expect(result.executionId).toBe('exec_from_id_field'); + }); + + it('should provide generic guidance when no execution ID is available', async () => { + const apiError = new N8nServerError('Server error without execution context'); + apiError.details = {}; // No execution ID + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Workflow failed to execute'); + expect(result.error).toContain('n8n_list_executions'); + expect(result.error).toContain('n8n_get_execution'); + expect(result.error).toContain("mode='preview'"); + expect(result.executionId).toBeUndefined(); + }); + + it('should use standard error message for authentication errors', async () => { + const authError = new N8nAuthenticationError('Invalid API key'); + mockApiClient.triggerWebhook.mockRejectedValue(authError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result).toEqual({ + success: false, + error: 'Failed to authenticate with n8n. Please check your API key.', + code: 'AUTHENTICATION_ERROR', + details: undefined + }); + }); + + it('should use standard error message for validation errors', async () => { + const validationError = new N8nValidationError('Invalid webhook URL'); + mockApiClient.triggerWebhook.mockRejectedValue(validationError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error).toBe('Invalid request: Invalid webhook URL'); + expect(result.code).toBe('VALIDATION_ERROR'); + }); + + it('should handle invalid input with Zod validation error', async () => { + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'not-a-url', + httpMethod: 'INVALID_METHOD' + }); + + expect(result.success).toBe(false); + expect(result.error).toBe('Invalid input'); + expect(result.details).toHaveProperty('errors'); + }); + + it('should not include "contact support" in error messages', async () => { + const apiError = new N8nServerError('Test error'); + apiError.details = { executionId: 'test_exec' }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error?.toLowerCase()).not.toContain('contact support'); + expect(result.error?.toLowerCase()).not.toContain('try again later'); + }); + + it('should always recommend preview mode in error messages', async () => { + const apiError = new N8nServerError('Error'); + apiError.details = { executionId: 'test_123' }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error).toMatch(/mode:\s*'preview'/); + }); + }); }); \ No newline at end of file diff --git a/tests/unit/mcp/handlers-workflow-diff.test.ts b/tests/unit/mcp/handlers-workflow-diff.test.ts index 64e7941..da1bd07 100644 --- a/tests/unit/mcp/handlers-workflow-diff.test.ts +++ b/tests/unit/mcp/handlers-workflow-diff.test.ts @@ -499,7 +499,7 @@ describe('handlers-workflow-diff', () => { expect(result).toEqual({ success: false, - error: 'n8n server error. Please try again later or contact support.', + error: 'Internal server error', code: 'SERVER_ERROR', }); }); diff --git a/tests/unit/utils/n8n-errors.test.ts b/tests/unit/utils/n8n-errors.test.ts new file mode 100644 index 0000000..0d2f12f --- /dev/null +++ b/tests/unit/utils/n8n-errors.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect } from 'vitest'; +import { + formatExecutionError, + formatNoExecutionError, + getUserFriendlyErrorMessage, + N8nApiError, + N8nAuthenticationError, + N8nNotFoundError, + N8nValidationError, + N8nRateLimitError, + N8nServerError +} from '../../../src/utils/n8n-errors'; + +describe('formatExecutionError', () => { + it('should format error with both execution ID and workflow ID', () => { + const result = formatExecutionError('exec_12345', 'wf_abc'); + + expect(result).toBe("Workflow wf_abc execution exec_12345 failed. Use n8n_get_execution({id: 'exec_12345', mode: 'preview'}) to investigate the error."); + expect(result).toContain('mode: \'preview\''); + expect(result).toContain('exec_12345'); + expect(result).toContain('wf_abc'); + }); + + it('should format error with only execution ID', () => { + const result = formatExecutionError('exec_67890'); + + expect(result).toBe("Execution exec_67890 failed. Use n8n_get_execution({id: 'exec_67890', mode: 'preview'}) to investigate the error."); + expect(result).toContain('mode: \'preview\''); + expect(result).toContain('exec_67890'); + expect(result).not.toContain('Workflow'); + }); + + it('should include preview mode guidance', () => { + const result = formatExecutionError('test_id'); + + expect(result).toMatch(/mode:\s*'preview'/); + }); + + it('should format with undefined workflow ID (treated as missing)', () => { + const result = formatExecutionError('exec_123', undefined); + + expect(result).toBe("Execution exec_123 failed. Use n8n_get_execution({id: 'exec_123', mode: 'preview'}) to investigate the error."); + }); + + it('should properly escape execution ID in suggestion', () => { + const result = formatExecutionError('exec-with-special_chars.123'); + + expect(result).toContain("id: 'exec-with-special_chars.123'"); + }); +}); + +describe('formatNoExecutionError', () => { + it('should provide guidance to check recent executions', () => { + const result = formatNoExecutionError(); + + expect(result).toBe("Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate."); + expect(result).toContain('n8n_list_executions'); + expect(result).toContain('n8n_get_execution'); + expect(result).toContain("mode='preview'"); + }); + + it('should include preview mode in guidance', () => { + const result = formatNoExecutionError(); + + expect(result).toMatch(/mode\s*=\s*'preview'/); + }); +}); + +describe('getUserFriendlyErrorMessage', () => { + it('should handle authentication error', () => { + const error = new N8nAuthenticationError('Invalid API key'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Failed to authenticate with n8n. Please check your API key.'); + }); + + it('should handle not found error', () => { + const error = new N8nNotFoundError('Workflow', '123'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toContain('not found'); + }); + + it('should handle validation error', () => { + const error = new N8nValidationError('Missing required field'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Invalid request: Missing required field'); + }); + + it('should handle rate limit error', () => { + const error = new N8nRateLimitError(60); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Too many requests. Please wait a moment and try again.'); + }); + + it('should handle server error with custom message', () => { + const error = new N8nServerError('Database connection failed', 503); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Database connection failed'); + }); + + it('should handle server error without message', () => { + const error = new N8nApiError('', 500, 'SERVER_ERROR'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('n8n server error occurred'); + }); + + it('should handle no response error', () => { + const error = new N8nApiError('Network error', undefined, 'NO_RESPONSE'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Unable to connect to n8n. Please check the server URL and ensure n8n is running.'); + }); + + it('should handle unknown error with message', () => { + const error = new N8nApiError('Custom error message'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Custom error message'); + }); + + it('should handle unknown error without message', () => { + const error = new N8nApiError(''); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('An unexpected error occurred'); + }); +}); + +describe('Error message integration', () => { + it('should use formatExecutionError for webhook failures with execution ID', () => { + const executionId = 'exec_webhook_123'; + const workflowId = 'wf_webhook_abc'; + const message = formatExecutionError(executionId, workflowId); + + expect(message).toContain('Workflow wf_webhook_abc execution exec_webhook_123 failed'); + expect(message).toContain('n8n_get_execution'); + expect(message).toContain("mode: 'preview'"); + }); + + it('should use formatNoExecutionError for server errors without execution context', () => { + const message = formatNoExecutionError(); + + expect(message).toContain('Workflow failed to execute'); + expect(message).toContain('n8n_list_executions'); + expect(message).toContain('n8n_get_execution'); + }); + + it('should not include "contact support" in any error message', () => { + const executionMessage = formatExecutionError('test'); + const noExecutionMessage = formatNoExecutionError(); + const serverError = new N8nServerError(); + const serverErrorMessage = getUserFriendlyErrorMessage(serverError); + + expect(executionMessage.toLowerCase()).not.toContain('contact support'); + expect(noExecutionMessage.toLowerCase()).not.toContain('contact support'); + expect(serverErrorMessage.toLowerCase()).not.toContain('contact support'); + }); + + it('should always guide users to use preview mode first', () => { + const executionMessage = formatExecutionError('test'); + const noExecutionMessage = formatNoExecutionError(); + + expect(executionMessage).toContain("mode: 'preview'"); + expect(noExecutionMessage).toContain("mode='preview'"); + }); +});