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

Permafail E2E: fix Notifications spec. #78616

Merged
merged 3 commits into from
Jun 27, 2023
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
55 changes: 13 additions & 42 deletions packages/calypso-e2e/src/lib/components/notifications-component.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import { Page } from 'playwright';
import { Locator, Page } from 'playwright';

const selectors = {
// Notifications panel (including sub-panels)
activeSingleViewPanel: '.wpnc__single-view.wpnc__current',
notification: ( text: string ) => `.wpnc__comment:has-text("${ text }")`,

// Comment actions
commentAction: ( action: string ) => `button.wpnc__action-link:has-text("${ action }"):visible`,
undoLocator: '.wpnc__undo-item',
undoLink: '.wpnc__undo-link',
};
/**
* Component representing the notifications panel and notifications themselves.
*/
export class NotificationsComponent {
private page: Page;
private anchor: Locator;

/**
* Constructs an instance of the component.
Expand All @@ -23,6 +14,8 @@ export class NotificationsComponent {
*/
constructor( page: Page ) {
this.page = page;
// There is no accessible locator for this panel.
this.anchor = page.locator( 'div[id=wpnc-panel]' );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something I've yet to codify in the E2E documentation but follows the precedent set in WordPress/gutenberg (example).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you meant. TIL about the full accessibility tree in Chrome DevTools!

}

/**
Expand All @@ -31,51 +24,29 @@ export class NotificationsComponent {
* @param {string} text Text contained in the notification.
* @returns {Promise<void>} No return value.
*/
async clickNotification( text: string ): Promise< void > {
await this.page.click( selectors.notification( text ) );
async openNotification( text: string ): Promise< void > {
await this.anchor.getByText( text ).click();
}

/**
* Given a string of text, click on the button in expanded single notification view to execute the action.
*
* eg. 'Trash' -> Clicks on the 'Trash' button when viewing a single notification.
*
* @param {string} action Predefined list of strings that are accepted.
* @returns {Promise<void>} No return value.
* @param {string} action Action to perform on the notification.
*/
async clickNotificationAction( action: string ): Promise< void > {
// we need to make sure we're in a specific notification view before proceeding with the individual action
const elementHandle = await this.page.waitForSelector( selectors.activeSingleViewPanel );
await elementHandle.waitForElementState( 'stable' );
await this.page.click( selectors.commentAction( action ) );
await this.anchor
.getByRole( 'list' )
.getByRole( 'button', { name: action, exact: true } )
.click();
}

/**
* Clicks the undo link to undo the previous action.
*
* @returns {Promise<void>} No return value.
*/
async clickUndo(): Promise< void > {
await this.waitForUndoMessage();
await this.page.click( selectors.undoLink );
}

/**
* Waits for undo message to appear
*
* @returns {Promise<void>} No return value.
*/
async waitForUndoMessage(): Promise< void > {
await this.page.waitForSelector( selectors.undoLocator );
}

/**
* Waits for undo message to disappear
*
* @returns {Promise<void>} No return value.
*/
async waitForUndoMessageToDisappear(): Promise< void > {
await this.waitForUndoMessage();
await this.page.waitForSelector( selectors.undoLocator, { state: 'hidden' } );
await this.anchor.getByRole( 'button', { name: 'Undo', exact: true } ).click();
await this.anchor.getByText( 'Comment trashed' ).waitFor( { state: 'detached' } );
}
}
10 changes: 9 additions & 1 deletion packages/calypso-e2e/src/lib/test-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EmailClient } from '../email-client';
import envVariables from '../env-variables';
import { SecretsManager } from '../secrets';
import { TOTPClient } from '../totp-client';
import { SidebarComponent } from './components/sidebar-component';
import { LoginPage } from './pages/login-page';
import type { TestAccountCredentials } from '../secrets';

