diff --git a/client/controller/index.web.js b/client/controller/index.web.js index b29501598a2731..3a8ac38486b41d 100644 --- a/client/controller/index.web.js +++ b/client/controller/index.web.js @@ -16,6 +16,7 @@ import MomentProvider from 'calypso/components/localized-moment/provider'; import { RouteProvider } from 'calypso/components/route'; import Layout from 'calypso/layout'; import LayoutLoggedOut from 'calypso/layout/logged-out'; +import { isE2ETest } from 'calypso/lib/e2e'; import { loadExperimentAssignment } from 'calypso/lib/explat'; import { navigate } from 'calypso/lib/navigate'; import { createAccountUrl, login } from 'calypso/lib/paths'; @@ -396,7 +397,7 @@ export const redirectIfDuplicatedView = ( wpAdminPath ) => async ( context, next const duplicateViewsExperimentAssignment = await loadExperimentAssignment( 'calypso_post_onboarding_holdout_120924' ); - if ( duplicateViewsExperimentAssignment.variationName === 'treatment' ) { + if ( isE2ETest() || duplicateViewsExperimentAssignment.variationName === 'treatment' ) { const state = context.store.getState(); const siteId = getSelectedSiteId( state ); const wpAdminUrl = getSiteAdminUrl( state, siteId, wpAdminPath ); diff --git a/packages/calypso-e2e/src/lib/components/index.ts b/packages/calypso-e2e/src/lib/components/index.ts index 66083cd5c79f1f..ce968888a8dbc9 100644 --- a/packages/calypso-e2e/src/lib/components/index.ts +++ b/packages/calypso-e2e/src/lib/components/index.ts @@ -36,6 +36,7 @@ export * from './full-side-editor-nav-sidebar-component'; export * from './full-side-editor-data-views-component'; export * from './editor-dimensions-component'; export * from './jetpack-instant-search-modal-component'; +export * from './wp-admin-notice-component'; export * from './me'; diff --git a/packages/calypso-e2e/src/lib/components/wp-admin-notice-component.ts b/packages/calypso-e2e/src/lib/components/wp-admin-notice-component.ts new file mode 100644 index 00000000000000..745f0d4db14844 --- /dev/null +++ b/packages/calypso-e2e/src/lib/components/wp-admin-notice-component.ts @@ -0,0 +1,45 @@ +import { Page } from 'playwright'; + +type NoticeType = 'Updated'; + +/** + * Represents the Notification component. + */ +export class WpAdminNoticeComponent { + private page: Page; + + /** + * Creates an instance of the component. + * + * @param {Page} page Object representing the base page. + */ + constructor( page: Page ) { + this.page = page; + } + + /** + * Verifies the content in a notification on the page. + * + * This method requires either full or partial text of + * the notification to be supplied as parameter. + * + * Optionally, it is possible to specify the `type` parameter to limit + * validation to a certain type of notifications eg. `error`. + * + * @param {string} text Full or partial text to validate on page. + * @param param1 Optional parameters. + * @param {NoticeType} param1.type Type of notice to limit validation to. + * @param {number} param1.timeout Custom timeout value. + */ + async noticeShown( + text: string, + { type, timeout }: { type?: NoticeType; timeout?: number } = {} + ): Promise< void > { + const noticeType = type ? `.${ type.toLowerCase() }` : ''; + + const selector = `div.notice${ noticeType } :text("${ text }")`; + + const locator = this.page.locator( selector ); + await locator.waitFor( { state: 'visible', timeout: timeout } ); + } +} diff --git a/packages/calypso-e2e/src/lib/pages/editor-page.ts b/packages/calypso-e2e/src/lib/pages/editor-page.ts index 6ae3d55fafa5c5..0f72675f313b8c 100644 --- a/packages/calypso-e2e/src/lib/pages/editor-page.ts +++ b/packages/calypso-e2e/src/lib/pages/editor-page.ts @@ -153,7 +153,7 @@ export class EditorPage { // Lacking a perfect cross-site type (Simple/Atomic) way to check the loading state, // it is a fairly good stand-in. await Promise.all( [ - this.page.waitForURL( /(\/post\/.+|\/page\/+|\/post-new.php)/, { timeout } ), + this.page.waitForURL( /(\/post\/.+|\/page\/+|\/post-new.php|\/post.php+)/, { timeout } ), this.page.waitForResponse( /.*posts.*/, { timeout } ), ] ); } diff --git a/packages/calypso-e2e/src/lib/pages/pages-page.ts b/packages/calypso-e2e/src/lib/pages/pages-page.ts index 6981de34b0ab39..4d2834b5a92106 100644 --- a/packages/calypso-e2e/src/lib/pages/pages-page.ts +++ b/packages/calypso-e2e/src/lib/pages/pages-page.ts @@ -32,7 +32,10 @@ export class PagesPage { async addNewPage(): Promise< void > { await Promise.all( [ this.page.waitForNavigation(), - this.page.getByRole( 'link', { name: /(Add new|Start a) page/ } ).click(), + this.page + .getByRole( 'link', { name: /Add New Page/ } ) + .first() + .click(), ] ); } } diff --git a/packages/calypso-e2e/src/lib/pages/posts-page.ts b/packages/calypso-e2e/src/lib/pages/posts-page.ts index 01508d9d629e79..191c4ebd1f7fa8 100644 --- a/packages/calypso-e2e/src/lib/pages/posts-page.ts +++ b/packages/calypso-e2e/src/lib/pages/posts-page.ts @@ -1,24 +1,27 @@ import { Page, Response } from 'playwright'; import { getCalypsoURL } from '../../data-helper'; -import { reloadAndRetry, clickNavTab } from '../../element-helper'; +import { reloadAndRetry } from '../../element-helper'; type TrashedMenuItems = 'Restore' | 'Copy link' | 'Delete Permanently'; type GenericMenuItems = 'Trash'; type MenuItems = TrashedMenuItems | GenericMenuItems; -type PostsPageTabs = 'Published' | 'Drafts' | 'Scheduled' | 'Trashed'; +type PostsPageTabs = 'Published' | 'Drafts' | 'Scheduled' | 'Trash'; const selectors = { // General - placeholder: `div.is-placeholder`, - addNewPostButton: 'a.post-type-list__add-post', + addNewPostButton: 'a.page-title-action, span.split-page-title-action>a', // Post Item - postItem: ( title: string ) => `div.post-item:has([data-e2e-title="${ title }"])`, + postRow: 'tr.type-post', + postItem: ( title: string ) => + `a.row-title:has-text("${ title }"), strong>span:has-text("${ title }")`, - // Menu - menuToggleButton: 'button[title="Toggle menu"]', - menuItem: ( item: string ) => `button[role="menuitem"]:has-text("${ item }")`, + // Status Filter + statusItem: ( item: string ) => `ul.subsubsub a:has-text("${ item }")`, + + // Actions + actionItem: ( item: string ) => `.row-actions a:has-text("${ item }")`, }; /** @@ -42,38 +45,24 @@ export class PostsPage { * Example {@link https://wordpress.com/posts} */ async visit(): Promise< Response | null > { - const response = await this.page.goto( getCalypsoURL( 'posts' ) ); - await this.waitUntilLoaded(); - return response; + return await this.page.goto( getCalypsoURL( 'posts' ) ); } /** - * Clicks on the navigation tab (desktop) or dropdown (mobile). + * Clicks on the navigation tab. * * @param {string} name Name of the tab to click. * @returns {Promise} No return value. */ async clickTab( name: PostsPageTabs ): Promise< void > { - // Without waiting for the `networkidle` event to fire, the clicks on the - // mobile navbar dropdowns are swallowed up. - await this.page.waitForLoadState( 'networkidle', { timeout: 20 * 1000 } ); - - await clickNavTab( this.page, name ); - await this.waitUntilLoaded(); + const locator = this.page.locator( selectors.statusItem( name ) ); + await locator.click(); } /* Page readiness */ - /** - * Wait until the page is completely loaded. - */ - async waitUntilLoaded(): Promise< void > { - await this.page.waitForSelector( selectors.placeholder, { state: 'detached' } ); - } - /** * Ensures the post item denoted by the parameter `title` is shown on the page. - * This method is a superset of the `waitUntilLoaded` method. * * Due to a race condition, sometimes the expected post does not appear * on the list of posts. This can occur when state for multiple posts are being modified @@ -82,15 +71,13 @@ export class PostsPage { * @param {string} title Post title. */ private async ensurePostShown( title: string ): Promise< void > { - await this.waitUntilLoaded(); - /** * Closure to wait until the post to appear in the list of posts. * * @param {Page} page Page object. */ async function waitForPostToAppear( page: Page ): Promise< void > { - const postLocator = page.locator( selectors.postItem( title ) ); + const postLocator = page.locator( `${ selectors.postRow } ${ selectors.postItem( title ) }` ); await postLocator.waitFor( { state: 'visible', timeout: 20 * 1000 } ); } @@ -103,7 +90,7 @@ export class PostsPage { async newPost(): Promise< void > { const locator = this.page.locator( selectors.addNewPostButton ); await Promise.all( [ - this.page.waitForNavigation( { url: /post/, timeout: 20 * 1000 } ), + this.page.waitForNavigation( { url: /post-new.php/, timeout: 20 * 1000 } ), locator.click(), ] ); } @@ -119,37 +106,37 @@ export class PostsPage { async clickPost( title: string ): Promise< void > { await this.ensurePostShown( title ); - const locator = this.page.locator( selectors.postItem( title ) ); + const locator = this.page.locator( `${ selectors.postRow } ${ selectors.postItem( title ) }` ); await locator.click(); } /** - * Toggles the Post Menu (hamberger menu) of a matching post. + * Toggles the Post Actions of a matching post. * - * @param {string} title Post title on which the menu should be toggled. + * @param {string} title Post title on which the actions should be toggled. */ - async togglePostMenu( title: string ): Promise< void > { + async togglePostActions( title: string ): Promise< void > { await this.ensurePostShown( title ); - const locator = this.page.locator( - `${ selectors.postItem( title ) } ${ selectors.menuToggleButton }` - ); - await locator.click(); + const locator = this.page.locator( selectors.postRow, { + has: this.page.locator( selectors.postItem( title ) ), + } ); + await locator.hover(); } /* Menu actions */ /** - * Given a post title and target menu item, performs the following actions: + * Given a post title and target action item, performs the following actions: * - locate the post with matching title. - * - toggle the post menu. - * - click on an menu action with matching name. + * - toggle the post action. + * - click on an action with matching name. * * @param param0 Object parameter. * @param {string} param0.title Title of the post. * @param {MenuItems} param0.action Name of the target action in the menu. */ - async clickMenuItemForPost( { + async clickActionItemForPost( { title, action, }: { @@ -158,34 +145,20 @@ export class PostsPage { } ): Promise< void > { await this.ensurePostShown( title ); - await this.togglePostMenu( title ); - await this.clickMenuItem( action ); + await this.togglePostActions( title ); + await this.clickActionItem( title, action ); } /** - * Clicks on the menu item. + * Clicks on the action item. * + * @param {string} title Title of the post. * @param {string} menuItem Target menu item. */ - private async clickMenuItem( menuItem: string ): Promise< void > { - const locator = this.page.locator( selectors.menuItem( menuItem ) ); - - // {@TODO} In the future, a possible idea may be to implement a following structure: - // pre-process - // perform the menu click - // post-process - // This is because sometimes the action performed on the menu may require additional - // pre- and post-processing, such as in the case of Delete Permanently. - // The pre-process and post-process actions are to be called through either a - // case-switch statement, or by locating and exeucting predefined function in - // an dictionary object, keyed by the value of menuItem. - - if ( menuItem === 'Delete Permanently' ) { - this.page.once( 'dialog', async ( dialog ) => { - await dialog.accept(); - } ); - } - - await locator.click(); + private async clickActionItem( title: string, menuItem: string ): Promise< void > { + const locator = this.page.locator( selectors.postRow, { + has: this.page.locator( selectors.postItem( title ) ), + } ); + await locator.locator( selectors.actionItem( menuItem ) ).click(); } } diff --git a/test/e2e/specs/editor/editor__post-advanced-flow.ts b/test/e2e/specs/editor/editor__post-advanced-flow.ts index 6bf49bcc0d0179..68783dd3a10dfa 100644 --- a/test/e2e/specs/editor/editor__post-advanced-flow.ts +++ b/test/e2e/specs/editor/editor__post-advanced-flow.ts @@ -10,7 +10,7 @@ import { TestAccount, PostsPage, ParagraphBlock, - NoticeComponent, + WpAdminNoticeComponent, getTestAccountByFeature, envToFeatureKey, ElementHelper, @@ -177,30 +177,30 @@ describe( `Editor: Advanced Post Flow`, function () { it( 'Trash post', async function () { await postsPage.clickTab( 'Drafts' ); - await postsPage.clickMenuItemForPost( { title: postTitle, action: 'Trash' } ); + await postsPage.clickActionItemForPost( { title: postTitle, action: 'Trash' } ); } ); it( 'Confirmation notice is shown', async function () { - const noticeComponent = new NoticeComponent( page ); - await noticeComponent.noticeShown( 'Post successfully moved to trash.', { - type: 'Success', + const noticeComponent = new WpAdminNoticeComponent( page ); + await noticeComponent.noticeShown( '1 post moved to the Trash.', { + type: 'Updated', } ); } ); } ); describe( 'Permanently delete post', function () { it( 'View trashed posts', async function () { - await postsPage.clickTab( 'Trashed' ); + await postsPage.clickTab( 'Trash' ); } ); it( 'Hard trash post', async function () { - await postsPage.clickMenuItemForPost( { title: postTitle, action: 'Delete Permanently' } ); + await postsPage.clickActionItemForPost( { title: postTitle, action: 'Delete Permanently' } ); } ); it( 'Confirmation notice is shown', async function () { - const noticeComponent = new NoticeComponent( page ); - await noticeComponent.noticeShown( 'Post successfully deleted', { - type: 'Success', + const noticeComponent = new WpAdminNoticeComponent( page ); + await noticeComponent.noticeShown( '1 post permanently deleted', { + type: 'Updated', } ); } ); } );