Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove duplicate views: Fix E2E tests #98279

Merged
merged 8 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading