From 0835f6e6aaeb7b3f36aff81747c9b42eadb9843d Mon Sep 17 00:00:00 2001 From: yasser khan Date: Tue, 16 Sep 2025 09:33:38 +0530 Subject: [PATCH] feat: add AI-powered pilot framework for Detox E2E testing (#8981) --- detox/README.md | 73 ++ .../e2e/support/pilot/ClaudePromptHandler.ts | 128 +++ detox/e2e/support/server_api/index.ts | 4 + detox/e2e/support/server_api/playbooks.ts | 752 ++++++++++++++++++ .../support/server_api/playbooks_helpers.ts | 144 ++++ detox/e2e/support/utils/detoxhelpers.ts | 25 + detox/e2e/support/utils/index.ts | 1 + .../channels}/account/about.e2e.ts | 0 .../channels}/account/account_menu.e2e.ts | 0 .../account/advanced_settings.e2e.ts | 0 ...uto_responder_notification_settings.e2e.ts | 0 .../account/clock_display_settings.e2e.ts | 0 .../channels}/account/custom_status.e2e.ts | 0 .../channels}/account/display_settings.e2e.ts | 0 .../channels}/account/edit_profile.e2e.ts | 0 .../email_notification_settings.e2e.ts | 0 .../mention_notification_settings.e2e.ts | 0 .../account/notification_settings.e2e.ts | 0 .../account/push_notification_settings.e2e.ts | 0 .../channels}/account/settings.e2e.ts | 0 .../account/theme_display_settings.e2e.ts | 0 .../account/timezone_display_settings.e2e.ts | 0 .../channels}/autocomplete/at_mention.e2e.ts | 0 .../autocomplete/channel_mention.e2e.ts | 0 .../autocomplete/channel_post_draft.e2e.ts | 0 .../autocomplete/create_channel.e2e.ts | 0 .../autocomplete/edit_channel.e2e.ts | 0 .../autocomplete/edit_channel_header.e2e.ts | 0 .../channels}/autocomplete/edit_post.e2e.ts | 0 .../autocomplete/emoji_suggestion.e2e.ts | 0 .../autocomplete/slash_suggestion.e2e.ts | 0 .../autocomplete/thread_post_draft.e2e.ts | 0 .../channels}/channels/archive_channel.e2e.ts | 0 .../channels}/channels/browse_channels.e2e.ts | 0 .../channels}/channels/channel_info.e2e.ts | 0 .../channels}/channels/channel_list.e2e.ts | 0 .../channels/channel_post_list.e2e.ts | 0 .../convert_to_private_channel.e2e.ts | 0 ...ate_channel_and_edit_channel_header.e2e.ts | 0 .../channels/create_direct_message.e2e.ts | 0 .../channels}/channels/edit_channel.e2e.ts | 0 .../favorite_and_unfavorite_channel.e2e.ts | 0 .../channels}/channels/find_channels.e2e.ts | 0 .../channels}/channels/leave_channel.e2e.ts | 0 .../channels/mute_and_unmute_channel.e2e.ts | 0 .../channels/unarchive_channel.e2e.ts | 0 .../channels}/messaging/at_mention.e2e.ts | 0 .../channels}/messaging/channel_link.e2e.ts | 0 .../messaging/channel_mention.e2e.ts | 0 .../messaging/emojis_and_reactions.e2e.ts | 0 .../follow_and_unfollow_messsage.e2e.ts | 0 .../messaging/markdown_block_quote.e2e.ts | 0 .../channels}/messaging/markdown_code.e2e.ts | 0 .../messaging/markdown_heading.e2e.ts | 0 .../channels}/messaging/markdown_image.e2e.ts | 0 .../channels}/messaging/markdown_latex.e2e.ts | 0 .../channels}/messaging/markdown_list.e2e.ts | 0 .../messaging/markdown_separator.e2e.ts | 0 .../channels}/messaging/markdown_table.e2e.ts | 0 .../channels}/messaging/message_delete.e2e.ts | 0 .../channels}/messaging/message_draft.e2e.ts | 0 .../channels}/messaging/message_edit.e2e.ts | 4 +- .../messaging/message_local_drafts.e2e.ts | 0 .../channels}/messaging/message_post.e2e.ts | 0 .../channels}/messaging/message_reply.e2e.ts | 0 .../channels}/messaging/permalink.e2e.ts | 0 .../messaging/pin_and_unpin_message.e2e.ts | 0 .../messaging/save_and_unsave_message.e2e.ts | 0 .../create_schedule_message.e2e.ts | 0 .../channels}/search/pinned_messages.e2e.ts | 2 +- .../channels}/search/recent_mentions.e2e.ts | 2 +- .../channels}/search/saved_messages.e2e.ts | 2 +- .../channels}/search/search_messages.e2e.ts | 2 +- .../server_login/connect_to_server.e2e.ts | 0 .../server_login/login_by_email.e2e.ts | 0 .../preauth_secret_connection.e2e.ts | 0 .../channels}/server_login/server_list.e2e.ts | 0 .../channels}/smoke_test/account.e2e.ts | 0 .../channels}/smoke_test/autocomplete.e2e.ts | 0 .../channels}/smoke_test/channels.e2e.ts | 0 .../channels}/smoke_test/messaging.e2e.ts | 2 +- .../channels}/smoke_test/search.e2e.ts | 0 .../channels}/smoke_test/server_login.e2e.ts | 0 .../channels}/smoke_test/threads.e2e.ts | 0 .../channels}/teams/invite_people.e2e.ts | 0 .../threads/follow_and_unfollow_thread.e2e.ts | 0 .../channels}/threads/global_threads.e2e.ts | 0 .../mark_thread_as_read_and_unread.e2e.ts | 0 .../threads/open_thread_in_channel.e2e.ts | 0 .../channels}/threads/reply_to_thread.e2e.ts | 0 .../threads/save_and_unsave_thread.e2e.ts | 0 .../__pilot_cache__/playbooks.e2e.json | 497 ++++++++++++ .../test/products/playbooks/playbooks.e2e.ts | 228 ++++++ detox/e2e/test/setup.ts | 16 + detox/package-lock.json | 71 +- detox/package.json | 5 + detox/save_report.js | 4 +- 97 files changed, 1925 insertions(+), 37 deletions(-) create mode 100644 detox/e2e/support/pilot/ClaudePromptHandler.ts create mode 100644 detox/e2e/support/server_api/playbooks.ts create mode 100644 detox/e2e/support/server_api/playbooks_helpers.ts create mode 100644 detox/e2e/support/utils/detoxhelpers.ts rename detox/e2e/test/{ => products/channels}/account/about.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/account_menu.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/advanced_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/auto_responder_notification_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/clock_display_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/custom_status.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/display_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/edit_profile.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/email_notification_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/mention_notification_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/notification_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/push_notification_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/theme_display_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/account/timezone_display_settings.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/at_mention.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/channel_mention.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/channel_post_draft.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/create_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/edit_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/edit_channel_header.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/edit_post.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/emoji_suggestion.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/slash_suggestion.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/autocomplete/thread_post_draft.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/archive_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/browse_channels.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/channel_info.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/channel_list.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/channel_post_list.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/convert_to_private_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/create_channel_and_edit_channel_header.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/create_direct_message.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/edit_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/favorite_and_unfavorite_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/find_channels.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/leave_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/mute_and_unmute_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/channels/unarchive_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/at_mention.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/channel_link.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/channel_mention.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/emojis_and_reactions.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/follow_and_unfollow_messsage.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_block_quote.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_code.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_heading.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_image.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_latex.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_list.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_separator.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/markdown_table.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/message_delete.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/message_draft.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/message_edit.e2e.ts (99%) rename detox/e2e/test/{ => products/channels}/messaging/message_local_drafts.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/message_post.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/message_reply.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/permalink.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/pin_and_unpin_message.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/messaging/save_and_unsave_message.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/scheduled_messages/create_schedule_message.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/search/pinned_messages.e2e.ts (99%) rename detox/e2e/test/{ => products/channels}/search/recent_mentions.e2e.ts (99%) rename detox/e2e/test/{ => products/channels}/search/saved_messages.e2e.ts (99%) rename detox/e2e/test/{ => products/channels}/search/search_messages.e2e.ts (99%) rename detox/e2e/test/{ => products/channels}/server_login/connect_to_server.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/server_login/login_by_email.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/server_login/preauth_secret_connection.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/server_login/server_list.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/smoke_test/account.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/smoke_test/autocomplete.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/smoke_test/channels.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/smoke_test/messaging.e2e.ts (99%) rename detox/e2e/test/{ => products/channels}/smoke_test/search.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/smoke_test/server_login.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/smoke_test/threads.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/teams/invite_people.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/threads/follow_and_unfollow_thread.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/threads/global_threads.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/threads/mark_thread_as_read_and_unread.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/threads/open_thread_in_channel.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/threads/reply_to_thread.e2e.ts (100%) rename detox/e2e/test/{ => products/channels}/threads/save_and_unsave_thread.e2e.ts (100%) create mode 100644 detox/e2e/test/products/playbooks/__pilot_cache__/playbooks.e2e.json create mode 100644 detox/e2e/test/products/playbooks/playbooks.e2e.ts diff --git a/detox/README.md b/detox/README.md index bbacd68a3..6bad8ec14 100644 --- a/detox/README.md +++ b/detox/README.md @@ -85,3 +85,76 @@ npm run e2e:android-test path to test file. The Local Runs generate artifacts under `detox/artifacts/ios-debug-**` or `detox/artifacts/android-debug-**`. You can see the html report, failure screenshot under that folder. + +# Playbooks Tests (AI-Powered Testing) + +The Playbooks tests leverage AI-powered testing through the Wix Pilot framework, enabling natural language test creation and execution. + +### How It Works + +The Playbooks tests utilize the [pilot library](https://wix-pilot.com/docs/guides/technical-overview), which connects to Anthropic Claude via the `ClaudePromptHandler`. This innovative approach allows you to: + +- Write tests using natural language prompts +- Automatically translate prompts into executable test steps +- Maintain tests more intuitively through conversational interfaces +- Handle complex scenarios with AI-generated test logic + +The system integrates seamlessly with your existing Detox test infrastructure while adding AI capabilities. + +### Best Practices + +For reliable and effective Playbooks tests, follow these guidelines from the [Pilot Best Practices Guide](https://wix-pilot.com/docs/guides/pilot-best-practices): + +1. **Prompt Clarity**: + - Write clear, concise prompts + - Use specific action-oriented language + - Avoid ambiguous phrasing + +2. **Test Design**: + - Keep prompts focused on single scenarios + - Use deterministic language to prevent flaky tests + - Structure complex tests as sequences of simple prompts + +3. **Maintenance**: + - Review AI-generated test steps before committing + - Version control both prompts and generated code + - Monitor test stability regularly + +4. **Integration**: + - Combine AI tests with traditional assertions + - Use AI for complex scenarios, traditional methods for simple verifications + - Document which tests are AI-generated + +### Getting Started + +To run Playbooks tests: + +1. **Set up your environment**: + ```sh + export ANTHROPIC_API_KEY='your-api-key-here' + ``` + +2. **Run tests normally**: + - The system will automatically initialize the `ClaudePromptHandler` when the API key is detected + - No additional configuration is required beyond the API key + +3. **Execution examples**: + ```sh + # Run all Playbooks tests + npm run e2e:android-test tests/playbooks/ + + # Run a specific Playbook test + npm run e2e:ios-test tests/playbooks/playbooks_basic.e2e.ts + ``` + +### Technical Notes + +- The Pilot framework handles the translation between natural language prompts and executable code +- Tests are cached locally after generation for faster subsequent runs +- All AI interactions are logged in the test artifacts for debugging +- The system falls back to traditional testing methods if the API key isn't available + +For complete documentation, see: +- [Wix Pilot Technical Overview](https://wix-pilot.com/docs/guides/technical-overview) +- [Pilot Best Practices Guide](https://wix-pilot.com/docs/guides/pilot-best-practices) + diff --git a/detox/e2e/support/pilot/ClaudePromptHandler.ts b/detox/e2e/support/pilot/ClaudePromptHandler.ts new file mode 100644 index 000000000..b5bcab09e --- /dev/null +++ b/detox/e2e/support/pilot/ClaudePromptHandler.ts @@ -0,0 +1,128 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +/* eslint-disable no-console */ + +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; + +import {Anthropic} from '@anthropic-ai/sdk'; + +export class ClaudePromptHandler { + private anthropic: Anthropic; + private cachePath: string; + private cacheEnabled: boolean; + + constructor(apiKey: string, cachePath?: string) { + this.anthropic = new Anthropic({ + apiKey, + }); + this.cachePath = cachePath || path.join(process.cwd(), 'detox/e2e/test/.pilot_cache.json'); + this.cacheEnabled = !process.env.PILOT_CACHE_DISABLED; + + // Ensure cache directory exists + if (this.cacheEnabled && !fs.existsSync(path.dirname(this.cachePath))) { + fs.mkdirSync(path.dirname(this.cachePath), {recursive: true}); + } + } + + private getCacheKey(prompt: string): string { + return crypto.createHash('sha256').update(prompt).digest('hex'); + } + + private loadCache(): Record { + try { + if (fs.existsSync(this.cachePath)) { + return JSON.parse(fs.readFileSync(this.cachePath, 'utf-8')); + } + } catch (error) { + console.warn('Failed to load cache:', error); + } + return {}; + } + + private saveCache(cache: Record): void { + try { + fs.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2)); + } catch (error) { + console.warn('Failed to save cache:', error); + } + } + + async runPrompt(prompt: string): Promise { + const systemPrompt = ` + You are a test automation assistant for a React Native application using Detox for end-to-end testing. Generate test steps that are: + - Generic and adaptable to various UI components (e.g., lists, buttons, text fields). + - Compatible with Detox matchers (by.id, by.text, by.textMatching for partial matches). + - Resilient to timing issues by including waits, retries, or scrolling when needed. + - Prioritize using testID for element identification over text or component type. + - Handle dynamic content with partial text matches or regex-based matching. + - Include explicit waits for loading states or animations (e.g., waitFor with reasonable timeouts). + - Ensure elements are visible by scrolling if necessary before verifying visibility. + - Avoid assuming immediate visibility; check if scrolling or waiting is required. + - Use longer timeouts (e.g., 10-15 seconds) for operations prone to delays. + - If a list is involved, generate steps to scroll to the target element. + - Use regex for text matching when applicable, e.g., element(by.text(/text [A-Za-z]+/i)). + + Example steps for verifying a list item: + - 'Wait for the list with testID "myList" to be visible for up to 10 seconds' + - 'Scroll the list with testID "myList" until text containing "item-name" is visible' + - 'Verify text containing "item-name" is visible with a 10-second timeout' + - User regex matcher. e.g element(by.text(/text [A-Za-z]+/i)); + + Avoid: + - Hardcoding component types like ReactScrollView or exact text strings. + - Assuming elements are immediately visible without scrolling or waiting. + - Using short timeouts (e.g., 5 seconds) for UI interactions. + `; + const cacheKey = this.getCacheKey(prompt); + + // If caching is enabled + if (this.cacheEnabled) { + const cache = this.loadCache(); + + // Check if we have a cached response + if (cache[cacheKey]) { + console.log('Using cached response for prompt'); + return cache[cacheKey].code; + } + } + + // No cache hit, make API call + console.log('Making API call to Claude'); + const response = await this.anthropic.messages.create({ + model: 'claude-opus-4-20250514', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: prompt, + }, + ], + system: systemPrompt, + }); + + // Extract text from the first content block + const firstContent = response.content[0]; + if (firstContent && firstContent.type === 'text') { + const result = firstContent.text; + + // Save to cache if enabled + if (this.cacheEnabled) { + const cache = this.loadCache(); + cache[cacheKey] = { + code: result, + creationTime: Date.now(), + }; + this.saveCache(cache); + } + + return result; + } + + // Return empty string if no text content is found + return ''; + } + + isSnapshotImageSupported = () => false; // Disable images for Mattermost tests +} diff --git a/detox/e2e/support/server_api/index.ts b/detox/e2e/support/server_api/index.ts index 576ca0677..4744b969a 100644 --- a/detox/e2e/support/server_api/index.ts +++ b/detox/e2e/support/server_api/index.ts @@ -4,6 +4,8 @@ import Bot from './bot'; import Channel from './channel'; import Ldap from './ldap'; +import Playbooks from './playbooks'; +import PlaybooksHelpers from './playbooks_helpers'; import Plugin from './plugin'; import Post from './post'; import Preference from './preference'; @@ -17,6 +19,8 @@ export { Bot, Channel, Ldap, + Playbooks, + PlaybooksHelpers, Plugin, Post, Preference, diff --git a/detox/e2e/support/server_api/playbooks.ts b/detox/e2e/support/server_api/playbooks.ts new file mode 100644 index 000000000..34fe60fbe --- /dev/null +++ b/detox/e2e/support/server_api/playbooks.ts @@ -0,0 +1,752 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import client from './client'; +import {getResponseFromError} from './common'; +import PlaybooksHelpers from './playbooks_helpers'; + +const playbookRunsEndpoint = '/plugins/playbooks/api/v0/runs'; +const playbooksEndpoint = '/plugins/playbooks/api/v0/playbooks'; +const configEndpoint = '/api/v4/config'; +const pluginsEndpoint = '/api/v4/plugins'; +const StatusOK = 200; + +interface PlaybookRun { + id?: string; + name: string; + owner_user_id: string; + team_id: string; + playbook_id: string; + channel_id: string; + description?: string; + end_at?: number; +} + +interface Checklist { + title: string; + items: ChecklistItem[]; +} + +interface ChecklistItem { + title: string; + description?: string; + command?: string; + state?: string; + assignee_id?: string; +} + +interface PlaybookMember { + user_id: string; + roles: string[]; +} + +interface Metric { + id?: string; + title: string; + description?: string; + type: string; + target?: number; +} + +interface Playbook { + id?: string; + title: string; + description?: string; + team_id: string; + create_public_playbook_run?: boolean; + create_channel_member_on_new_participant?: boolean; + checklists: Checklist[]; + members?: PlaybookMember[]; + public?: boolean; + broadcast_enabled?: boolean; + broadcast_channel_ids?: string[]; + reminder_message_template?: string; + reminder_timer_default_seconds?: number; + status_update_enabled?: boolean; + retrospective_reminder_interval_seconds?: number; + retrospective_template?: string; + retrospective_enabled?: boolean; + invited_user_ids?: string[]; + invite_users_enabled?: boolean; + default_owner_id?: string; + default_owner_enabled?: boolean; + announcement_channel_id?: string; + announcement_channel_enabled?: boolean; + webhook_on_creation_urls?: string[]; + webhook_on_creation_enabled?: boolean; + webhook_on_status_update_urls?: string[]; + webhook_on_status_update_enabled?: boolean; + message_on_join?: string; + message_on_join_enabled?: boolean; + signal_any_keywords?: string[]; + signal_any_keywords_enabled?: boolean; + channel_name_template?: string; + run_summary_template?: string; + run_summary_template_enabled?: boolean; + channel_mode?: string; + channel_id?: string; + metrics?: Metric[]; +} + +/** + * Get the server configuration + * @param {string} baseUrl - the base server URL + * @param {boolean} old - whether to use the old config format + * @return {Object} returns { config: Object } on success or { error, status } on error + */ +export const apiGetConfig = async (baseUrl: string, old: boolean = false): Promise => { + try { + const response = await client.get( + `${baseUrl}${configEndpoint}${old ? '/client?format=old' : ''}`, + ); + + if (response.status === StatusOK) { + return {config: response.data}; + } + throw new Error(`Unexpected status code: ${response.status}`); + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Update the server configuration + * @param {string} baseUrl - the base server URL + * @param {Object} newConfig - the configuration updates to apply + * @return {Object} returns { config: Object } on success or { error, status } on error + */ +export const apiUpdateConfig = async (baseUrl: string, newConfig: any = {}): Promise => { + try { + // Get current config + const currentConfigResponse = await apiGetConfig(baseUrl); + + if ('error' in currentConfigResponse) { + throw new Error(`Failed to get current config: ${currentConfigResponse.error}`); + } + const currentConfig = currentConfigResponse.config; + + // Merge configurations (basic merge; replace with your merge utility if available) + const config = {...currentConfig, ...newConfig}; + + // Update the config + const response = await client.put( + `${baseUrl}${configEndpoint}/patch`, + config, + { + headers: {'X-Requested-With': 'XMLHttpRequest'}, + }, + ); + + if (response.status === StatusOK) { + // Fetch the updated config + return await apiGetConfig(baseUrl); + } + throw new Error(`Unexpected status code: ${response.status}`); + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Enable the Playbooks plugin + * @param {string} baseUrl - the base server URL + * @return {Object} returns { config: Object } on success or { error, status } on error + */ +export const apiEnablePlaybooksPlugin = async (baseUrl: string): Promise => { + try { + // Get current config + const currentConfigResponse = await apiGetConfig(baseUrl); + + if ('error' in currentConfigResponse) { + throw new Error(`Failed to get current config: ${currentConfigResponse.error}`); + } + + // Update the Playbooks plugin state to enabled + const updatedConfig = { + PluginSettings: { + ...currentConfigResponse.config.PluginSettings, + PluginStates: { + ...currentConfigResponse.config.PluginSettings.PluginStates, + playbooks: { + ...currentConfigResponse.config.PluginSettings.PluginStates.playbooks, + Enabled: true, + }, + }, + }, + }; + + // Update the configuration + const update = await apiUpdateConfig(baseUrl, updatedConfig); + + return update; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Enable a plugin by ID + * @param {string} baseUrl - the base server URL + * @param {string} pluginId - the ID of the plugin to enable (e.g., 'playbooks') + * @return {Object} returns { status: string } on success or { error, status } on error + */ +export const apiEnablePlugin = async (baseUrl: string, pluginId: string): Promise => { + try { + const response = await client.post( + `${baseUrl}${pluginsEndpoint}/${pluginId}/enable`, + ); + + if (response.status === StatusOK) { + return {status: response.data.status}; + } + throw new Error(`Unexpected status code: ${response.status}`); + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Get all playbook runs + * @param {string} baseUrl - the base server URL + * @param {string} teamId - the team ID + * @param {Object} options - additional options like per_page + * @return {Object} returns {items: PlaybookRun[]} on success or {error, status} on error + */ +export const apiGetAllPlaybookRuns = async (baseUrl: string, teamId: string, options: {per_page?: number} = {}): Promise => { + try { + const response = await client.get( + `${baseUrl}${playbookRunsEndpoint}`, + { + params: { + team_id: teamId, + per_page: options.per_page || 10000, + }, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Get all InProgress playbook runs + * @param {string} baseUrl - the base server URL + * @param {string} teamId - the team ID + * @param {string} userId - optional user ID to filter by participant + * @return {Object} returns {items: PlaybookRun[]} on success or {error, status} on error + */ +export const apiGetAllInProgressPlaybookRuns = async (baseUrl: string, teamId: string, userId: string = ''): Promise => { + try { + const response = await client.get( + `${baseUrl}${playbookRunsEndpoint}`, + { + params: { + team_id: teamId, + status: 'InProgress', + participant_id: userId, + }, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Get playbook run by name + * @param {string} baseUrl - the base server URL + * @param {string} teamId - the team ID + * @param {string} name - the playbook run name to search for + * @return {Object} returns {items: PlaybookRun[]} on success or {error, status} on error + */ +export const apiGetPlaybookRunByName = async (baseUrl: string, teamId: string, name: string): Promise => { + try { + const response = await client.get( + `${baseUrl}${playbookRunsEndpoint}`, + { + params: { + team_id: teamId, + search_term: name, + }, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Get a playbook run by ID + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID + * @return {Object} returns {PlaybookRun} on success or {error, status} on error + */ +export const apiGetPlaybookRun = async (baseUrl: string, playbookRunId: string): Promise => { + try { + const response = await client.get( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}`, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Start a playbook run + * @param {string} baseUrl - the base server URL + * @param {Object} playbookRun - the playbook run to create + * @param {Object} options - additional options + * @return {Object} returns the created playbook run on success or {error, status} on error + */ +export const apiRunPlaybook = async ( + baseUrl: string, + playbookRun: PlaybookRun, + options: {expectedStatusCode?: number} = {}, +): Promise => { + try { + const response = await client.post( + `${baseUrl}${playbookRunsEndpoint}`, + playbookRun, + ); + + return response.data; + } catch (err) { + if (options.expectedStatusCode && (err as any).response && (err as any).response.status === options.expectedStatusCode) { + return (err as any).response.data; + } + return getResponseFromError(err); + } +}; + +/** + * Finish a playbook run + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID to finish + * @return {Object} returns the updated playbook run on success or {error, status} on error + */ +export const apiFinishRun = async (baseUrl: string, playbookRunId: string): Promise => { + try { + const response = await client.put( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}/finish`, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Update a playbook run's status + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID + * @param {string} message - the status message + * @param {number} reminder - reminder in seconds + * @return {Object} returns the updated status on success or {error, status} on error + */ +export const apiUpdateStatus = async ( + baseUrl: string, + playbookRunId: string, + message: string, + reminder: number = 300, +): Promise => { + try { + const response = await client.post( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}/status`, + { + message, + reminder, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Change the owner of a playbook run + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID + * @param {string} userId - the new owner user ID + * @return {Object} returns the updated playbook run on success or {error, status} on error + */ +export const apiChangePlaybookRunOwner = async (baseUrl: string, playbookRunId: string, userId: string): Promise => { + try { + const response = await client.post( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}/owner`, + { + owner_id: userId, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Change the assignee of a checklist item + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID + * @param {string} checklistId - the checklist ID + * @param {string} itemId - the checklist item ID + * @param {string} userId - the new assignee user ID + * @return {Object} returns the updated checklist item on success or {error, status} on error + */ +export const apiChangeChecklistItemAssignee = async ( + baseUrl: string, + playbookRunId: string, + checklistId: string, + itemId: string, + userId: string, +): Promise => { + try { + const response = await client.put( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}/checklists/${checklistId}/item/${itemId}/assignee`, + { + assignee_id: userId, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Set a checklist item state + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID + * @param {string} checklistId - the checklist ID + * @param {string} itemId - the checklist item ID + * @param {string} state - the new state ('' or 'closed') + * @return {Object} returns the updated checklist item on success or {error, status} on error + */ +export const apiSetChecklistItemState = async ( + baseUrl: string, + playbookRunId: string, + checklistId: string, + itemId: string, + state: string, +): Promise => { + try { + const response = await client.put( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}/checklists/${checklistId}/item/${itemId}/state`, + { + new_state: state, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Create a playbook + * @param {string} baseUrl - the base server URL + * @param {Object} playbook - the playbook to create + * @return {Object} returns the created playbook on success or {error, status} on error + */ +export const apiCreatePlaybook = async (baseUrl: String, playbook: Playbook): Promise => { + try { + const response = await client.post(`${baseUrl}${playbooksEndpoint}`, playbook); + return response.data; + } catch (err) { + throw new Error(`Failed to create playbook: ${err}`); + } +}; + +/** + * Create a test playbook with basic settings + * @param {string} baseUrl - the base server URL + * @param {Object} options - playbook options + * @return {Object} returns the created playbook on success or {error, status} on error + */ +export const apiCreateTestPlaybook = async ( + { + baseUrl, + teamId, + title, + userId, + broadcastEnabled, + broadcastChannelIds, + reminderMessageTemplate, + checklists, + inviteUsersEnabled, + reminderTimerDefaultSeconds = 24 * 60 * 60, // 24 hours + otherMembers = [], + invitedUserIds = [], + channelNameTemplate = '', + }: { + baseUrl: string; + teamId: string; + title: string; + userId: string; + broadcastEnabled?: boolean; + broadcastChannelIds?: string[]; + reminderMessageTemplate?: string; + checklists?: Checklist[]; + inviteUsersEnabled?: boolean; + reminderTimerDefaultSeconds?: number; + otherMembers?: string[]; + invitedUserIds?: string[]; + channelNameTemplate?: string; + }, +): Promise => { + const defaultChecklists: Checklist[] = [{ + title: 'Stage 1', + items: [ + {title: 'Step 1'}, + {title: 'Step 2'}, + ], + }]; + + return apiCreatePlaybook( + baseUrl, + { + team_id: teamId, + title, + checklists: checklists || defaultChecklists, + members: [ + {user_id: userId, roles: ['playbook_member', 'playbook_admin']}, + ...otherMembers.map((id) => ({user_id: id, roles: ['playbook_member', 'playbook_admin']})), + ], + broadcast_enabled: broadcastEnabled, + broadcast_channel_ids: broadcastChannelIds, + reminder_message_template: reminderMessageTemplate, + reminder_timer_default_seconds: reminderTimerDefaultSeconds, + invited_user_ids: invitedUserIds, + invite_users_enabled: inviteUsersEnabled, + channel_name_template: channelNameTemplate, + create_channel_member_on_new_participant: true, + }, + ); +}; + +/** + * Get a playbook by ID + * @param {string} baseUrl - the base server URL + * @param {string} playbookId - the playbook ID + * @return {Object} returns the playbook on success or {error, status} on error + */ +export const apiGetPlaybook = async (baseUrl: string, playbookId: string): Promise => { + try { + const response = await client.get( + `${baseUrl}${playbooksEndpoint}/${playbookId}`, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Update a playbook + * @param {string} baseUrl - the base server URL + * @param {Object} playbook - the playbook to update + * @param {number} expectedHttpCode - expected HTTP status code + * @return {Object} returns the updated playbook on success or {error, status} on error + */ +export const apiUpdatePlaybook = async ( + baseUrl: string, + playbook: Playbook, + expectedHttpCode: number = StatusOK, +): Promise => { + try { + const response = await client.put( + `${baseUrl}${playbooksEndpoint}/${playbook.id}`, + playbook, + ); + + return response.data; + } catch (err) { + if ((err as any).response && (err as any).response.status === expectedHttpCode) { + return (err as any).response.data; + } + return getResponseFromError(err); + } +}; + +/** + * Archive a playbook + * @param {string} baseUrl - the base server URL + * @param {string} playbookId - the playbook ID to archive + * @return {Object} returns {status} on success or {error, status} on error + */ +export const apiArchivePlaybook = async (baseUrl: string, playbookId: string): Promise => { + try { + const response = await client.delete( + `${baseUrl}${playbooksEndpoint}/${playbookId}`, + ); + + return {status: response.status}; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Follow a playbook run + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID to follow + * @return {Object} returns the response on success or {error, status} on error + */ +export const apiFollowPlaybookRun = async (baseUrl: string, playbookRunId: string): Promise => { + try { + const response = await client.put( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}/followers`, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Unfollow a playbook run + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID to unfollow + * @return {Object} returns the response on success or {error, status} on error + */ +export const apiUnfollowPlaybookRun = async (baseUrl: string, playbookRunId: string): Promise => { + try { + const response = await client.delete( + `${baseUrl}${playbookRunsEndpoint}/${playbookRunId}/followers`, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Add users to a playbook run + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID + * @param {string[]} userIds - array of user IDs to add + * @return {Object} returns the response on success or {error, status} on error + */ +export const apiAddUsersToRun = async (baseUrl: string, playbookRunId: string, userIds: string[]): Promise => { + try { + const query = ` + mutation AddRunParticipants($runID: String!, $userIDs: [String!]!) { + addRunParticipants(runID: $runID, userIDs: $userIDs) + } + `; + const variables = { + runID: playbookRunId, + userIDs: userIds, + }; + + const response = await client.post( + `${baseUrl}/plugins/playbooks/api/v0/query`, + { + query, + operationName: 'AddRunParticipants', + variables, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Update a playbook run + * @param {string} baseUrl - the base server URL + * @param {string} playbookRunId - the playbook run ID + * @param {Object} updates - the updates to apply + * @return {Object} returns the response on success or {error, status} on error + */ +export const apiUpdateRun = async (baseUrl: string, playbookRunId: string, updates: any): Promise => { + try { + const query = ` + mutation UpdateRun($id: String!, $updates: RunUpdates!) { + updateRun(id: $id, updates: $updates) + } + `; + const variables = { + id: playbookRunId, + updates, + }; + + const response = await client.post( + `${baseUrl}/plugins/playbooks/api/v0/query`, + { + query, + operationName: 'UpdateRun', + variables, + }, + ); + + return response.data; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Delete a playbook + * @param {string} baseUrl - the base server URL + * @param {string} playbookId - the playbook ID to delete + * @return {Object} returns {status} on success or {error, status} on error + */ +export const apiDeletePlaybook = async (baseUrl: string, playbookId: string): Promise => { + try { + const response = await client.delete( + `${baseUrl}${playbooksEndpoint}/${playbookId}`, + ); + return {status: response.status}; + } catch (err) { + return getResponseFromError(err); + } +}; + +export const Playbooks = { + apiEnablePlaybooksPlugin, + apiGetConfig, + apiGetAllPlaybookRuns, + apiGetAllInProgressPlaybookRuns, + apiGetPlaybookRunByName, + apiGetPlaybookRun, + apiRunPlaybook, + apiFinishRun, + apiUpdateStatus, + apiChangePlaybookRunOwner, + apiChangeChecklistItemAssignee, + apiSetChecklistItemState, + apiCreatePlaybook, + apiCreateTestPlaybook, + apiGetPlaybook, + apiUpdatePlaybook, + apiArchivePlaybook, + apiFollowPlaybookRun, + apiUnfollowPlaybookRun, + apiAddUsersToRun, + apiUpdateRun, + apiUpdateConfig, + apiDeletePlaybook, + apiEnablePlugin, + ...PlaybooksHelpers, +}; + +export default Playbooks; diff --git a/detox/e2e/support/server_api/playbooks_helpers.ts b/detox/e2e/support/server_api/playbooks_helpers.ts new file mode 100644 index 000000000..1137adc7e --- /dev/null +++ b/detox/e2e/support/server_api/playbooks_helpers.ts @@ -0,0 +1,144 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {getRandomId} from '@support/utils'; + +/** + * Generate a random checklist for a playbook + * @param {number} numChecklists - number of checklists to generate + * @param {number} numItems - number of items per checklist + * @return {Array} returns array of checklist objects + */ +export const generateRandomChecklists = (numChecklists = 1, numItems = 3) => { + const checklists = []; + + for (let i = 0; i < numChecklists; i++) { + const randomId = getRandomId(6); + const items = []; + + for (let j = 0; j < numItems; j++) { + items.push({ + title: `Task ${j + 1} for checklist ${i + 1} (${randomId})`, + description: `Description for task ${j + 1}`, + }); + } + + checklists.push({ + title: `Checklist ${i + 1} (${randomId})`, + items, + }); + } + + return checklists; +}; + +/** + * Generate a random playbook + * @param {Object} options - playbook options + * @return {Object} returns a playbook object + */ +interface PlaybookOptions { + teamId: string; + userId: string; + prefix?: string; + numChecklists?: number; + numItems?: number; + isPublic?: boolean; + broadcastEnabled?: boolean; + broadcastChannelIds?: string[]; + statusUpdateEnabled?: boolean; + retrospectiveEnabled?: boolean; + metricsEnabled?: boolean; + reminderTimerDefaultSeconds?: number; + channel_id?: string; +} + +export const generateRandomPlaybook = ({ + teamId, + userId, + prefix = 'playbook', + numChecklists = 1, + numItems = 3, + isPublic = true, + broadcastEnabled = false, + broadcastChannelIds = [], + statusUpdateEnabled = true, + retrospectiveEnabled = true, + metricsEnabled = false, + channel_id = '', +}: PlaybookOptions) => { + const randomId = getRandomId(6); + const title = `${prefix}-${randomId}`; + + return { + channel_id, + team_id: teamId, + title, + description: `Description for ${title}`, + checklists: generateRandomChecklists(numChecklists, numItems), + members: [ + {user_id: userId, roles: ['playbook_member', 'playbook_admin']}, + ], + public: isPublic, + broadcast_enabled: broadcastEnabled, + broadcast_channel_ids: broadcastChannelIds, + status_update_enabled: statusUpdateEnabled, + retrospective_enabled: retrospectiveEnabled, + reminder_timer_default_seconds: 3600, // 1 hour in seconds + metrics: metricsEnabled ? [ + { + title: 'Time to resolution', + description: 'Time from playbook run start to finish', + type: 'duration', + target: 60 * 60 * 24, // 1 day in seconds + }, + { + title: 'Customer impact', + description: 'Number of customers affected', + type: 'integer', + target: 0, + }, + ] : [], + }; +}; + +/** + * Generate a random playbook run + * @param {Object} options - playbook run options, must include teamId, playbookId, channelId, and ownerId + * @return {Object} returns a playbook run object + */ +interface PlaybookRunOptions { + teamId: string; + playbookId: string; + channelId: string; + ownerId: string; + prefix?: string; +} + +export const generateRandomPlaybookRun = ({ + teamId, + playbookId, + channelId, + ownerId, + prefix = 'run', +}: PlaybookRunOptions) => { + const randomId = getRandomId(6); + const name = `${prefix}-${randomId}`; + + return { + name, + owner_user_id: ownerId, + team_id: teamId, + playbook_id: playbookId, + channel_id: channelId, + description: `Description for ${name}`, + }; +}; + +export const PlaybooksHelpers = { + generateRandomChecklists, + generateRandomPlaybook, + generateRandomPlaybookRun, +}; + +export default PlaybooksHelpers; diff --git a/detox/e2e/support/utils/detoxhelpers.ts b/detox/e2e/support/utils/detoxhelpers.ts new file mode 100644 index 000000000..2421543ad --- /dev/null +++ b/detox/e2e/support/utils/detoxhelpers.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {by, element, waitFor} from 'detox'; + +export async function waitForPartialText(text: string, timeout = 10000): Promise { + // Detox does not support partial text matching directly; use exact match or implement a workaround. + await waitFor(element(by.text(new RegExp(text, 'i')))).toBeVisible().withTimeout(timeout); +} + +export async function expectPartialText(text: string, timeout = 10000): Promise { + // Detox does not support partial text matching directly; use exact match or implement a workaround. + await waitFor(element(by.text(new RegExp(text, 'i')))).toBeVisible().withTimeout(timeout); +} + +export async function scrollUntilVisible(testID: string, text: string): Promise { + // Detox does not support partial text matching directly; use exact match or implement a workaround. + await waitFor(element(by.text(new RegExp(text, 'i')))). + toBeVisible(). + whileElement(by.id(testID)). + scroll(100, 'down'); +} + +export async function waitForLoadingSpinner(testID: string, timeout = 10000): Promise { + await waitFor(element(by.id(testID))).not.toBeVisible().withTimeout(timeout); +} diff --git a/detox/e2e/support/utils/index.ts b/detox/e2e/support/utils/index.ts index 6f533bc7f..39d518102 100644 --- a/detox/e2e/support/utils/index.ts +++ b/detox/e2e/support/utils/index.ts @@ -5,6 +5,7 @@ import {adminEmail, adminPassword, adminUsername} from '@support/test_config'; import {v4 as uuidv4} from 'uuid'; export * from './email'; +export * from './detoxhelpers'; /** * Explicit `wait` should not normally used but made available for special cases. diff --git a/detox/e2e/test/account/about.e2e.ts b/detox/e2e/test/products/channels/account/about.e2e.ts similarity index 100% rename from detox/e2e/test/account/about.e2e.ts rename to detox/e2e/test/products/channels/account/about.e2e.ts diff --git a/detox/e2e/test/account/account_menu.e2e.ts b/detox/e2e/test/products/channels/account/account_menu.e2e.ts similarity index 100% rename from detox/e2e/test/account/account_menu.e2e.ts rename to detox/e2e/test/products/channels/account/account_menu.e2e.ts diff --git a/detox/e2e/test/account/advanced_settings.e2e.ts b/detox/e2e/test/products/channels/account/advanced_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/advanced_settings.e2e.ts rename to detox/e2e/test/products/channels/account/advanced_settings.e2e.ts diff --git a/detox/e2e/test/account/auto_responder_notification_settings.e2e.ts b/detox/e2e/test/products/channels/account/auto_responder_notification_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/auto_responder_notification_settings.e2e.ts rename to detox/e2e/test/products/channels/account/auto_responder_notification_settings.e2e.ts diff --git a/detox/e2e/test/account/clock_display_settings.e2e.ts b/detox/e2e/test/products/channels/account/clock_display_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/clock_display_settings.e2e.ts rename to detox/e2e/test/products/channels/account/clock_display_settings.e2e.ts diff --git a/detox/e2e/test/account/custom_status.e2e.ts b/detox/e2e/test/products/channels/account/custom_status.e2e.ts similarity index 100% rename from detox/e2e/test/account/custom_status.e2e.ts rename to detox/e2e/test/products/channels/account/custom_status.e2e.ts diff --git a/detox/e2e/test/account/display_settings.e2e.ts b/detox/e2e/test/products/channels/account/display_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/display_settings.e2e.ts rename to detox/e2e/test/products/channels/account/display_settings.e2e.ts diff --git a/detox/e2e/test/account/edit_profile.e2e.ts b/detox/e2e/test/products/channels/account/edit_profile.e2e.ts similarity index 100% rename from detox/e2e/test/account/edit_profile.e2e.ts rename to detox/e2e/test/products/channels/account/edit_profile.e2e.ts diff --git a/detox/e2e/test/account/email_notification_settings.e2e.ts b/detox/e2e/test/products/channels/account/email_notification_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/email_notification_settings.e2e.ts rename to detox/e2e/test/products/channels/account/email_notification_settings.e2e.ts diff --git a/detox/e2e/test/account/mention_notification_settings.e2e.ts b/detox/e2e/test/products/channels/account/mention_notification_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/mention_notification_settings.e2e.ts rename to detox/e2e/test/products/channels/account/mention_notification_settings.e2e.ts diff --git a/detox/e2e/test/account/notification_settings.e2e.ts b/detox/e2e/test/products/channels/account/notification_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/notification_settings.e2e.ts rename to detox/e2e/test/products/channels/account/notification_settings.e2e.ts diff --git a/detox/e2e/test/account/push_notification_settings.e2e.ts b/detox/e2e/test/products/channels/account/push_notification_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/push_notification_settings.e2e.ts rename to detox/e2e/test/products/channels/account/push_notification_settings.e2e.ts diff --git a/detox/e2e/test/account/settings.e2e.ts b/detox/e2e/test/products/channels/account/settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/settings.e2e.ts rename to detox/e2e/test/products/channels/account/settings.e2e.ts diff --git a/detox/e2e/test/account/theme_display_settings.e2e.ts b/detox/e2e/test/products/channels/account/theme_display_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/theme_display_settings.e2e.ts rename to detox/e2e/test/products/channels/account/theme_display_settings.e2e.ts diff --git a/detox/e2e/test/account/timezone_display_settings.e2e.ts b/detox/e2e/test/products/channels/account/timezone_display_settings.e2e.ts similarity index 100% rename from detox/e2e/test/account/timezone_display_settings.e2e.ts rename to detox/e2e/test/products/channels/account/timezone_display_settings.e2e.ts diff --git a/detox/e2e/test/autocomplete/at_mention.e2e.ts b/detox/e2e/test/products/channels/autocomplete/at_mention.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/at_mention.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/at_mention.e2e.ts diff --git a/detox/e2e/test/autocomplete/channel_mention.e2e.ts b/detox/e2e/test/products/channels/autocomplete/channel_mention.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/channel_mention.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/channel_mention.e2e.ts diff --git a/detox/e2e/test/autocomplete/channel_post_draft.e2e.ts b/detox/e2e/test/products/channels/autocomplete/channel_post_draft.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/channel_post_draft.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/channel_post_draft.e2e.ts diff --git a/detox/e2e/test/autocomplete/create_channel.e2e.ts b/detox/e2e/test/products/channels/autocomplete/create_channel.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/create_channel.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/create_channel.e2e.ts diff --git a/detox/e2e/test/autocomplete/edit_channel.e2e.ts b/detox/e2e/test/products/channels/autocomplete/edit_channel.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/edit_channel.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/edit_channel.e2e.ts diff --git a/detox/e2e/test/autocomplete/edit_channel_header.e2e.ts b/detox/e2e/test/products/channels/autocomplete/edit_channel_header.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/edit_channel_header.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/edit_channel_header.e2e.ts diff --git a/detox/e2e/test/autocomplete/edit_post.e2e.ts b/detox/e2e/test/products/channels/autocomplete/edit_post.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/edit_post.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/edit_post.e2e.ts diff --git a/detox/e2e/test/autocomplete/emoji_suggestion.e2e.ts b/detox/e2e/test/products/channels/autocomplete/emoji_suggestion.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/emoji_suggestion.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/emoji_suggestion.e2e.ts diff --git a/detox/e2e/test/autocomplete/slash_suggestion.e2e.ts b/detox/e2e/test/products/channels/autocomplete/slash_suggestion.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/slash_suggestion.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/slash_suggestion.e2e.ts diff --git a/detox/e2e/test/autocomplete/thread_post_draft.e2e.ts b/detox/e2e/test/products/channels/autocomplete/thread_post_draft.e2e.ts similarity index 100% rename from detox/e2e/test/autocomplete/thread_post_draft.e2e.ts rename to detox/e2e/test/products/channels/autocomplete/thread_post_draft.e2e.ts diff --git a/detox/e2e/test/channels/archive_channel.e2e.ts b/detox/e2e/test/products/channels/channels/archive_channel.e2e.ts similarity index 100% rename from detox/e2e/test/channels/archive_channel.e2e.ts rename to detox/e2e/test/products/channels/channels/archive_channel.e2e.ts diff --git a/detox/e2e/test/channels/browse_channels.e2e.ts b/detox/e2e/test/products/channels/channels/browse_channels.e2e.ts similarity index 100% rename from detox/e2e/test/channels/browse_channels.e2e.ts rename to detox/e2e/test/products/channels/channels/browse_channels.e2e.ts diff --git a/detox/e2e/test/channels/channel_info.e2e.ts b/detox/e2e/test/products/channels/channels/channel_info.e2e.ts similarity index 100% rename from detox/e2e/test/channels/channel_info.e2e.ts rename to detox/e2e/test/products/channels/channels/channel_info.e2e.ts diff --git a/detox/e2e/test/channels/channel_list.e2e.ts b/detox/e2e/test/products/channels/channels/channel_list.e2e.ts similarity index 100% rename from detox/e2e/test/channels/channel_list.e2e.ts rename to detox/e2e/test/products/channels/channels/channel_list.e2e.ts diff --git a/detox/e2e/test/channels/channel_post_list.e2e.ts b/detox/e2e/test/products/channels/channels/channel_post_list.e2e.ts similarity index 100% rename from detox/e2e/test/channels/channel_post_list.e2e.ts rename to detox/e2e/test/products/channels/channels/channel_post_list.e2e.ts diff --git a/detox/e2e/test/channels/convert_to_private_channel.e2e.ts b/detox/e2e/test/products/channels/channels/convert_to_private_channel.e2e.ts similarity index 100% rename from detox/e2e/test/channels/convert_to_private_channel.e2e.ts rename to detox/e2e/test/products/channels/channels/convert_to_private_channel.e2e.ts diff --git a/detox/e2e/test/channels/create_channel_and_edit_channel_header.e2e.ts b/detox/e2e/test/products/channels/channels/create_channel_and_edit_channel_header.e2e.ts similarity index 100% rename from detox/e2e/test/channels/create_channel_and_edit_channel_header.e2e.ts rename to detox/e2e/test/products/channels/channels/create_channel_and_edit_channel_header.e2e.ts diff --git a/detox/e2e/test/channels/create_direct_message.e2e.ts b/detox/e2e/test/products/channels/channels/create_direct_message.e2e.ts similarity index 100% rename from detox/e2e/test/channels/create_direct_message.e2e.ts rename to detox/e2e/test/products/channels/channels/create_direct_message.e2e.ts diff --git a/detox/e2e/test/channels/edit_channel.e2e.ts b/detox/e2e/test/products/channels/channels/edit_channel.e2e.ts similarity index 100% rename from detox/e2e/test/channels/edit_channel.e2e.ts rename to detox/e2e/test/products/channels/channels/edit_channel.e2e.ts diff --git a/detox/e2e/test/channels/favorite_and_unfavorite_channel.e2e.ts b/detox/e2e/test/products/channels/channels/favorite_and_unfavorite_channel.e2e.ts similarity index 100% rename from detox/e2e/test/channels/favorite_and_unfavorite_channel.e2e.ts rename to detox/e2e/test/products/channels/channels/favorite_and_unfavorite_channel.e2e.ts diff --git a/detox/e2e/test/channels/find_channels.e2e.ts b/detox/e2e/test/products/channels/channels/find_channels.e2e.ts similarity index 100% rename from detox/e2e/test/channels/find_channels.e2e.ts rename to detox/e2e/test/products/channels/channels/find_channels.e2e.ts diff --git a/detox/e2e/test/channels/leave_channel.e2e.ts b/detox/e2e/test/products/channels/channels/leave_channel.e2e.ts similarity index 100% rename from detox/e2e/test/channels/leave_channel.e2e.ts rename to detox/e2e/test/products/channels/channels/leave_channel.e2e.ts diff --git a/detox/e2e/test/channels/mute_and_unmute_channel.e2e.ts b/detox/e2e/test/products/channels/channels/mute_and_unmute_channel.e2e.ts similarity index 100% rename from detox/e2e/test/channels/mute_and_unmute_channel.e2e.ts rename to detox/e2e/test/products/channels/channels/mute_and_unmute_channel.e2e.ts diff --git a/detox/e2e/test/channels/unarchive_channel.e2e.ts b/detox/e2e/test/products/channels/channels/unarchive_channel.e2e.ts similarity index 100% rename from detox/e2e/test/channels/unarchive_channel.e2e.ts rename to detox/e2e/test/products/channels/channels/unarchive_channel.e2e.ts diff --git a/detox/e2e/test/messaging/at_mention.e2e.ts b/detox/e2e/test/products/channels/messaging/at_mention.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/at_mention.e2e.ts rename to detox/e2e/test/products/channels/messaging/at_mention.e2e.ts diff --git a/detox/e2e/test/messaging/channel_link.e2e.ts b/detox/e2e/test/products/channels/messaging/channel_link.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/channel_link.e2e.ts rename to detox/e2e/test/products/channels/messaging/channel_link.e2e.ts diff --git a/detox/e2e/test/messaging/channel_mention.e2e.ts b/detox/e2e/test/products/channels/messaging/channel_mention.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/channel_mention.e2e.ts rename to detox/e2e/test/products/channels/messaging/channel_mention.e2e.ts diff --git a/detox/e2e/test/messaging/emojis_and_reactions.e2e.ts b/detox/e2e/test/products/channels/messaging/emojis_and_reactions.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/emojis_and_reactions.e2e.ts rename to detox/e2e/test/products/channels/messaging/emojis_and_reactions.e2e.ts diff --git a/detox/e2e/test/messaging/follow_and_unfollow_messsage.e2e.ts b/detox/e2e/test/products/channels/messaging/follow_and_unfollow_messsage.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/follow_and_unfollow_messsage.e2e.ts rename to detox/e2e/test/products/channels/messaging/follow_and_unfollow_messsage.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_block_quote.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_block_quote.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_block_quote.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_block_quote.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_code.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_code.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_code.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_code.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_heading.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_heading.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_heading.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_heading.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_image.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_image.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_image.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_image.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_latex.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_latex.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_latex.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_latex.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_list.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_list.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_list.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_list.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_separator.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_separator.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_separator.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_separator.e2e.ts diff --git a/detox/e2e/test/messaging/markdown_table.e2e.ts b/detox/e2e/test/products/channels/messaging/markdown_table.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/markdown_table.e2e.ts rename to detox/e2e/test/products/channels/messaging/markdown_table.e2e.ts diff --git a/detox/e2e/test/messaging/message_delete.e2e.ts b/detox/e2e/test/products/channels/messaging/message_delete.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/message_delete.e2e.ts rename to detox/e2e/test/products/channels/messaging/message_delete.e2e.ts diff --git a/detox/e2e/test/messaging/message_draft.e2e.ts b/detox/e2e/test/products/channels/messaging/message_draft.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/message_draft.e2e.ts rename to detox/e2e/test/products/channels/messaging/message_draft.e2e.ts diff --git a/detox/e2e/test/messaging/message_edit.e2e.ts b/detox/e2e/test/products/channels/messaging/message_edit.e2e.ts similarity index 99% rename from detox/e2e/test/messaging/message_edit.e2e.ts rename to detox/e2e/test/products/channels/messaging/message_edit.e2e.ts index 61cc07a41..e509c3974 100644 --- a/detox/e2e/test/messaging/message_edit.e2e.ts +++ b/detox/e2e/test/products/channels/messaging/message_edit.e2e.ts @@ -78,7 +78,7 @@ describe('Messaging - Message Edit', () => { // * Verify post message is updated and displays edited indicator '(edited)' const {postListPostItem: updatedPostListPostItem, postListPostItemEditedIndicator} = ChannelScreen.getPostListPostItem(post.id, updatedMessage); await expect(updatedPostListPostItem).toBeVisible(); - await expect(postListPostItemEditedIndicator).toHaveText('(edited)'); + await expect(postListPostItemEditedIndicator).toHaveText('Edited'); // # Go back to channel list screen await ChannelScreen.back(); @@ -144,7 +144,7 @@ describe('Messaging - Message Edit', () => { // * Verify reply post message is updated and displays edited indicator '(edited)' const {postListPostItem: updatedReplyPostListPostItem, postListPostItemEditedIndicator} = ThreadScreen.getPostListPostItem(replyPost.id, updatedReplyMessage); await expect(updatedReplyPostListPostItem).toBeVisible(); - await expect(postListPostItemEditedIndicator).toHaveText('(edited)'); + await expect(postListPostItemEditedIndicator).toHaveText('Edited'); // # Go back to channel list screen await ThreadScreen.back(); diff --git a/detox/e2e/test/messaging/message_local_drafts.e2e.ts b/detox/e2e/test/products/channels/messaging/message_local_drafts.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/message_local_drafts.e2e.ts rename to detox/e2e/test/products/channels/messaging/message_local_drafts.e2e.ts diff --git a/detox/e2e/test/messaging/message_post.e2e.ts b/detox/e2e/test/products/channels/messaging/message_post.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/message_post.e2e.ts rename to detox/e2e/test/products/channels/messaging/message_post.e2e.ts diff --git a/detox/e2e/test/messaging/message_reply.e2e.ts b/detox/e2e/test/products/channels/messaging/message_reply.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/message_reply.e2e.ts rename to detox/e2e/test/products/channels/messaging/message_reply.e2e.ts diff --git a/detox/e2e/test/messaging/permalink.e2e.ts b/detox/e2e/test/products/channels/messaging/permalink.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/permalink.e2e.ts rename to detox/e2e/test/products/channels/messaging/permalink.e2e.ts diff --git a/detox/e2e/test/messaging/pin_and_unpin_message.e2e.ts b/detox/e2e/test/products/channels/messaging/pin_and_unpin_message.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/pin_and_unpin_message.e2e.ts rename to detox/e2e/test/products/channels/messaging/pin_and_unpin_message.e2e.ts diff --git a/detox/e2e/test/messaging/save_and_unsave_message.e2e.ts b/detox/e2e/test/products/channels/messaging/save_and_unsave_message.e2e.ts similarity index 100% rename from detox/e2e/test/messaging/save_and_unsave_message.e2e.ts rename to detox/e2e/test/products/channels/messaging/save_and_unsave_message.e2e.ts diff --git a/detox/e2e/test/scheduled_messages/create_schedule_message.e2e.ts b/detox/e2e/test/products/channels/scheduled_messages/create_schedule_message.e2e.ts similarity index 100% rename from detox/e2e/test/scheduled_messages/create_schedule_message.e2e.ts rename to detox/e2e/test/products/channels/scheduled_messages/create_schedule_message.e2e.ts diff --git a/detox/e2e/test/search/pinned_messages.e2e.ts b/detox/e2e/test/products/channels/search/pinned_messages.e2e.ts similarity index 99% rename from detox/e2e/test/search/pinned_messages.e2e.ts rename to detox/e2e/test/products/channels/search/pinned_messages.e2e.ts index b43b03f1d..6b50a240b 100644 --- a/detox/e2e/test/search/pinned_messages.e2e.ts +++ b/detox/e2e/test/products/channels/search/pinned_messages.e2e.ts @@ -138,7 +138,7 @@ describe('Search - Pinned Messages', () => { // * Verify post message is updated and displays edited indicator '(edited)' const {postListPostItem: updatedPostListPostItem, postListPostItemEditedIndicator} = PinnedMessagesScreen.getPostListPostItem(pinnedPost.id, updatedMessage); await expect(updatedPostListPostItem).toBeVisible(); - await expect(postListPostItemEditedIndicator).toHaveText('(edited)'); + await expect(postListPostItemEditedIndicator).toHaveText('Edited'); // # Open post options for updated pinned message and tap on reply option await PinnedMessagesScreen.openPostOptionsFor(pinnedPost.id, updatedMessage); diff --git a/detox/e2e/test/search/recent_mentions.e2e.ts b/detox/e2e/test/products/channels/search/recent_mentions.e2e.ts similarity index 99% rename from detox/e2e/test/search/recent_mentions.e2e.ts rename to detox/e2e/test/products/channels/search/recent_mentions.e2e.ts index 1b9dfdd88..fb5e95ecb 100644 --- a/detox/e2e/test/search/recent_mentions.e2e.ts +++ b/detox/e2e/test/products/channels/search/recent_mentions.e2e.ts @@ -137,7 +137,7 @@ describe('Search - Recent Mentions', () => { // * Verify post message is updated and displays edited indicator '(edited)' const {postListPostItem: updatedPostListPostItem, postListPostItemEditedIndicator} = RecentMentionsScreen.getPostListPostItem(mentionPost.id, updatedMessage); await expect(updatedPostListPostItem).toBeVisible(); - await expect(postListPostItemEditedIndicator).toHaveText('(edited)'); + await expect(postListPostItemEditedIndicator).toHaveText('Edited'); // # Open post options for recent mention and tap on reply option await RecentMentionsScreen.openPostOptionsFor(mentionPost.id, updatedMessage); diff --git a/detox/e2e/test/search/saved_messages.e2e.ts b/detox/e2e/test/products/channels/search/saved_messages.e2e.ts similarity index 99% rename from detox/e2e/test/search/saved_messages.e2e.ts rename to detox/e2e/test/products/channels/search/saved_messages.e2e.ts index c586f5195..a11f1cee1 100644 --- a/detox/e2e/test/search/saved_messages.e2e.ts +++ b/detox/e2e/test/products/channels/search/saved_messages.e2e.ts @@ -140,7 +140,7 @@ describe('Search - Saved Messages', () => { // * Verify post message is updated and displays edited indicator '(edited)' const {postListPostItem: updatedPostListPostItem, postListPostItemEditedIndicator} = SavedMessagesScreen.getPostListPostItem(savedPost.id, updatedMessage); await expect(updatedPostListPostItem).toBeVisible(); - await expect(postListPostItemEditedIndicator).toHaveText('(edited)'); + await expect(postListPostItemEditedIndicator).toHaveText('Edited'); // # Open post options for updated saved message and tap on reply option await SavedMessagesScreen.openPostOptionsFor(savedPost.id, updatedMessage); diff --git a/detox/e2e/test/search/search_messages.e2e.ts b/detox/e2e/test/products/channels/search/search_messages.e2e.ts similarity index 99% rename from detox/e2e/test/search/search_messages.e2e.ts rename to detox/e2e/test/products/channels/search/search_messages.e2e.ts index ed783123f..9e6179cb0 100644 --- a/detox/e2e/test/search/search_messages.e2e.ts +++ b/detox/e2e/test/products/channels/search/search_messages.e2e.ts @@ -393,7 +393,7 @@ describe('Search - Search Messages', () => { // * Verify post message is updated and displays edited indicator '(edited)' const {postListPostItem: updatedPostListPostItem, postListPostItemEditedIndicator} = SearchMessagesScreen.getPostListPostItem(searchedPost.id, updatedMessage); await expect(updatedPostListPostItem).toBeVisible(); - await expect(postListPostItemEditedIndicator).toHaveText('(edited)'); + await expect(postListPostItemEditedIndicator).toHaveText('Edited'); // # Open post options for searched message and tap on reply option await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, updatedMessage); diff --git a/detox/e2e/test/server_login/connect_to_server.e2e.ts b/detox/e2e/test/products/channels/server_login/connect_to_server.e2e.ts similarity index 100% rename from detox/e2e/test/server_login/connect_to_server.e2e.ts rename to detox/e2e/test/products/channels/server_login/connect_to_server.e2e.ts diff --git a/detox/e2e/test/server_login/login_by_email.e2e.ts b/detox/e2e/test/products/channels/server_login/login_by_email.e2e.ts similarity index 100% rename from detox/e2e/test/server_login/login_by_email.e2e.ts rename to detox/e2e/test/products/channels/server_login/login_by_email.e2e.ts diff --git a/detox/e2e/test/server_login/preauth_secret_connection.e2e.ts b/detox/e2e/test/products/channels/server_login/preauth_secret_connection.e2e.ts similarity index 100% rename from detox/e2e/test/server_login/preauth_secret_connection.e2e.ts rename to detox/e2e/test/products/channels/server_login/preauth_secret_connection.e2e.ts diff --git a/detox/e2e/test/server_login/server_list.e2e.ts b/detox/e2e/test/products/channels/server_login/server_list.e2e.ts similarity index 100% rename from detox/e2e/test/server_login/server_list.e2e.ts rename to detox/e2e/test/products/channels/server_login/server_list.e2e.ts diff --git a/detox/e2e/test/smoke_test/account.e2e.ts b/detox/e2e/test/products/channels/smoke_test/account.e2e.ts similarity index 100% rename from detox/e2e/test/smoke_test/account.e2e.ts rename to detox/e2e/test/products/channels/smoke_test/account.e2e.ts diff --git a/detox/e2e/test/smoke_test/autocomplete.e2e.ts b/detox/e2e/test/products/channels/smoke_test/autocomplete.e2e.ts similarity index 100% rename from detox/e2e/test/smoke_test/autocomplete.e2e.ts rename to detox/e2e/test/products/channels/smoke_test/autocomplete.e2e.ts diff --git a/detox/e2e/test/smoke_test/channels.e2e.ts b/detox/e2e/test/products/channels/smoke_test/channels.e2e.ts similarity index 100% rename from detox/e2e/test/smoke_test/channels.e2e.ts rename to detox/e2e/test/products/channels/smoke_test/channels.e2e.ts diff --git a/detox/e2e/test/smoke_test/messaging.e2e.ts b/detox/e2e/test/products/channels/smoke_test/messaging.e2e.ts similarity index 99% rename from detox/e2e/test/smoke_test/messaging.e2e.ts rename to detox/e2e/test/products/channels/smoke_test/messaging.e2e.ts index 1b99ee6d3..b04366794 100644 --- a/detox/e2e/test/smoke_test/messaging.e2e.ts +++ b/detox/e2e/test/products/channels/smoke_test/messaging.e2e.ts @@ -86,7 +86,7 @@ describe('Smoke Test - Messaging', () => { // * Verify post message is updated and displays edited indicator '(edited)' const {postListPostItem: updatedPostListPostItem, postListPostItemEditedIndicator} = ChannelScreen.getPostListPostItem(post.id, updatedMessage); await expect(updatedPostListPostItem).toBeVisible(); - await expect(postListPostItemEditedIndicator).toHaveText('(edited)'); + await expect(postListPostItemEditedIndicator).toHaveText('Edited'); // # Open post options for the updated message, tap delete option and confirm await ChannelScreen.openPostOptionsFor(post.id, updatedMessage); diff --git a/detox/e2e/test/smoke_test/search.e2e.ts b/detox/e2e/test/products/channels/smoke_test/search.e2e.ts similarity index 100% rename from detox/e2e/test/smoke_test/search.e2e.ts rename to detox/e2e/test/products/channels/smoke_test/search.e2e.ts diff --git a/detox/e2e/test/smoke_test/server_login.e2e.ts b/detox/e2e/test/products/channels/smoke_test/server_login.e2e.ts similarity index 100% rename from detox/e2e/test/smoke_test/server_login.e2e.ts rename to detox/e2e/test/products/channels/smoke_test/server_login.e2e.ts diff --git a/detox/e2e/test/smoke_test/threads.e2e.ts b/detox/e2e/test/products/channels/smoke_test/threads.e2e.ts similarity index 100% rename from detox/e2e/test/smoke_test/threads.e2e.ts rename to detox/e2e/test/products/channels/smoke_test/threads.e2e.ts diff --git a/detox/e2e/test/teams/invite_people.e2e.ts b/detox/e2e/test/products/channels/teams/invite_people.e2e.ts similarity index 100% rename from detox/e2e/test/teams/invite_people.e2e.ts rename to detox/e2e/test/products/channels/teams/invite_people.e2e.ts diff --git a/detox/e2e/test/threads/follow_and_unfollow_thread.e2e.ts b/detox/e2e/test/products/channels/threads/follow_and_unfollow_thread.e2e.ts similarity index 100% rename from detox/e2e/test/threads/follow_and_unfollow_thread.e2e.ts rename to detox/e2e/test/products/channels/threads/follow_and_unfollow_thread.e2e.ts diff --git a/detox/e2e/test/threads/global_threads.e2e.ts b/detox/e2e/test/products/channels/threads/global_threads.e2e.ts similarity index 100% rename from detox/e2e/test/threads/global_threads.e2e.ts rename to detox/e2e/test/products/channels/threads/global_threads.e2e.ts diff --git a/detox/e2e/test/threads/mark_thread_as_read_and_unread.e2e.ts b/detox/e2e/test/products/channels/threads/mark_thread_as_read_and_unread.e2e.ts similarity index 100% rename from detox/e2e/test/threads/mark_thread_as_read_and_unread.e2e.ts rename to detox/e2e/test/products/channels/threads/mark_thread_as_read_and_unread.e2e.ts diff --git a/detox/e2e/test/threads/open_thread_in_channel.e2e.ts b/detox/e2e/test/products/channels/threads/open_thread_in_channel.e2e.ts similarity index 100% rename from detox/e2e/test/threads/open_thread_in_channel.e2e.ts rename to detox/e2e/test/products/channels/threads/open_thread_in_channel.e2e.ts diff --git a/detox/e2e/test/threads/reply_to_thread.e2e.ts b/detox/e2e/test/products/channels/threads/reply_to_thread.e2e.ts similarity index 100% rename from detox/e2e/test/threads/reply_to_thread.e2e.ts rename to detox/e2e/test/products/channels/threads/reply_to_thread.e2e.ts diff --git a/detox/e2e/test/threads/save_and_unsave_thread.e2e.ts b/detox/e2e/test/products/channels/threads/save_and_unsave_thread.e2e.ts similarity index 100% rename from detox/e2e/test/threads/save_and_unsave_thread.e2e.ts rename to detox/e2e/test/products/channels/threads/save_and_unsave_thread.e2e.ts diff --git a/detox/e2e/test/products/playbooks/__pilot_cache__/playbooks.e2e.json b/detox/e2e/test/products/playbooks/__pilot_cache__/playbooks.e2e.json new file mode 100644 index 000000000..ea3ef4cff --- /dev/null +++ b/detox/e2e/test/products/playbooks/__pilot_cache__/playbooks.e2e.json @@ -0,0 +1,497 @@ +{ + "{\"currentStep\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"previousSteps\":[]}": [ + { + "value": { + "code": "// Wait for and verify the Playbook icon is visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Wait for and verify the count \"1\" is visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\n .toBeVisible()\n .withTimeout(10000);" + }, + "creationTime": 1753348699909 + } + ], + "{\"currentStep\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"previousSteps\":[{\"step\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"code\":\"// Wait for and verify the Playbook icon is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Wait for and verify the count \\\"1\\\" is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\\n .toBeVisible()\\n .withTimeout(10000);\"}]}": [ + { + "value": { + "code": "await element(by.id('channel_header.channel_quick_actions.button')).tap();" + }, + "creationTime": 1753348715407 + } + ], + "{\"currentStep\":\"Verify a bottom sheet with the \\\"Playbook runs\\\" option is displayed\",\"previousSteps\":[{\"step\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"code\":\"// Wait for and verify the Playbook icon is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Wait for and verify the count \\\"1\\\" is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\\n .toBeVisible()\\n .withTimeout(10000);\"},{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"}]}": [ + { + "value": { + "code": "// Wait for and verify the bottom sheet is visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the \"Playbook runs\" option is displayed\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the \"Playbook runs\" text is visible\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');" + }, + "creationTime": 1753348726207 + } + ], + "{\"currentStep\":\"Verify a bottom sheet with the \\\"Playbook runs\\\" option is displayed\",\"previousSteps\":[{\"step\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"code\":\"// Wait for and verify the Playbook icon is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Wait for and verify the count \\\"1\\\" is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\\n .toBeVisible()\\n .withTimeout(10000);\"},{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Verify a bottom sheet with the \\\"Playbook runs\\\" option is displayed\",\"code\":\"// Wait for and verify the bottom sheet is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" option is displayed\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is visible\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\",\"error\":\"Test Failed: 10.0sec timeout expired without matching of given matcher: (view has effective visibility and view.getGlobalVisibleRect() covers at least <75> percent of the view's area)\"}]}": [ + { + "value": { + "code": "// Wait for the bottom sheet to be visible\nawait waitFor(element(by.id('undefined.screen')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the \"Playbook runs\" option is visible by checking its container\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the \"Playbook runs\" text is displayed\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');" + }, + "creationTime": 1753348755396 + } + ], + "{\"currentStep\":\"Verify that \\\"Playbook runs\\\" option is listed in the bottom sheet\",\"previousSteps\":[{\"step\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"code\":\"// Wait for and verify the Playbook icon is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Wait for and verify the count \\\"1\\\" is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\\n .toBeVisible()\\n .withTimeout(10000);\"},{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Verify a bottom sheet with the \\\"Playbook runs\\\" option is displayed\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.id('undefined.screen')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" option is visible by checking its container\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is displayed\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\"}]}": [ + { + "value": { + "code": "// Wait for the bottom sheet to be visible\nawait waitFor(element(by.id('undefined.screen')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the \"Playbook runs\" text is visible in the bottom sheet\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the text content is \"Playbook runs\"\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');" + }, + "creationTime": 1753348766253 + } + ], + "{\"currentStep\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"previousSteps\":[{\"step\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"code\":\"// Wait for and verify the Playbook icon is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Wait for and verify the count \\\"1\\\" is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\\n .toBeVisible()\\n .withTimeout(10000);\"},{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Verify a bottom sheet with the \\\"Playbook runs\\\" option is displayed\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.id('undefined.screen')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" option is visible by checking its container\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is displayed\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\"},{\"step\":\"Verify that \\\"Playbook runs\\\" option is listed in the bottom sheet\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.id('undefined.screen')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is visible in the bottom sheet\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the text content is \\\"Playbook runs\\\"\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\"}]}": [ + { + "value": { + "code": "await element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')).tap();" + }, + "creationTime": 1753348788363 + } + ], + "{\"currentStep\":\"Verify the \\\"Playbook runs\\\" screen opens\",\"previousSteps\":[{\"step\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"code\":\"// Wait for and verify the Playbook icon is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Wait for and verify the count \\\"1\\\" is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\\n .toBeVisible()\\n .withTimeout(10000);\"},{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Verify a bottom sheet with the \\\"Playbook runs\\\" option is displayed\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.id('undefined.screen')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" option is visible by checking its container\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is displayed\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\"},{\"step\":\"Verify that \\\"Playbook runs\\\" option is listed in the bottom sheet\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.id('undefined.screen')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is visible in the bottom sheet\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the text content is \\\"Playbook runs\\\"\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')).tap();\"}]}": [ + { + "value": { + "code": "// Wait for and verify the \"Playbook runs\" title is visible in the top bar\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_1_0_0_0_1_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the title text is \"Playbook runs\"\nawait expect(element(by.id('detox_temp_0_1_0_1_0_0_1_0_0_0_1_0'))).toHaveText('Playbook runs');\n\n// Verify the subtitle showing the channel name is visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_1_0_0_0_1_1')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the tabs are visible (In Progress and Finished)\nawait waitFor(element(by.id('tabs.in-progress.button')))\n .toBeVisible()\n .withTimeout(10000);\n\nawait waitFor(element(by.id('tabs.finished.button')))\n .toBeVisible()\n .withTimeout(10000);" + }, + "creationTime": 1753348810060 + } + ], + "{\"currentStep\":\"Verify the \\\"In Progress\\\" tab is selected\",\"previousSteps\":[{\"step\":\"Verify the Playbook icon and count \\\"1\\\" are visible in the Channel Header\",\"code\":\"// Wait for and verify the Playbook icon is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Wait for and verify the count \\\"1\\\" is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_0_0_0_2_0_1')))\\n .toBeVisible()\\n .withTimeout(10000);\"},{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Verify a bottom sheet with the \\\"Playbook runs\\\" option is displayed\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.id('undefined.screen')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" option is visible by checking its container\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is displayed\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\"},{\"step\":\"Verify that \\\"Playbook runs\\\" option is listed in the bottom sheet\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.id('undefined.screen')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the \\\"Playbook runs\\\" text is visible in the bottom sheet\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the text content is \\\"Playbook runs\\\"\\nawait expect(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5_1'))).toHaveText('Playbook runs');\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_5')).tap();\"},{\"step\":\"Verify the \\\"Playbook runs\\\" screen opens\",\"code\":\"// Wait for and verify the \\\"Playbook runs\\\" title is visible in the top bar\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_1_0_0_0_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the title text is \\\"Playbook runs\\\"\\nawait expect(element(by.id('detox_temp_0_1_0_1_0_0_1_0_0_0_1_0'))).toHaveText('Playbook runs');\\n\\n// Verify the subtitle showing the channel name is visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_1_0_0_0_1_1')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the tabs are visible (In Progress and Finished)\\nawait waitFor(element(by.id('tabs.in-progress.button')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\nawait waitFor(element(by.id('tabs.finished.button')))\\n .toBeVisible()\\n .withTimeout(10000);\"}]}": [ + { + "value": { + "code": "// Wait for the \"In Progress\" tab to be visible\nawait waitFor(element(by.id('tabs.in-progress.button')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the \"In Progress\" tab is selected by checking if it has the selected visual state\n// Since there's no explicit \"selected\" attribute in the hierarchy, we verify it's the active tab\n// by confirming it's visible and tappable (which indicates it's the current tab)\nawait expect(element(by.id('tabs.in-progress.button'))).toBeVisible();\n\n// Additionally verify the tab contains the \"In Progress\" text\nawait expect(element(by.text('In Progress'))).toBeVisible();" + }, + "creationTime": 1753348819704 + } + ], + "{\"currentStep\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"previousSteps\":[]}": [ + { + "value": { + "code": "await element(by.id('channel_header.channel_quick_actions.button')).tap();" + }, + "creationTime": 1753349012436 + } + ], + "{\"currentStep\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"}]}": [ + { + "value": { + "code": "await element(by.text(/Playbook runs/)).tap();" + }, + "creationTime": 1753349018677 + } + ], + "{\"currentStep\":\"Switch to the \\\"Finished\\\" tab\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"}]}": [ + { + "value": { + "code": "await element(by.id('tabs.finished.button')).tap();" + }, + "creationTime": 1753349029420 + } + ], + "{\"currentStep\":\"Verify multiple finished playbook runs are listed\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Switch to the \\\"Finished\\\" tab\",\"code\":\"await element(by.id('tabs.finished.button')).tap();\"}]}": [ + { + "value": { + "code": "// Wait for the scroll view to be visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Get all the run items that contain the \"finished-run-\" text pattern\nconst runElements = await element(by.text(/finished-run-\\d+/)).getAttributes();\n\n// Verify that runElements is an array (multiple elements found)\nif (!Array.isArray(runElements)) {\n throw new Error(\"Expected multiple finished playbook runs but found only one or none\");\n}\n\n// Verify we have more than one finished run\njestExpect(runElements.length).toBeGreaterThan(1);" + }, + "creationTime": 1753349039873 + } + ], + "{\"currentStep\":\"Verify multiple finished playbook runs are listed\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Switch to the \\\"Finished\\\" tab\",\"code\":\"await element(by.id('tabs.finished.button')).tap();\"},{\"step\":\"Verify multiple finished playbook runs are listed\",\"code\":\"// Wait for the scroll view to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Get all the run items that contain the \\\"finished-run-\\\" text pattern\\nconst runElements = await element(by.text(/finished-run-\\\\d+/)).getAttributes();\\n\\n// Verify that runElements is an array (multiple elements found)\\nif (!Array.isArray(runElements)) {\\n throw new Error(\\\"Expected multiple finished playbook runs but found only one or none\\\");\\n}\\n\\n// Verify we have more than one finished run\\njestExpect(runElements.length).toBeGreaterThan(1);\",\"error\":\"Expected multiple finished playbook runs but found only one or none\"}]}": [ + { + "value": { + "code": "// Wait for the scroll view containing the finished runs to be visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Since we can only see one finished run in the current view, we need to verify it exists\n// and then scroll to check for more\nconst firstRunText = 'finished-run-586316';\nawait waitFor(element(by.text(firstRunText)))\n .toBeVisible()\n .withTimeout(10000);\n\n// Scroll down to potentially reveal more finished runs\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(500, 'down');\n\n// Wait a moment for any new content to load\nawait new Promise(resolve => setTimeout(resolve, 1000));\n\n// Try to find another finished run with a different ID pattern\n// Since we can't get attributes for multiple elements, we'll check if scrolling reveals new content\ntry {\n // Check if the first run is still visible after scrolling\n const firstRunStillVisible = await element(by.text(firstRunText)).getAttributes();\n \n // If we scrolled but the same element is still the only one visible, \n // it means there's only one finished run\n if (firstRunStillVisible && firstRunStillVisible.visible) {\n // Scroll back up to check if there are runs above\n await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(500, 'up');\n await new Promise(resolve => setTimeout(resolve, 1000));\n \n // Try to find any other finished run pattern\n await waitFor(element(by.text(/finished-run-\\d+/)).atIndex(1))\n .toExist()\n .withTimeout(5000);\n }\n} catch (e) {\n throw new Error(\"Only one finished playbook run is visible in the list\");\n}" + }, + "creationTime": 1753349056976 + } + ], + "{\"currentStep\":\"Tap on the Playbook run to open it\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"}]}": [ + { + "value": { + "code": "await element(by.text(/checklist-details-run-[0-9a-f]+/)).tap();" + }, + "creationTime": 1754379279962 + } + ], + "{\"currentStep\":\"Verify Owner and Participants are displayed\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Tap on the Playbook run to open it\",\"code\":\"await element(by.text(/checklist-details-run-[0-9a-f]+/)).tap();\"}]}": [ + { + "value": { + "code": "// Wait for the \"Owner\" text to be visible\nawait waitFor(element(by.text('Owner'))).toBeVisible().withTimeout(10000);\n\n// Wait for the \"Participants\" text to be visible\nawait waitFor(element(by.text('Participants'))).toBeVisible().withTimeout(10000);\n\n// Verify Owner text is displayed\nawait expect(element(by.text('Owner'))).toBeVisible();\n\n// Verify Participants text is displayed\nawait expect(element(by.text('Participants'))).toBeVisible();" + }, + "creationTime": 1754379286296 + } + ], + "{\"currentStep\":\"Tap on the \\\"Checklist 1\\\" tab to collapse the checklist section\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Tap on the Playbook run to open it\",\"code\":\"await element(by.text(/checklist-details-run-[0-9a-f]+/)).tap();\"},{\"step\":\"Verify Owner and Participants are displayed\",\"code\":\"// Wait for the \\\"Owner\\\" text to be visible\\nawait waitFor(element(by.text('Owner'))).toBeVisible().withTimeout(10000);\\n\\n// Wait for the \\\"Participants\\\" text to be visible\\nawait waitFor(element(by.text('Participants'))).toBeVisible().withTimeout(10000);\\n\\n// Verify Owner text is displayed\\nawait expect(element(by.text('Owner'))).toBeVisible();\\n\\n// Verify Participants text is displayed\\nawait expect(element(by.text('Participants'))).toBeVisible();\"}]}": [ + { + "value": { + "code": "await element(by.text(/Checklist 1 \\([0-9a-f]+\\)/)).tap();" + }, + "creationTime": 1754379291802 + } + ], + "{\"currentStep\":\"Tap on the \\\"Checklist 1\\\" tab to expand the checklist section\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Tap on the Playbook run to open it\",\"code\":\"await element(by.text(/checklist-details-run-[0-9a-f]+/)).tap();\"},{\"step\":\"Verify Owner and Participants are displayed\",\"code\":\"// Wait for the \\\"Owner\\\" text to be visible\\nawait waitFor(element(by.text('Owner'))).toBeVisible().withTimeout(10000);\\n\\n// Wait for the \\\"Participants\\\" text to be visible\\nawait waitFor(element(by.text('Participants'))).toBeVisible().withTimeout(10000);\\n\\n// Verify Owner text is displayed\\nawait expect(element(by.text('Owner'))).toBeVisible();\\n\\n// Verify Participants text is displayed\\nawait expect(element(by.text('Participants'))).toBeVisible();\"},{\"step\":\"Tap on the \\\"Checklist 1\\\" tab to collapse the checklist section\",\"code\":\"await element(by.text(/Checklist 1 \\\\([0-9a-f]+\\\\)/)).tap();\"}]}": [ + { + "value": { + "code": "await element(by.text(/Checklist 1 \\([0-9a-f]+\\)/)).tap();" + }, + "creationTime": 1754379297366 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with Checklists", + "plan": { + "action": "Tap on the checkbox next to \"Task 1 for checklist 1 (b84cda)\"", + "thoughts": "I can see a playbook run details page displaying *two checklists* with their associated tasks. The view shows:\n- **Checklist 1 (b84cda)** with \"0 / 2 done\" status\n- **Checklist 2 (5625a2)** with \"0 / 2 done\" status\n\nEach checklist contains ***uncompleted tasks*** with empty checkbox indicators (ReactViewGroup elements at positions where checkboxes would be). To verify interacting with checklist items and updating progress, I need to *tap on one of these checkboxes* to mark a task as complete and observe the progress update.\n\nThe first task \"Task 1 for checklist 1 (b84cda)\" appears to have an associated checkbox element that I can interact with to test the progress updating functionality." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "5bdec756ec058b4d166332b08fca3206" + }, + "creationTime": 1754379704068 + } + ], + "{\"currentStep\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"previousSteps\":[]}": [ + { + "value": { + "code": "// Wait for the checkbox element to be visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Tap on the checkbox\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')).tap();" + }, + "creationTime": 1754379710032 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with Checklists", + "plan": { + "action": "Tap on the checkbox next to \"Task 2 for checklist 1 (b84cda)\"", + "thoughts": "Looking at the view hierarchy, I can see that the user has already interacted with the first task checkbox in the previous step. The screen shows **two checklists** with their respective tasks:\n\n1. ***Checklist 1 (b84cda)*** - Shows \"1 / 2 done\", indicating one task is already completed\n2. ***Checklist 2 (5625a2)*** - Shows \"0 / 2 done\", indicating no tasks are completed yet\n\nThe first checklist has a checkmark icon (✓) visible next to \"Task 1 for checklist 1 (b84cda)\", confirming the previous tap action was successful. The progress indicator has updated from what was likely \"0 / 2 done\" to \"1 / 2 done\".\n\nTo *verify interacting with checklist items and updating progress*, I should now interact with another unchecked task to see if the progress updates correctly. The most logical next action would be to tap on an unchecked task, such as **\"Task 2 for checklist 1 (b84cda)\"** or any task from ***Checklist 2***.\n\nTapping on \"Task 2 for checklist 1 (b84cda)\" would allow me to verify if the progress updates to \"2 / 2 done\" for the first checklist." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "c162bfa8ed82aa6464b255c25ae12169" + }, + "creationTime": 1754379724552 + } + ], + "{\"currentStep\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"previousSteps\":[{\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"code\":\"// Wait for the checkbox element to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')).tap();\"}]}": [ + { + "value": { + "code": "// Find and tap the checkbox next to \"Task 2 for checklist 1 (b84cda)\"\n// The checkbox is an empty ReactViewGroup element that appears before the task text\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')))\n .toBeVisible()\n .withTimeout(10000);\n\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')).tap();" + }, + "creationTime": 1754379735011 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with Task Progress", + "plan": { + "action": "wait 3000", + "thoughts": "Looking at the view hierarchy, I can see that the user has already interacted with checklist items based on the previous steps. They tapped on \"Task 1 for checklist 1\" and \"Task 2 for checklist 1\". \n\nNow examining the current state:\n- ***Checklist 1 (b84cda)*** shows \"1 / 2 done\" which indicates partial completion\n- I notice there's a *loading indicator* (ProgressBarContainerView with id=\"checklist-item-loading\") visible for one of the tasks in Checklist 1\n- ***Checklist 2 (5625a2)*** shows \"0 / 2 done\" meaning no tasks are completed yet\n\nThe goal is to \"Verify Interacting with checklist items and updating progress\". Since the user has already interacted with items in Checklist 1 and there's a **loading spinner active**, I should wait for this operation to complete before taking the next action. This will allow us to see if the progress updates correctly after the interaction." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "300b280e0adf3fdb8ddaf36063a303b5" + }, + "creationTime": 1754379747972 + } + ], + "{\"currentStep\":\"wait 3000\",\"previousSteps\":[{\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"code\":\"// Wait for the checkbox element to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')).tap();\"},{\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"code\":\"// Find and tap the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\\n// The checkbox is an empty ReactViewGroup element that appears before the task text\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')).tap();\"}]}": [ + { + "value": { + "code": "await new Promise(resolve => setTimeout(resolve, 3000));" + }, + "creationTime": 1754379755222 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Task Progress\",\"step\":\"wait 3000\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with Completed Checklist 1", + "plan": { + "action": "Tap on the checkbox area next to \"Task 1 for checklist 2 (5625a2)\"", + "thoughts": "Looking at the current state, I can see that *Checklist 1 (b84cda)* shows **\"2 / 2 done\"**, indicating that both tasks have been completed. The previous steps show that the user tapped on the checkboxes for both Task 1 and Task 2 of Checklist 1, and after a wait, the progress has been updated. Both tasks in Checklist 1 now show the ***check icon (✓)***, confirming they are marked as complete.\n\nNow I need to verify that I can interact with the second checklist. Looking at *Checklist 2 (5625a2)*, it shows **\"0 / 2 done\"**, meaning none of its tasks are completed yet. The tasks for Checklist 2 don't have check icons next to them, indicating they are unchecked. To further verify the interaction functionality, I should tap on one of the uncompleted tasks in Checklist 2.\n\nThe first uncompleted task is ***\"Task 1 for checklist 2 (5625a2)\"*** which appears to have an empty checkbox area where a check icon would appear if completed." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "d15760934197fa7d448cad4b9f6d4c2a" + }, + "creationTime": 1754379770246 + } + ], + "{\"currentStep\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"previousSteps\":[{\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"code\":\"// Wait for the checkbox element to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')).tap();\"},{\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"code\":\"// Find and tap the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\\n// The checkbox is an empty ReactViewGroup element that appears before the task text\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')).tap();\"},{\"step\":\"wait 3000\",\"code\":\"await new Promise(resolve => setTimeout(resolve, 3000));\"}]}": [ + { + "value": { + "code": "// Wait for the checkbox area next to \"Task 1 for checklist 2 (5625a2)\" to be visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Tap on the checkbox area\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')).tap();" + }, + "creationTime": 1754379776705 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Task Progress\",\"step\":\"wait 3000\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Completed Checklist 1\",\"step\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with Loading Task", + "plan": { + "action": "wait 3000", + "thoughts": "I can see that there's an active *loading indicator* (ProgressBar) next to \"Task 1 for checklist 2 (5625a2)\". This indicates that the checkbox interaction from the previous step is still being processed. The view shows that **Checklist 1 is complete** with \"2 / 2 done\" and both tasks showing check icons. However, **Checklist 2** still shows \"0 / 2 done\" and has a ***loading spinner*** where the checkbox should be for the first task. Since there's an active loader present, I need to wait for the operation to complete before proceeding with further interactions." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "44a307c87737bfb7e06de5ebfb370c9a" + }, + "creationTime": 1754379785580 + } + ], + "{\"currentStep\":\"wait 3000\",\"previousSteps\":[{\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"code\":\"// Wait for the checkbox element to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')).tap();\"},{\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"code\":\"// Find and tap the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\\n// The checkbox is an empty ReactViewGroup element that appears before the task text\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')).tap();\"},{\"step\":\"wait 3000\",\"code\":\"await new Promise(resolve => setTimeout(resolve, 3000));\"},{\"step\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"code\":\"// Wait for the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\" to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox area\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')).tap();\"}]}": [ + { + "value": { + "code": "await new Promise(resolve => setTimeout(resolve, 3000));" + }, + "creationTime": 1754379788555 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Task Progress\",\"step\":\"wait 3000\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Completed Checklist 1\",\"step\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Loading Task\",\"step\":\"wait 3000\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with Partial Task Completion", + "plan": { + "action": "Tap on the checkbox area next to \"Task 2 for checklist 2 (5625a2)\"", + "thoughts": "Looking at the current state, I can see that ***Checklist 1 (b84cda)*** shows \"2 / 2 done\" with both tasks having checkmarks (✓), indicating they are completed. For ***Checklist 2 (5625a2)***, it shows \"1 / 2 done\", meaning one task is completed and one is pending. \n\nExamining the checklist items more closely:\n- *Task 1 for checklist 2* has a checkmark icon (✓) indicating it's completed\n- *Task 2 for checklist 2* has an empty checkbox area with no checkmark, indicating it's not yet completed\n\nTo continue verifying the interaction with checklist items and updating progress, I should **tap on the uncompleted task** to mark it as done. This will help demonstrate the full interaction flow and show how the progress updates when all tasks are completed." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "40bbd36959a395fcd7fafd38997142dc" + }, + "creationTime": 1754379801469 + } + ], + "{\"currentStep\":\"Tap on the checkbox area next to \\\"Task 2 for checklist 2 (5625a2)\\\"\",\"previousSteps\":[{\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"code\":\"// Wait for the checkbox element to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')).tap();\"},{\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"code\":\"// Find and tap the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\\n// The checkbox is an empty ReactViewGroup element that appears before the task text\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')).tap();\"},{\"step\":\"wait 3000\",\"code\":\"await new Promise(resolve => setTimeout(resolve, 3000));\"},{\"step\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"code\":\"// Wait for the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\" to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox area\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')).tap();\"},{\"step\":\"wait 3000\",\"code\":\"await new Promise(resolve => setTimeout(resolve, 3000));\"}]}": [ + { + "value": { + "code": "// Wait for the checkbox area next to \"Task 2 for checklist 2 (5625a2)\" to be visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_3')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Tap on the checkbox area\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_3')).tap();" + }, + "creationTime": 1754379811102 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Task Progress\",\"step\":\"wait 3000\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Completed Checklist 1\",\"step\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Loading Task\",\"step\":\"wait 3000\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Partial Task Completion\",\"step\":\"Tap on the checkbox area next to \\\"Task 2 for checklist 2 (5625a2)\\\"\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with Loading Task 2", + "plan": { + "action": "wait 3000", + "thoughts": "I can see that there's a *loading indicator* (ProgressBar) visible in the view hierarchy next to \"Task 2 for checklist 2 (5625a2)\". The loading indicator is shown with id=\"checklist-item-loading\" at coordinates where the checkbox would normally appear. This indicates that the **action from the previous step is still being processed**. The system is updating the task status after the user tapped on the checkbox area. I should ***wait for this loading state to complete*** before proceeding with any verification or further actions." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "d0857d88d11be4ab07a6c750b9425af1" + }, + "creationTime": 1754379819199 + } + ], + "{\"currentStep\":\"wait 3000\",\"previousSteps\":[{\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"code\":\"// Wait for the checkbox element to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_0')).tap();\"},{\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"code\":\"// Find and tap the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\\n// The checkbox is an empty ReactViewGroup element that appears before the task text\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_5_1_3')).tap();\"},{\"step\":\"wait 3000\",\"code\":\"await new Promise(resolve => setTimeout(resolve, 3000));\"},{\"step\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"code\":\"// Wait for the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\" to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox area\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_0')).tap();\"},{\"step\":\"wait 3000\",\"code\":\"await new Promise(resolve => setTimeout(resolve, 3000));\"},{\"step\":\"Tap on the checkbox area next to \\\"Task 2 for checklist 2 (5625a2)\\\"\",\"code\":\"// Wait for the checkbox area next to \\\"Task 2 for checklist 2 (5625a2)\\\" to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the checkbox area\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_6_1_3')).tap();\"}]}": [ + { + "value": { + "code": "await new Promise(resolve => setTimeout(resolve, 3000));" + }, + "creationTime": 1754379822059 + } + ], + "{\"goal\":\"Verify Interacting with checklist items and updating progress\",\"previousSteps\":[{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 1 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Checklists\",\"step\":\"Tap on the checkbox next to \\\"Task 2 for checklist 1 (b84cda)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Task Progress\",\"step\":\"wait 3000\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Completed Checklist 1\",\"step\":\"Tap on the checkbox area next to \\\"Task 1 for checklist 2 (5625a2)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Loading Task\",\"step\":\"wait 3000\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Partial Task Completion\",\"step\":\"Tap on the checkbox area next to \\\"Task 2 for checklist 2 (5625a2)\\\"\",\"review\":{}},{\"screenDescription\":\"Playbook Run Details with Loading Task 2\",\"step\":\"wait 3000\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Run Details with All Tasks Completed", + "plan": { + "action": "success", + "thoughts": "Looking at the current state of the screen, I can see that ***all tasks have been completed*** successfully. Both checklists show *\"2 / 2 done\"* status:\n\n1. **Checklist 1 (b84cda)** - Shows \"2 / 2 done\" with both Task 1 and Task 2 displaying checkmark icons (✓)\n2. **Checklist 2 (5625a2)** - Shows \"2 / 2 done\" with both Task 1 and Task 2 displaying checkmark icons (✓)\n\nThe view hierarchy confirms all four checkbox elements have the *check-icon* testID and display the checkmark symbol (󰄬), indicating they are in a checked state. The progress indicators for both checklists show **100% completion**.\n\nThe goal was to \"Verify Interacting with checklist items and updating progress\" - through the previous steps, we have:\n- Successfully interacted with all checklist items by tapping their checkboxes\n- Observed the UI updating after each interaction (with wait times for loading states)\n- Achieved full completion of both checklists with all tasks marked as done\n\nThe interaction flow has been completed successfully and the progress has been fully updated to reflect all tasks as completed." + }, + "review": {}, + "goalAchieved": true, + "summary": "The goal has been ***successfully achieved***. We verified the ability to *interact with checklist items* and observed the **progress updating** correctly:\n\n1. Started with unchecked tasks in two checklists\n2. **Tapped checkboxes** for Task 1 and Task 2 in Checklist 1\n3. Waited for UI updates between interactions\n4. **Tapped checkboxes** for Task 1 and Task 2 in Checklist 2\n5. All tasks now show as completed with *checkmark icons*\n6. Both checklists display ***\"2 / 2 done\"*** indicating 100% completion\n\nThe interface successfully handled all interactions and updated the progress indicators appropriately." + }, + "snapshotHashes": { + "ViewHierarchyHash": "94d083cc453da6f5536ed35d28fb12fc" + }, + "creationTime": 1754379841824 + } + ], + "{\"currentStep\":\"Tap on the Playbook run to open it\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Tap on the Playbook run to open it\",\"code\":\"await element(by.text(/checklist-details-run-[0-9a-f]+/)).tap();\",\"error\":\"Test Failed: No views in hierarchy found matching: (an instance of android.widget.TextView and view.getText() with or without transformation to match: should match the pattern: /checklist-details-run-[0-9a-f]+/ and view has effective visibility )\\n\\n\\nHINT: To print view hierarchy on failed actions/matches, use log-level verbose or higher.\"}]}": [ + { + "value": { + "code": "await element(by.text('checklist-progress-run-60bc16')).tap();" + }, + "creationTime": 1754383016861 + } + ], + "{\"currentStep\":\"Tap on the icons section below \\\"Participants\\\" label\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Tap on the Playbook run to open it\",\"code\":\"await element(by.text('checklist-progress-run-60bc16')).tap();\"}]}": [ + { + "value": { + "code": "// Wait for the participants icon container to be visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_2_3')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Tap on the participants icon container\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_2_3')).tap();" + }, + "creationTime": 1754383024473 + } + ], + "{\"currentStep\":\"Verify bottom sheet with title \\\"Run Participants\\\" is displayed\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Tap on the Playbook run to open it\",\"code\":\"await element(by.text('checklist-progress-run-60bc16')).tap();\"},{\"step\":\"Tap on the icons section below \\\"Participants\\\" label\",\"code\":\"// Wait for the participants icon container to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_2_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the participants icon container\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_2_3')).tap();\"}]}": [ + { + "value": { + "code": "// Wait for the bottom sheet to be visible\nawait waitFor(element(by.text('Run Participants')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the title \"Run Participants\" is displayed\nawait expect(element(by.text('Run Participants'))).toBeVisible();" + }, + "creationTime": 1754383031211 + } + ], + "{\"currentStep\":\"Verify the participants list contains list of 3 users\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Tap on the Playbook run to open it\",\"code\":\"await element(by.text('checklist-progress-run-60bc16')).tap();\"},{\"step\":\"Tap on the icons section below \\\"Participants\\\" label\",\"code\":\"// Wait for the participants icon container to be visible\\nawait waitFor(element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_2_3')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Tap on the participants icon container\\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_2_3')).tap();\"},{\"step\":\"Verify bottom sheet with title \\\"Run Participants\\\" is displayed\",\"code\":\"// Wait for the bottom sheet to be visible\\nawait waitFor(element(by.text('Run Participants')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the title \\\"Run Participants\\\" is displayed\\nawait expect(element(by.text('Run Participants'))).toBeVisible();\"}]}": [ + { + "value": { + "code": "// Wait for the participants list to be visible\nawait waitFor(element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_1_0')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Get the attributes of the participants list container to verify it contains 3 users\nconst participantsList = await element(by.id('detox_temp_0_1_0_1_1_0_0_0_0_0_1_0_1_0_0_0_1_0')).getAttributes();\n\n// Check if the label contains all 3 users\nif (!participantsList.label || !participantsList.label.includes('user1aad5e') || \n !participantsList.label.includes('user94cd60') || !participantsList.label.includes('admin')) {\n throw new Error('Participants list does not contain all 3 expected users');\n}\n\n// Additionally verify each individual user element is visible\nawait expect(element(by.id('undefined.1e9z3yxjyt8suj857swaanpehc'))).toBeVisible();\nawait expect(element(by.id('undefined.jz6bt7pntf8ppfnx5961qim73r'))).toBeVisible();\nawait expect(element(by.id('undefined.s3hhuqd93tdi9k74xq3y6tqcxw'))).toBeVisible();" + }, + "creationTime": 1754383045212 + } + ], + "{\"currentStep\":\"Verify the \\\"Playbook runs\\\" screen opens\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"}]}": [ + { + "value": { + "code": "// Wait for and verify the \"Playbook runs\" title is visible in the header\nawait waitFor(element(by.text('Playbook runs')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the subtitle \"Channel fdb636\" is visible\nawait expect(element(by.text('Channel fdb636'))).toBeVisible();\n\n// Verify the tabs are visible\nawait expect(element(by.id('tabs.in-progress.button'))).toBeVisible();\nawait expect(element(by.id('tabs.finished.button'))).toBeVisible();\n\n// Verify that at least one playbook run item is visible in the list\nawait waitFor(element(by.text(/scroll-inprogress-[0-9]+-[a-f0-9]+/)))\n .toBeVisible()\n .withTimeout(10000);" + }, + "creationTime": 1754383302446 + } + ], + "{\"currentStep\":\"Verify the \\\"Playbook runs\\\" screen opens\",\"previousSteps\":[{\"step\":\"Tap the \\\"Quick Actions\\\" icon in the Channel Header\",\"code\":\"await element(by.id('channel_header.channel_quick_actions.button')).tap();\"},{\"step\":\"Tap the \\\"Playbook runs\\\" option in the bottom sheet\",\"code\":\"await element(by.text(/Playbook runs/)).tap();\"},{\"step\":\"Verify the \\\"Playbook runs\\\" screen opens\",\"code\":\"// Wait for and verify the \\\"Playbook runs\\\" title is visible in the header\\nawait waitFor(element(by.text('Playbook runs')))\\n .toBeVisible()\\n .withTimeout(10000);\\n\\n// Verify the subtitle \\\"Channel fdb636\\\" is visible\\nawait expect(element(by.text('Channel fdb636'))).toBeVisible();\\n\\n// Verify the tabs are visible\\nawait expect(element(by.id('tabs.in-progress.button'))).toBeVisible();\\nawait expect(element(by.id('tabs.finished.button'))).toBeVisible();\\n\\n// Verify that at least one playbook run item is visible in the list\\nawait waitFor(element(by.text(/scroll-inprogress-[0-9]+-[a-f0-9]+/)))\\n .toBeVisible()\\n .withTimeout(10000);\",\"error\":\"Test Failed: '(an instance of android.widget.TextView and view.getText() with or without transformation to match: should match the pattern: /scroll-inprogress-[0-9]+-[a-f0-9]+/ and view has effective visibility )' matches 10 views in the hierarchy:\\n- [1] ReactTextView{id=2267, visibility=VISIBLE, width=1153, height=84, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@YYYYYY, tag=detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_0_0_0_0, root-is-layout-requested=false, has-input-connection=false, x=74.0, y=60.0, text=scroll-inprogress-9-bac2fe, input-type=0, ime-target=false, has-links=false}\\n- [2] ReactTextView{id=2329, visibility=VISIBLE, width=1153, height=84, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@YYYYYY, tag=detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_0_1_0_0, root-is-layout-requested=false, has-input-connection=false, x=74.0, y=60.0, text=scroll-inprogress-8-8dfa63, input-type=0, ime-target=false, has-links=false}\\n- [3] ReactTextView{id=2393, visibility=VISIBLE, width=1153, height=84, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@YYYYYY, tag=detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_0_2_0_0, root-is-layout-requested=false, has-input-connection=false, x=74.0, y=60.0, text=scroll-inprogress-7-c08730, input-type=0, ime-target=false, has-links=false}\\n- [4] ReactTextView{id=2455, visibility=VISIBLE, width=1153, height=84, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@YYYYYY, tag=detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_0_3_0_0, root-is-layout-requested=false, has-input-connection=false, x=74.0, y=60.0, text=scroll-inprogress-6-98e216, input-type=0, ime-target=false, has-links=false}\\n- [5] ReactTextView{id=2517, visibility=VISIBLE, width=1153, height=84, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@YYYYYY, tag=detox_temp_0_1_0_1_0_0_0_0_0_0_1_0_0_0_4_0_0, root-is-layout-requested=false, has-input-connection=false, x=74.0, y=60.0, text=scroll-inprogress-5-07e410, input-type=0, ime-target=false, has-links=false}\\n- [truncated, listing 5 out of 10 views].\\nProblem views are marked with '****MATCHES****' below.\\n\\n\\nHINT: To print view hierarchy on failed actions/matches, use log-level verbose or higher.\"}]}": [ + { + "value": { + "code": "// Wait for the \"Playbook runs\" title to be visible\nawait waitFor(element(by.text('Playbook runs')))\n .toBeVisible()\n .withTimeout(10000);\n\n// Verify the subtitle is visible\nawait expect(element(by.text('Channel fdb636'))).toBeVisible();\n\n// Verify the \"In Progress\" tab is visible\nawait expect(element(by.id('tabs.in-progress.button'))).toBeVisible();\n\n// Verify the \"Finished\" tab is visible\nawait expect(element(by.id('tabs.finished.button'))).toBeVisible();\n\n// Verify that at least one playbook run item is visible in the list\n// Using a more specific text match to avoid multiple matches error\nawait waitFor(element(by.text('scroll-inprogress-9-bac2fe')))\n .toBeVisible()\n .withTimeout(10000);" + }, + "creationTime": 1754383311041 + } + ], + "{\"goal\":\"Scroll through the list of playbooks in the In Progress tab and verify that the list scrolls smoothly and all playbooks are visible\",\"previousSteps\":[]}": [ + { + "value": { + "screenDescription": "Playbook Runs - In Progress Tab", + "plan": { + "action": "Scroll down through the list of playbooks to see more items", + "thoughts": "I can see the *In Progress tab* is currently selected and displaying a list of playbooks. The view shows multiple playbook items (scroll-inprogress-0 through scroll-inprogress-9) in a **ReactScrollView**. Each item displays the playbook name, user \"user6d1445\", and \"Last update Now\". \n\nThe goal is to ***scroll through the list*** and verify smooth scrolling and visibility of all playbooks. Looking at the view hierarchy, I can see there are 10 playbook items currently visible (scroll-inprogress-0 through scroll-inprogress-9). The list appears to be contained within a scrollable container (ReactScrollView).\n\nTo properly test the scrolling functionality and verify all playbooks are visible, I should perform a **scroll action** on this list. This will help verify both the smooth scrolling behavior and whether there are more playbooks beyond what's currently visible." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "089a7dc3539814641623143e0e48a6fb" + }, + "creationTime": 1754383320869 + } + ], + "{\"currentStep\":\"Scroll down through the list of playbooks to see more items\",\"previousSteps\":[]}": [ + { + "value": { + "code": "await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(200, 'down');" + }, + "creationTime": 1754383325808 + } + ], + "{\"goal\":\"Scroll through the list of playbooks in the In Progress tab and verify that the list scrolls smoothly and all playbooks are visible\",\"previousSteps\":[{\"screenDescription\":\"Playbook Runs - In Progress Tab\",\"step\":\"Scroll down through the list of playbooks to see more items\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Runs - In Progress Tab with Scrollable List", + "plan": { + "action": "Continue scrolling down in the playbooks list to see if there are more items below", + "thoughts": "Looking at the view hierarchy, I can see a *ReactScrollView* containing a list of playbook items. The list shows 10 items from \"scroll-inprogress-9-bac2fe\" down to \"scroll-inprogress-0-8a7c5d\". The scroll view appears to be **already scrolled down** since the y-coordinate of the inner container is at -130, indicating some content is above the visible area. \n\nTo verify that the list scrolls smoothly and all playbooks are visible, I need to ***continue scrolling down*** to see if there are more items below the currently visible \"scroll-inprogress-0-8a7c5d\" item. This will help confirm whether we've reached the end of the list or if there are additional playbooks to display.\n\nThe previous step was to \"Scroll down through the list of playbooks to see more items\", and now I need to continue this scrolling action to fully explore the list and ensure smooth scrolling behavior." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "a86efde8df8e156f69dd0585274ec095" + }, + "creationTime": 1754383338494 + } + ], + "{\"currentStep\":\"Continue scrolling down in the playbooks list to see if there are more items below\",\"previousSteps\":[{\"step\":\"Scroll down through the list of playbooks to see more items\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(200, 'down');\"}]}": [ + { + "value": { + "code": "await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(200, 'down');" + }, + "creationTime": 1754383345693 + } + ], + "{\"goal\":\"Scroll through the list of playbooks in the In Progress tab and verify that the list scrolls smoothly and all playbooks are visible\",\"previousSteps\":[{\"screenDescription\":\"Playbook Runs - In Progress Tab\",\"step\":\"Scroll down through the list of playbooks to see more items\",\"review\":{}},{\"screenDescription\":\"Playbook Runs - In Progress Tab with Scrollable List\",\"step\":\"Continue scrolling down in the playbooks list to see if there are more items below\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Runs - In Progress Tab with Scrolled List", + "plan": { + "action": "Scroll up in the playbooks list to see the items at the top", + "thoughts": "Looking at the view hierarchy, I can see a *ReactScrollView* containing a list of playbook items. The list shows **10 playbook items** labeled from \"scroll-inprogress-9-bac2fe\" down to \"scroll-inprogress-0-8a7c5d\". The Y coordinates indicate that the list has been scrolled - the first visible item starts at Y: -759, meaning content above is hidden. The last visible item \"scroll-inprogress-0-8a7c5d\" is at Y: 3273. \n\nBased on the previous steps, the user has already ***scrolled down twice*** to see more items. The goal is to verify that the list scrolls smoothly and all playbooks are visible. Since we've scrolled down and can see items 9 through 0, we should now *scroll back up* to verify we can see the beginning of the list again and confirm the scrolling works in both directions. This will help verify the smooth scrolling functionality and ensure all items are accessible." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "93143f546c46bcb1b7afd591e17562ea" + }, + "creationTime": 1754383358446 + } + ], + "{\"currentStep\":\"Scroll up in the playbooks list to see the items at the top\",\"previousSteps\":[{\"step\":\"Scroll down through the list of playbooks to see more items\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(200, 'down');\"},{\"step\":\"Continue scrolling down in the playbooks list to see if there are more items below\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(200, 'down');\"}]}": [ + { + "value": { + "code": "await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scrollTo('top');" + }, + "creationTime": 1754383362885 + } + ], + "{\"goal\":\"Scroll through the list of playbooks in the In Progress tab and verify that the list scrolls smoothly and all playbooks are visible\",\"previousSteps\":[{\"screenDescription\":\"Playbook Runs - In Progress Tab\",\"step\":\"Scroll down through the list of playbooks to see more items\",\"review\":{}},{\"screenDescription\":\"Playbook Runs - In Progress Tab with Scrollable List\",\"step\":\"Continue scrolling down in the playbooks list to see if there are more items below\",\"review\":{}},{\"screenDescription\":\"Playbook Runs - In Progress Tab with Scrolled List\",\"step\":\"Scroll up in the playbooks list to see the items at the top\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Runs - In Progress Tab with Full List Visible", + "plan": { + "action": "Scroll down slowly through the playbooks list to verify smooth scrolling", + "thoughts": "Looking at the view hierarchy, I can see the *In Progress tab* is currently selected and displaying a list of playbooks. The list contains **10 playbook items** (scroll-inprogress-0 through scroll-inprogress-9), each showing the playbook name, user \"user6d1445\", and \"Last update Now\" timestamp.\n\nBased on the previous steps:\n1. The user scrolled down to see more items\n2. Continued scrolling to check for additional items\n3. Scrolled back up to see items at the top\n\nThe current view shows all 10 playbooks are visible in the list. The ReactScrollView contains an AutoLayoutView with all items from ***scroll-inprogress-9*** at the top to ***scroll-inprogress-0*** at the bottom. The list appears to be fully loaded with no loading indicators present.\n\nTo complete the goal of verifying that \"the list scrolls smoothly and all playbooks are visible\", I need to perform one more scroll action to demonstrate the smooth scrolling capability. Since we've already scrolled down and up, I should scroll down once more to verify the smooth scrolling behavior one final time." + }, + "review": {}, + "goalAchieved": false + }, + "snapshotHashes": { + "ViewHierarchyHash": "089a7dc3539814641623143e0e48a6fb" + }, + "creationTime": 1754383376682 + } + ], + "{\"currentStep\":\"Scroll down slowly through the playbooks list to verify smooth scrolling\",\"previousSteps\":[{\"step\":\"Scroll down through the list of playbooks to see more items\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(200, 'down');\"},{\"step\":\"Continue scrolling down in the playbooks list to see if there are more items below\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(200, 'down');\"},{\"step\":\"Scroll up in the playbooks list to see the items at the top\",\"code\":\"await element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scrollTo('top');\"}]}": [ + { + "value": { + "code": "// Scroll down slowly through the playbooks list\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(50, 'down');\nawait new Promise(resolve => setTimeout(resolve, 500));\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(50, 'down');\nawait new Promise(resolve => setTimeout(resolve, 500));\nawait element(by.id('detox_temp_0_1_0_1_0_0_0_0_0_0_1_0')).scroll(50, 'down');" + }, + "creationTime": 1754383383122 + } + ], + "{\"goal\":\"Scroll through the list of playbooks in the In Progress tab and verify that the list scrolls smoothly and all playbooks are visible\",\"previousSteps\":[{\"screenDescription\":\"Playbook Runs - In Progress Tab\",\"step\":\"Scroll down through the list of playbooks to see more items\",\"review\":{}},{\"screenDescription\":\"Playbook Runs - In Progress Tab with Scrollable List\",\"step\":\"Continue scrolling down in the playbooks list to see if there are more items below\",\"review\":{}},{\"screenDescription\":\"Playbook Runs - In Progress Tab with Scrolled List\",\"step\":\"Scroll up in the playbooks list to see the items at the top\",\"review\":{}},{\"screenDescription\":\"Playbook Runs - In Progress Tab with Full List Visible\",\"step\":\"Scroll down slowly through the playbooks list to verify smooth scrolling\",\"review\":{}}]}": [ + { + "value": { + "screenDescription": "Playbook Runs - In Progress Tab with Full List", + "plan": { + "action": "success", + "thoughts": "Looking at the view hierarchy, I can see that the *In Progress tab* is currently active and displaying a list of playbooks. The list contains **10 playbook items** (scroll-inprogress-0 through scroll-inprogress-9), all of which are visible in the current view. Each playbook item shows the playbook name, user \"user6d1445\", and \"Last update Now\".\n\nBased on the previous steps, the user has already:\n1. Scrolled down to see more items\n2. Continued scrolling down\n3. Scrolled back up to see items at the top\n4. Scrolled down slowly to verify smooth scrolling\n\nThe goal is to ***\"scroll through the list and verify that the list scrolls smoothly and all playbooks are visible\"***. \n\nLooking at the current state:\n- All 10 playbooks (scroll-inprogress-0 through scroll-inprogress-9) are already visible in the view\n- The list appears to be fully loaded with no loading indicators\n- The previous steps have already tested scrolling in both directions\n- The user has verified smooth scrolling in step 4\n\nSince the user has already scrolled through the list in multiple directions and all playbooks are visible, the goal has been achieved. The list has been thoroughly tested for smooth scrolling and visibility of all items." + }, + "review": {}, + "goalAchieved": true, + "summary": "The goal has been ***successfully completed***. The user has *scrolled through the playbook list* in the In Progress tab multiple times:\n- First scrolled down to reveal more items\n- Continued scrolling to check for additional items\n- Scrolled back up to see the top items\n- Performed a slow scroll to verify smooth scrolling\n\nAll **10 playbooks** (scroll-inprogress-0 through scroll-inprogress-9) are visible and the list scrolling functionality has been thoroughly tested in both directions, confirming smooth scrolling behavior." + }, + "snapshotHashes": { + "ViewHierarchyHash": "80c9fd411cfc8ea35851b910192122da" + }, + "creationTime": 1754383403157 + } + ] +} \ No newline at end of file diff --git a/detox/e2e/test/products/playbooks/playbooks.e2e.ts b/detox/e2e/test/products/playbooks/playbooks.e2e.ts new file mode 100644 index 000000000..91b25b553 --- /dev/null +++ b/detox/e2e/test/products/playbooks/playbooks.e2e.ts @@ -0,0 +1,228 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Setup, User, Team, Playbooks, PlaybooksHelpers, Channel} from '@support/server_api'; +import {siteOneUrl} from '@support/test_config'; +import {ServerScreen, LoginScreen, ChannelScreen, ChannelListScreen, ThreadScreen} from '@support/ui/screen'; + +describe('Playbooks - Basic', () => { + const serverOneDisplayName = 'Server 1'; + const channelsCategory = 'channels'; + + let testUser: any; + let testTeam: any; + let testChannel: any; + let secondUser: any; + let thirdUser: any; + + beforeAll(async () => { + // Create test user, team, and channel + ({user: testUser, team: testTeam, channel: testChannel} = await Setup.apiInit(siteOneUrl)); + await Playbooks.apiEnablePlugin(siteOneUrl, 'playbooks'); + + // Create additional test users + ({user: secondUser} = await User.apiCreateUser(siteOneUrl)); + ({user: thirdUser} = await User.apiCreateUser(siteOneUrl)); + + await Team.apiAddUserToTeam(siteOneUrl, secondUser.id, testTeam.id); + await Team.apiAddUserToTeam(siteOneUrl, thirdUser.id, testTeam.id); + + await Channel.apiAddUserToChannel(siteOneUrl, secondUser.id, testChannel.id); + await Channel.apiAddUserToChannel(siteOneUrl, thirdUser.id, testChannel.id); + + // # Log in to server + await ServerScreen.connectToServer(siteOneUrl, serverOneDisplayName); + await LoginScreen.login(testUser); + }); + + beforeEach(async () => { + // * Verify on channel list screen + await ChannelListScreen.toBeVisible(); + }); + + it('should verify in-progress playbook information in Channel Header and Channel Info ', async () => { + // Generate a random playbook + const playbook = PlaybooksHelpers.generateRandomPlaybook({ + teamId: testTeam.id, + userId: secondUser.id, + prefix: 'e2e-test', + numChecklists: 2, + numItems: 3, + channel_id: testChannel.id, + }); + + // Create the playbook + const {id: playbookId} = await Playbooks.apiCreatePlaybook(siteOneUrl, playbook); + + // Get the created playbook + await Playbooks.apiGetPlaybook(siteOneUrl, playbookId); + + // Generate a playbook run + const playbookRun = PlaybooksHelpers.generateRandomPlaybookRun({ + teamId: testTeam.id, + playbookId, + channelId: testChannel.id, + ownerId: secondUser.id, + prefix: 'e2e-run', + }); + + // Start the playbook run + const activeRun = await Playbooks.apiRunPlaybook(siteOneUrl, playbookRun); + await Playbooks.apiAddUsersToRun(siteOneUrl, activeRun.id, [secondUser.id, thirdUser.id]); + await ChannelScreen.open(channelsCategory, testChannel.name); + + await pilot.perform( + 'Verify the Playbook icon and count "1" are visible in the Channel Header', + 'Tap the "Quick Actions" icon in the Channel Header', + 'Verify a bottom sheet with the "Playbook runs" option is displayed', + 'Verify that "Playbook runs" option is listed in the bottom sheet', + 'Tap the "Playbook runs" option in the bottom sheet', + 'Verify the "Playbook runs" screen opens', + 'Verify the "In Progress" tab is selected', + ); + + await Playbooks.apiDeletePlaybook(siteOneUrl, playbookId); + await ThreadScreen.back(); + await ChannelScreen.back(); + }); + + it('should verify finished playbooks are listed in the Finished tab', async () => { + // Create and finish a playbook run + const playbook = PlaybooksHelpers.generateRandomPlaybook({ + teamId: testTeam.id, + userId: secondUser.id, + prefix: 'finished-test', + channel_id: testChannel.id, + }); + const {id: playbookId} = await Playbooks.apiCreatePlaybook(siteOneUrl, playbook); + const playbookRun = PlaybooksHelpers.generateRandomPlaybookRun({ + teamId: testTeam.id, + playbookId, + channelId: testChannel.id, + ownerId: secondUser.id, + prefix: 'finished-run', + }); + const activeRun = await Playbooks.apiRunPlaybook(siteOneUrl, playbookRun); + await Playbooks.apiFinishRun(siteOneUrl, activeRun.id); + await ChannelScreen.open(channelsCategory, testChannel.name); + await pilot.perform( + 'Tap the "Quick Actions" icon in the Channel Header', + 'Tap the "Playbook runs" option in the bottom sheet', + 'Switch to the "Finished" tab', + 'Verify multiple finished playbook runs are listed', + ); + + await Playbooks.apiDeletePlaybook(siteOneUrl, playbookId); + await ThreadScreen.back(); + await ChannelScreen.back(); + }); + + it('should verify scrolling of playbooks in the In Progress tab', async () => { + const playbookRunsToDelete: string[] = []; + + // Create multiple in-progress playbook runs + const playbook = PlaybooksHelpers.generateRandomPlaybook({ + teamId: testTeam.id, + userId: secondUser.id, + prefix: 'scroll-inprogress', + channel_id: testChannel.id, + }); + const {id: playbookId} = await Playbooks.apiCreatePlaybook(siteOneUrl, playbook); + for (let i = 0; i < 10; i++) { + const playbookRun = PlaybooksHelpers.generateRandomPlaybookRun({ + teamId: testTeam.id, + playbookId, + channelId: testChannel.id, + ownerId: secondUser.id, + prefix: `scroll-inprogress-${i}`, + }); + // eslint-disable-next-line no-await-in-loop + const activeRun = await Playbooks.apiRunPlaybook(siteOneUrl, playbookRun); + playbookRunsToDelete.push(activeRun.id); + } + await ChannelScreen.open(channelsCategory, testChannel.name); + await pilot.perform( + 'Tap the "Quick Actions" icon in the Channel Header', + 'Tap the "Playbook runs" option in the bottom sheet', + 'Verify the "Playbook runs" screen opens', + ); + await pilot.autopilot('Scroll through the list of playbooks in the In Progress tab and verify that the list scrolls smoothly and all playbooks are visible'); + + await Promise.all(playbookRunsToDelete.map((runId) => Playbooks.apiDeletePlaybook(siteOneUrl, runId))); + await ThreadScreen.back(); + await ChannelScreen.back(); + }); + + it('should verify details of a checklist in a particular playbook', async () => { + // Create a playbook with a checklist + const playbook = PlaybooksHelpers.generateRandomPlaybook({ + teamId: testTeam.id, + userId: secondUser.id, + prefix: 'checklist-details', + numChecklists: 2, + numItems: 2, + channel_id: testChannel.id, + }); + const {id: playbookId} = await Playbooks.apiCreatePlaybook(siteOneUrl, playbook); + const playbookRun = PlaybooksHelpers.generateRandomPlaybookRun({ + teamId: testTeam.id, + playbookId, + channelId: testChannel.id, + ownerId: secondUser.id, + prefix: 'checklist-details-run', + }); + const activeRun = await Playbooks.apiRunPlaybook(siteOneUrl, playbookRun); + await Playbooks.apiAddUsersToRun(siteOneUrl, activeRun.id, [testUser.id, secondUser.id, thirdUser.id]); + await ChannelScreen.open(channelsCategory, testChannel.name); + await pilot.perform( + 'Tap the "Quick Actions" icon in the Channel Header', + 'Tap the "Playbook runs" option in the bottom sheet', + 'Tap on the Playbook run to open it', + 'Verify Owner and Participants are displayed', + 'Tap on the "Checklist 1" tab to collapse the checklist section', + 'Tap on the "Checklist 1" tab to expand the checklist section', + ); + await pilot.autopilot('Verify Interacting with checklist items and updating progress'); + + await Playbooks.apiDeletePlaybook(siteOneUrl, playbookId); + await ThreadScreen.back(); + await ThreadScreen.back(); + await ChannelScreen.back(); + }); + + it('should verify the participants list in the playbook', async () => { + // Create a playbook with a checklist + const playbook = PlaybooksHelpers.generateRandomPlaybook({ + teamId: testTeam.id, + userId: secondUser.id, + prefix: 'checklist-progress', + numChecklists: 1, + numItems: 2, + channel_id: testChannel.id, + }); + const {id: playbookId} = await Playbooks.apiCreatePlaybook(siteOneUrl, playbook); + const playbookRun = PlaybooksHelpers.generateRandomPlaybookRun({ + teamId: testTeam.id, + playbookId, + channelId: testChannel.id, + ownerId: secondUser.id, + prefix: 'checklist-progress-run', + }); + const activeRun = await Playbooks.apiRunPlaybook(siteOneUrl, playbookRun); + await Playbooks.apiAddUsersToRun(siteOneUrl, activeRun.id, [testUser.id, secondUser.id, thirdUser.id]); + await ChannelScreen.open(channelsCategory, testChannel.name); + await pilot.perform( + 'Tap the "Quick Actions" icon in the Channel Header', + 'Tap the "Playbook runs" option in the bottom sheet', + 'Tap on the Playbook run to open it', + 'Tap on the icons section below "Participants" label', + 'Verify bottom sheet with title "Run Participants" is displayed', + 'Verify the participants list contains list of 3 users', + ); + + await Playbooks.apiDeletePlaybook(siteOneUrl, playbookId); + await ThreadScreen.back(); + await ThreadScreen.back(); + await ChannelScreen.back(); + }); +}); diff --git a/detox/e2e/test/setup.ts b/detox/e2e/test/setup.ts index 59c8d232e..43e7248de 100644 --- a/detox/e2e/test/setup.ts +++ b/detox/e2e/test/setup.ts @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. /* eslint-disable no-await-in-loop, no-console */ +import {ClaudePromptHandler} from '@support/pilot/ClaudePromptHandler'; import {Plugin, System, User} from '@support/server_api'; import {siteOneUrl} from '@support/test_config'; @@ -82,7 +83,22 @@ export async function launchAppWithRetry(): Promise { throw new Error(`Failed to launch app after ${MAX_RETRY_ATTEMPTS} attempts. Last error: ${(lastError as Error).message}`); } +/** + * Initialize ClaudePromptHandler if ANTHROPIC_API_KEY is set + * @returns {Promise} + */ +async function initializeClaudePromptHandler(): Promise { + if (process.env.ANTHROPIC_API_KEY) { + const promptHandler = new ClaudePromptHandler(process.env.ANTHROPIC_API_KEY); + pilot.init(promptHandler); + } else { + console.info('To use ClaudePromptHandler, please set the ANTHROPIC_API_KEY environment variable.'); + } +} + beforeAll(async () => { + await initializeClaudePromptHandler(); + // Login as sysadmin and reset server configuration await System.apiCheckSystemHealth(siteOneUrl); await User.apiAdminLogin(siteOneUrl); diff --git a/detox/package-lock.json b/detox/package-lock.json index ca5f7d327..73fc11a14 100644 --- a/detox/package-lock.json +++ b/detox/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "mattermost-mobile-e2e", + "dependencies": { + "@anthropic-ai/sdk": "^0.56.0" + }, "devDependencies": { "@aws-sdk/client-s3": "3.712.0", "@aws-sdk/lib-storage": "3.712.0", @@ -16,6 +19,8 @@ "@types/jest": "29.5.14", "@types/tough-cookie": "4.0.5", "@types/uuid": "10.0.0", + "@wix-pilot/core": "^3.4.0", + "@wix-pilot/detox": "^1.0.12", "async": "3.2.6", "axios": "1.8.2", "axios-cookiejar-support": "5.0.4", @@ -59,6 +64,15 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.56.0.tgz", + "integrity": "sha512-SLCB8M8+VMg1cpCucnA1XWHGWqVSZtIWzmOdDOEu3eTFZMB+A0sGZ1ESO5MHDnqrNTXz3safMrWx9x4rMZSOqA==", + "license": "MIT", + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -4320,9 +4334,9 @@ "license": "MIT" }, "node_modules/@wix-pilot/core": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@wix-pilot/core/-/core-3.2.6.tgz", - "integrity": "sha512-iGjHOOpO13ZNfP3CZDlYhBZ7R6ZqO+1R38nDv5BCIOecD4A4z5/WpEyapvw+ZFsECjFZLWhuKNV0KTJoQAvxYw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@wix-pilot/core/-/core-3.4.0.tgz", + "integrity": "sha512-TH5iH0BISSkMSVBSRiTnh4oYEzlA4gErGiI14US+Ze17uSrWXyrcb4GBpjIuAFMvM3/UbmQBkEg2hXGcl9aDZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4340,12 +4354,12 @@ } }, "node_modules/@wix-pilot/detox": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@wix-pilot/detox/-/detox-1.0.11.tgz", - "integrity": "sha512-oCeYz7EhcWOyp0KbaJ3hipMbZ+nWuZ7Pvjno3sUzP1EebrZBZoRoDp16JZMR6H+i+h8OdqkrviI0mR2mYFZzMg==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@wix-pilot/detox/-/detox-1.0.12.tgz", + "integrity": "sha512-yMf9aUv7D+5rViGd7714P06bIgmcJklFFz4/Vjji6TH1gyChX+I/OwP0iK7L5Tob96bPTB9ApM6gR6Je66oIOA==", "dev": true, "peerDependencies": { - "@wix-pilot/core": "^3.1.6", + "@wix-pilot/core": "^3.4.0", "detox": ">=20.33.0", "expect": "29.x.x || 28.x.x || ^27.2.5" } @@ -5471,6 +5485,28 @@ "node": ">=10" } }, + "node_modules/detox/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/diff": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", @@ -9983,27 +10019,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", diff --git a/detox/package.json b/detox/package.json index 46a4ea4e5..13f0c7ac9 100644 --- a/detox/package.json +++ b/detox/package.json @@ -14,6 +14,8 @@ "@types/jest": "29.5.14", "@types/tough-cookie": "4.0.5", "@types/uuid": "10.0.0", + "@wix-pilot/core": "^3.4.0", + "@wix-pilot/detox": "^1.0.12", "async": "3.2.6", "axios": "1.8.2", "axios-cookiejar-support": "5.0.4", @@ -60,5 +62,8 @@ "e2e:save-report": "node save_report.js", "start:webhook": "node webhook_server.js", "tsc": "NODE_OPTIONS=--max_old_space_size=12000 tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.56.0" } } diff --git a/detox/save_report.js b/detox/save_report.js index a398151c1..5ee2e4caf 100644 --- a/detox/save_report.js +++ b/detox/save_report.js @@ -121,8 +121,8 @@ const saveReport = async () => { } // "ios-results-*" or "android-results-*" is the path in CI where the parallel detox jobs save the artifacts - await mergeJestStareJsonFiles(jestStareCombinedFilePath, [`${ARTIFACTS_DIR}/${platform}-results*/jest-stare/${platform}-data*.json`]); - generateJestStareHtmlReport(jestStareOutputDir, `${platform}-report.html`, jestStareCombinedFilePath, platform); + await mergeJestStareJsonFiles(jestStareCombinedFilePath, [`${ARTIFACTS_DIR}/${platform}-results*/jest-stare/${platform}-data*.json`], platform); + await generateJestStareHtmlReport(jestStareOutputDir, `${platform}-report.html`, jestStareCombinedFilePath, platform); if (process.env.CI) { // Delete folders starting with "ios-results-" or "android-results-" only in CI environment