Expand All @@ -34,7 +35,10 @@ export class TestAccount {
* @param {Page} page Page object.
* @param {string} [url] URL to expect once authenticated and redirections are finished.
*/
async authenticate( page: Page, { url }: { url?: string | RegExp } = {} ): Promise< void > {
async authenticate(
page: Page,
{ url, waitUntilStable }: { url?: string | RegExp; waitUntilStable?: boolean } = {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change and the accompanying changes in Line 57-60 are important for stable loading of specs that rely on interaction in the My Home page.

Because Calypso loads so slowly but Playwright executes at lightning speed, we can often end up in a situation where Calypso "swallows" interactions on the page. To work around this issue, we can wait until the page loads entirely, but on Simple sites, this is often in excess of 15-20 seconds.

However, the Sidebar component is rendered and ready to interact towards the middle-later portions of the loading process. Waiting for the Sidebar component to become ready is suitable 98% of the time.

): Promise< void > {
const browserContext = page.context();
await browserContext.clearCookies();

Expand All @@ -50,6 +54,10 @@ export class TestAccount {
if ( url ) {
await page.waitForURL( url, { timeout: 20 * 1000 } );
}
if ( waitUntilStable ) {
const sidebarComponent = new SidebarComponent( page );
await sidebarComponent.waitForSidebarInitialization();
}
}

/**
Expand Down
Binary file modified packages/calypso-e2e/src/secrets/encrypted.enc
Binary file not shown.
129 changes: 0 additions & 129 deletions test/e2e/specs/infrastructure/notification__actions.ts

This file was deleted.

130 changes: 130 additions & 0 deletions test/e2e/specs/infrastructure/notifications__general-interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @group calypso-pr
*/

import {
DataHelper,
NavbarComponent,
NotificationsComponent,
RestAPIClient,
TestAccount,
NewCommentResponse,
PostResponse,
} from '@automattic/calypso-e2e';
import { Page, Browser } from 'playwright';

declare const browser: Browser;

/**
* Tests general interaction with the notification panel, running through
* all actions once.
*/
describe( 'Notifications: General Interactions', function () {
const comment = DataHelper.getRandomPhrase() + ' notification-actions-spec';

// TestAccount and RestAPI instances.
let commentingUser: TestAccount;
let notificationsUser: TestAccount;
let commentingUserRestAPIClient: RestAPIClient;
let notificationUserRestAPIClient: RestAPIClient;

// API responses.
let newPost: PostResponse;
let newComment: NewCommentResponse;

let notificationsComponent: NotificationsComponent;
let page: Page;

beforeAll( async function () {
// Create an instance of RestAPI as the user making the comment.
commentingUser = new TestAccount( 'commentingUser' );
commentingUserRestAPIClient = new RestAPIClient( commentingUser.credentials );

// Create an instance of RestAPI as the user receiving notification.
notificationsUser = new TestAccount( 'notificationsUser' );
notificationUserRestAPIClient = new RestAPIClient( notificationsUser.credentials );

// Create a new post and store the response.
newPost = await notificationUserRestAPIClient.createPost(
notificationsUser.credentials.testSites?.primary.id as number,
{ title: DataHelper.getRandomPhrase() }
);

// Create a new comment on the post as the commentingUser and
// store the response.
newComment = await commentingUserRestAPIClient.createComment(
notificationsUser.credentials.testSites?.primary.id as number,
newPost.ID,
comment
);

// Log in as the user receiving the notification.
page = await browser.newPage();
await notificationsUser.authenticate( page, { waitUntilStable: true } );
} );

it( 'Open Notifications panel', async function () {
const navbarComponent = new NavbarComponent( page );
await navbarComponent.openNotificationsPanel();
} );

it( 'Click notification for the comment', async function () {
notificationsComponent = new NotificationsComponent( page );
await notificationsComponent.openNotification( comment );
} );

it( 'Approve comment', async function () {
await notificationsComponent.clickNotificationAction( 'Approve' );
} );

it( 'Like comment', async function () {
await notificationsComponent.clickNotificationAction( 'Like' );
} );

it( 'Mark comment as spam', async function () {
await notificationsComponent.clickNotificationAction( 'Spam' );
await notificationsComponent.clickUndo();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undo the action to mark the comment as spam so:

  1. we don't need to create another comment to perform the Trash action, and;
  2. we don't inadvertently train the backend systems to consider commentingUser as a spammer.

} );

it( 'Trash comment', async function () {
await notificationsComponent.clickNotificationAction( 'Trash' );
} );

afterAll( async function () {
if ( ! newComment ) {
return;
}

// Clean up the comment.
try {
await notificationUserRestAPIClient.deleteComment(
notificationsUser.credentials.testSites?.primary.id as number,
newComment.ID
);
} catch ( e: unknown ) {
console.warn(
`Failed to clean up test comment in notification_action spec for site ${
notificationsUser.credentials.testSites?.primary.id as number
}, comment ${ newComment.ID }`
);
}

if ( ! newPost ) {
return;
}

// Clean up the post.
try {
await notificationUserRestAPIClient.deletePost(
notificationsUser.credentials.testSites?.primary.id as number,
newPost.ID
);
} catch ( e: unknown ) {
console.warn(
`Failed to clean up test comment in notification_action spec for site ${
notificationsUser.credentials.testSites?.primary.id as number
}, comment ${ newComment.ID }`
);
}
} );
} );