Skip to content
Draft
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
138 changes: 138 additions & 0 deletions e2e/site-navigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { test, expect, Page } from '@playwright/test';
import fs from 'fs-extra';
import { E2ESession } from './e2e-helpers';
import Onboarding from './page-objects/onboarding';
import SiteContent from './page-objects/site-content';
import WhatsNewModal from './page-objects/whats-new-modal';
import { getUrlWithAutoLogin } from './utils';

/**
* Closes the WordPress Block Editor welcome guide if it appears.
* Attempts to close up to 3 times as the modal can appear multiple times.
*/
async function closeWelcomeGuide( page: Page ) {
// Try to close the modal up to 3 times
for ( let i = 0; i < 2; i++ ) {
Copy link

Choose a reason for hiding this comment

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

Bug: Loop counter mismatch

The comment on line 10 states "Attempts to close up to 3 times" but the loop condition is i < 2, which only allows 2 iterations (i=0 and i=1).

Suggested change
for ( let i = 0; i < 2; i++ ) {
for ( let i = 0; i < 3; i++ ) {

Either update the loop to i < 3 or update the comment to say "up to 2 times".

try {
// Wait for the modal frame to appear
const modalFrame = page.locator( '.components-modal__frame' );
await modalFrame.waitFor( { state: 'visible', timeout: 2000 } );

// Find and click the close button using specific selector
const closeButton = page.locator( '.components-modal__header > button[aria-label="Close"]' );
await closeButton.waitFor( { state: 'visible', timeout: 2000 } );
await closeButton.click();
} catch ( e ) {
// Modal not found or already closed, exit the loop
break;
}
}
Comment on lines +25 to +29
Copy link

Choose a reason for hiding this comment

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

Code Quality: Empty catch block loses error context

The catch block silently swallows the error without logging. While this may be intentional for the modal not appearing, it makes debugging harder if there's an actual error.

Consider logging at debug level:

		} catch ( e ) {
			// Modal not found or already closed, exit the loop
			// Note: This is expected when the modal doesn't appear
			break;
		}

Or if you have a logger, use it for visibility during test debugging.

}

test.describe( 'Site Navigation', () => {
const session = new E2ESession();

const siteName = 'My WordPress Website'; // Use the default site created during onboarding

let frontendUrl: string;
let wpAdminUrl: string;

test.beforeAll( async () => {
await session.launch();

// Complete onboarding before tests
const onboarding = new Onboarding( session.mainWindow );
await expect( onboarding.heading ).toBeVisible();
await onboarding.continueButton.click();

const whatsNewModal = new WhatsNewModal( session.mainWindow );
if ( await whatsNewModal.locator.isVisible( { timeout: 5000 } ) ) {
await whatsNewModal.closeButton.click();
}

// Wait for default site to be ready and get URLs
const siteContent = new SiteContent( session.mainWindow, siteName );
await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } );

// Get site URLs for tests
const settingsTab = await siteContent.navigateToTab( 'Settings' );
wpAdminUrl = await settingsTab.copyWPAdminUrlToClipboard( session.electronApp );
frontendUrl = await settingsTab.copySiteUrlToClipboard( session.electronApp );
} );

test.afterAll( async () => {
await session.cleanup();
} );

test( 'opens site at homepage', async ( { page } ) => {
// Navigate to the site homepage
await page.goto( frontendUrl );

// Verify the page loaded successfully
await expect( page ).toHaveURL( frontendUrl );

// Check for WordPress indicators (body classes, meta tags, etc.)
const bodyClass = await page.locator( 'body' ).getAttribute( 'class' );
expect( bodyClass ).toMatch( /wordpress|wp-|home/ );

// Verify page title exists
const title = await page.title();
expect( title ).toBeTruthy();
expect( title.length ).toBeGreaterThan( 0 );
} );

test( 'opens and automatically logs in to WP Admin', async ( { page } ) => {
// Navigate to wp-admin with auto-login
await page.goto( getUrlWithAutoLogin( wpAdminUrl ) );

// Verify we're on the dashboard
await expect( page ).toHaveURL( /wp-admin/ );

// Check for dashboard elements
await expect( page.locator( '#wpadminbar' ) ).toBeVisible();
await expect( page.locator( '#adminmenuback' ) ).toBeVisible();

// Verify we're logged in by checking for user menu
const userMenu = page.locator( '#wp-admin-bar-my-account' );
await expect( userMenu ).toBeVisible();
} );

test( 'creates a post', async ( { page } ) => {
// Navigate to new post page
const newPostUrl = `${ wpAdminUrl }/post-new.php`;
Copy link

Choose a reason for hiding this comment

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

Cross-Platform: Path concatenation

While string concatenation works here for URLs, consider using new URL() for consistency and to avoid potential issues:

		const newPostUrl = new URL( '/wp-admin/post-new.php', wpAdminUrl ).toString();

This ensures proper URL handling even if wpAdminUrl has trailing slashes or other edge cases.

await page.goto( getUrlWithAutoLogin( newPostUrl ) );

const editorFrame = page.frameLocator( 'iframe[name="editor-canvas"]' );

// Close welcome guide if it appears (always on main page, not in iframe)
await closeWelcomeGuide( page );

// Wait for title to be available in iframe
const titleSelector = 'h1.editor-post-title';
await editorFrame.locator( titleSelector ).waitFor( { timeout: 30_000 } );
Copy link

Choose a reason for hiding this comment

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

Performance: Long timeout

The 30-second timeout seems excessive for an iframe element to load. Consider:

  1. Reducing to 15-20 seconds if this typically loads faster
  2. Adding a comment explaining why 30s is needed (e.g., slow CI environments)
		// Wait for title to be available in iframe (long timeout for slower CI environments)
		await editorFrame.locator( titleSelector ).waitFor( { timeout: 30_000 } );

await editorFrame.locator( titleSelector ).fill( 'E2E Test Post' );

// Click into the content area and type
await editorFrame.locator( titleSelector ).press( 'Enter' );
const contentBlock = editorFrame.locator( 'p[role="document"]' ).first();
await contentBlock.click();
await contentBlock.fill( 'This is a test post created by automated E2E tests.' );

// Publish the post (publish buttons are on main page)
const publishButton = page.locator( 'button.editor-post-publish-button__button' ).first();
await publishButton.waitFor( { state: 'visible', timeout: 10_000 } );
await publishButton.click();
Comment on lines +122 to +124
Copy link

Choose a reason for hiding this comment

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

Flaky Test Risk: Selector fragility

Using .first() on a selector that matches multiple buttons is fragile. If WordPress changes the order or adds more buttons, this could break or click the wrong button.

Consider more specific selectors:

		const publishButton = page.locator( '.editor-header__settings button.editor-post-publish-button__button' );

Or use a more semantic selector like getByRole('button', { name: /publish/i }) if the button text is stable.


// Wait for and click the confirm publish button in the panel
const confirmPublishButton = page.locator( 'button.editor-post-publish-button__button' ).last();
Copy link

Choose a reason for hiding this comment

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

Flaky Test Risk: Using .last() is fragile

Similar to the previous comment, using .last() is brittle. The "confirm publish" button should have a more specific selector or be located within the publish panel:

		const confirmPublishButton = page.locator( '.editor-post-publish-panel button.editor-post-publish-button__button' );

await confirmPublishButton.waitFor( { state: 'visible', timeout: 10_000 } );
await confirmPublishButton.click();

// Wait for success message
await expect( page.locator( '.components-snackbar' ) ).toBeVisible( { timeout: 10_000 } );

// Verify post was created by visiting posts list
await page.goto( getUrlWithAutoLogin( `${ wpAdminUrl }/edit.php` ) );
Copy link

Choose a reason for hiding this comment

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

Cross-Platform: URL construction

Same as the earlier comment - consider using new URL():

		await page.goto( getUrlWithAutoLogin( new URL( '/wp-admin/edit.php', wpAdminUrl ).toString() ) );

await expect( page.locator( 'a.row-title:has-text("E2E Test Post")' ) ).toBeVisible();
} );
Comment on lines +100 to +137
Copy link

Choose a reason for hiding this comment

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

Test Coverage: Missing cleanup

This test creates a post but doesn't clean it up afterward. Consider one of these approaches:

  1. Add cleanup: Delete the post after verification
  2. Use a unique identifier: Add timestamp/UUID to post title to avoid conflicts in repeated runs
  3. Document the behavior: Add a comment explaining that test posts accumulate

Since this is an E2E test and the entire site is ephemeral (created per test session), this may be acceptable, but it's worth making the decision explicit.

} );