Skip to content

Commit

Permalink
Remove duplicate views: Fix E2E tests (#98279)
Browse files Browse the repository at this point in the history
- Forces redirects from Calypso to WP Admin for the E2E tests
- Updates the E2E tests to use the WP Admin UI
  • Loading branch information
mmtr authored Jan 14, 2025
1 parent 80ac958 commit d96ba07
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 78 deletions.
3 changes: 2 additions & 1 deletion client/controller/index.web.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 );
Expand Down
1 change: 1 addition & 0 deletions packages/calypso-e2e/src/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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 } );
}
}
2 changes: 1 addition & 1 deletion packages/calypso-e2e/src/lib/pages/editor-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } ),
] );
}
Expand Down
5 changes: 4 additions & 1 deletion packages/calypso-e2e/src/lib/pages/pages-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
] );
}
}
103 changes: 38 additions & 65 deletions packages/calypso-e2e/src/lib/pages/posts-page.ts
Original file line number Diff line number Diff line change
@@ -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 }")`,
};

/**
Expand All @@ -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<void>} 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
Expand All @@ -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 } );
}

Expand All @@ -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(),
] );
}
Expand All @@ -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,
}: {
Expand All @@ -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();
}
}
20 changes: 10 additions & 10 deletions test/e2e/specs/editor/editor__post-advanced-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
TestAccount,
PostsPage,
ParagraphBlock,
NoticeComponent,
WpAdminNoticeComponent,
getTestAccountByFeature,
envToFeatureKey,
ElementHelper,
Expand Down Expand Up @@ -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',
} );
} );
} );
Expand Down

0 comments on commit d96ba07

Please sign in to comment.