From eba4f5f1c1a22cea52c6605838edea556ef8c74d Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Oct 2026 13:52:43 +0200 Subject: [PATCH 1/3] Redirect to the main screen on every activation The redirect used to be set only on the first activation, before the site had a BlogVault account. A bulk activation now stays on the plugin list. Co-Authored-By: Claude Opus 5.5 --- plugin/wp_actions.php | 2 ++ plugin/wp_admin.php | 7 +++-- tests/e2e/blueprint-menu.json | 8 +++++ tests/e2e/menu-steps.php | 55 +++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/plugin/wp_actions.php b/plugin/wp_actions.php index e9b69c05..b9685e8e 100644 --- a/plugin/wp_actions.php +++ b/plugin/wp_actions.php @@ -27,6 +27,8 @@ public function activate() { } else { WPCOMAccount::setup($this->settings); } + // Every activation leads to the migration screen, not only the first. + $this->settings->updateOption($this->bvinfo->plug_redirect, 'yes'); } public function deactivate() { diff --git a/plugin/wp_admin.php b/plugin/wp_admin.php index 487f2028..b6557df4 100755 --- a/plugin/wp_admin.php +++ b/plugin/wp_admin.php @@ -61,8 +61,11 @@ public function initHandler() { if ($this->bvinfo->isActivateRedirectSet()) { $this->settings->updateOption($this->bvinfo->plug_redirect, 'no'); - if (!wp_doing_ajax()) { - wp_redirect($this->migrationScreenUrl()); + // A bulk activation lands on the plugin list; it stays there. + if (!wp_doing_ajax() && !isset($_GET['activate-multi'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if (wp_redirect($this->migrationScreenUrl())) { + exit; + } } } } diff --git a/tests/e2e/blueprint-menu.json b/tests/e2e/blueprint-menu.json index 49863bfb..3d126fbb 100644 --- a/tests/e2e/blueprint-menu.json +++ b/tests/e2e/blueprint-menu.json @@ -42,6 +42,14 @@ "step": "runPHP", "code": " array( 'wpcom-migration' => 1 ) ) ); + add_filter( + 'pre_http_request', + function () { + return array( + 'headers' => array(), + 'body' => '', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + ); + } + ); + $plugin = 'wpcom-migration/wpcom_migration.php'; + deactivate_plugins( $plugin, true ); + update_option( 'wpcomredirect', 'no' ); + $result = activate_plugin( $plugin ); + delete_option( 'bvAccountsList' ); + if ( is_wp_error( $result ) ) { + throw new RuntimeException( 'Reactivating failed: ' . $result->get_error_message() ); + } + if ( 'yes' !== get_option( 'wpcomredirect' ) ) { + throw new RuntimeException( 'Reactivating should ask for the redirect again.' ); + } + update_option( 'wpcomredirect', 'no' ); + break; + + case 'bulk-activation-stays': + $captured = null; + add_filter( + 'wp_redirect', + function ( $location ) use ( &$captured ) { + $captured = $location; + return ''; + } + ); + update_option( 'wpcomredirect', 'yes' ); + $_GET['activate-multi'] = 'true'; + + $admin = new WPCOMWPAdmin( new WPCOMWPSettings(), new WPCOMWPSiteInfo() ); + $admin->initHandler(); + + if ( null !== $captured ) { + throw new RuntimeException( 'A bulk activation should stay on the plugin list, got: ' . $captured ); + } + if ( 'no' !== get_option( 'wpcomredirect' ) ) { + throw new RuntimeException( 'A bulk activation should use up the redirect.' ); + } + break; + case 'multisite-subsite': // The plugin's entry points on a network lead to the old screen // in the network admin; a subsite has no row, since the Reprint From 22ca4df0251234c74ced32519b70d67c7e509a52 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Oct 2026 13:52:43 +0200 Subject: [PATCH 2/3] Apply the design tweaks to the Reprint migration screen A wider column without the logo header, a status pill above the heading on the connected, set-up and ready screens, and new text per mode. The WordPress.com button reads "Go to WordPress.com", carries an arrow, and links to my.wordpress.com/sites. The login button reads "Connect WordPress.com". Co-Authored-By: Claude Opus 5.5 --- README.md | 18 ++-- plugin/connection/class-connect-page.php | 57 ++++-------- plugin/reprint/class-settings-page.php | 111 ++++++++++------------- plugin/reprint/settings-page.css | 83 ++++++++++------- tests/e2e/blueprint-connection.json | 4 - tests/e2e/connection-steps.php | 54 ++++------- tests/e2e/screen-steps.php | 49 +++++----- 7 files changed, 174 insertions(+), 202 deletions(-) diff --git a/README.md b/README.md index 8dfd95f0..91e59b2f 100644 --- a/README.md +++ b/README.md @@ -26,18 +26,20 @@ To activate a source checkout directly (without a build), run `composer install ## The Reprint migration screen -`wp-admin/admin.php?page=wpcom-migration-status` (the plugin's one menu entry; `manage_options` to view; the `administrator` role to connect, disconnect, or provision through the routes; single-site only). It carries the old main screen's design: the WordPress mark, a serif heading and one centred column. The screen shows one mode at a time — one sentence saying where the site stands, at most one button, and the links that fit: +`wp-admin/admin.php?page=wpcom-migration-status` (the plugin's one menu entry; `manage_options` to view; the `administrator` role to connect, disconnect, or provision through the routes; single-site only). The screen shows one mode at a time: | Mode | When | Shows | |---|---|---| | The exporter runs on single sites only | Multisite | Nothing | -| The export credential no longer matches this site | The site's salts changed | *Log in with WordPress.com*, or *Continue on WordPress.com* when connected | -| The exporter is on until *time* | A migration is running | *Continue on WordPress.com* and *Disconnect*, when connected | -| Connected as *email* | Logged in; the migration has not started | *Continue on WordPress.com*, a *Disconnect* link | -| This site is set up and ready | Provisioned with an application password | A *Log in with WordPress.com* link | -| Log in with your WordPress.com account | Fresh site | *Log in with WordPress.com* | +| The export credential no longer matches this site | The site's salts changed | *Connect WordPress.com*, or *Go to WordPress.com* and *Disconnect* when connected | +| Pill: *Site ready to migrate* | The exporter is on | *Go to WordPress.com* | +| Pill: *Connected to WordPress.com* | Logged in; the exporter is off | *Go to WordPress.com*, a *Disconnect* link | +| Pill: *Set up by WordPress.com* | Provisioned with an application password; the exporter is off | *Go to WordPress.com* | +| Log in with your WordPress.com account to connect this site | Fresh site | *Connect WordPress.com* | -`Settings_Page::mode()` picks the first that fits, top to bottom. The *Continue* link's target is filtered by `wpcom_migration_continue_url`. +`Settings_Page::mode()` picks the first that fits, top to bottom. *Go to WordPress.com* links to `https://my.wordpress.com/sites`; the `wpcom_migration_continue_url` filter changes that. + +Every activation sends the administrator to the main screen on their next wp-admin page load, except a bulk activation, which stays on the plugin list. The old BlogVault screen at `wp-admin/admin.php?page=wpcom-migration` is no longer in the sidebar. It stays reachable at its URL. @@ -57,7 +59,7 @@ Which credential WordPress.com sets up depends on the host. Where `openssl_verif **Application password** — where `GET /wp-json/` lists `authentication.application-passwords` (core: `is_ssl()` true and no plugin has switched them off). WordPress.com sends the administrator once to `wp-admin/authorize-application.php?app_name=Migrate+to+WordPress.com&app_id=&success_url=…&reject_url=…`; core redirects to `success_url` with `site_url`, `user_login` and `password`. WordPress.com then checks the password with `GET /wp-json/wp/v2/users/me` (a 401 here means the server strips the `Authorization` header — Apache CGI without the rewrite rule WordPress 5.6 added; saving Permalinks regenerates it), installs and activates the plugin (`POST /wp-json/wp/v2/plugins {"slug":"wpcom-migration","status":"active"}`; an older copy is replaced by `PUT …/plugins/wpcom-migration/wpcom_migration {"status":"inactive"}`, `DELETE`, then the `POST`, since core has no update route), calls the two routes, exports, and revokes the password (`GET …/users/me/application-passwords/introspect`, then `DELETE …/application-passwords/`). No login on the site. -**Jetpack connection** — everywhere else, including sites where Wordfence or another security plugin has disabled application passwords. The administrator installs the plugin from the plugin directory and presses *Log in with WordPress.com* on the screen; the connection package registers the site and sends them to WordPress.com to authorize, then back. A site that already runs Jetpack is connected at once for the account that connected Jetpack — the connection is shared. WordPress.com then calls the routes signed with the user token. +**Jetpack connection** — everywhere else, including sites where Wordfence or another security plugin has disabled application passwords. The administrator installs the plugin from the plugin directory and presses *Connect WordPress.com* on the screen; the connection package registers the site and sends them to WordPress.com to authorize, then back. A site that already runs Jetpack is connected at once for the account that connected Jetpack — the connection is shared. WordPress.com then calls the routes signed with the user token. Connection events fire `wpcom_migration_connection_event`, exporter events `wpcom_migration_reprint_export_event`; none carries a token, secret or signature. diff --git a/plugin/connection/class-connect-page.php b/plugin/connection/class-connect-page.php index 42350b44..d4bd8a45 100644 --- a/plugin/connection/class-connect-page.php +++ b/plugin/connection/class-connect-page.php @@ -180,25 +180,23 @@ private function redirect_with_notice( $result, $code = '' ) { } /** - * Renders the login form: the screen's primary button, or a plain link. - * - * @param bool $primary Whether the submit is the primary button. + * Renders the login form, with the terms it agrees to. */ - public function render_connect_button( $primary ) { + public function render_connect_button() { ?>

-

' . esc_html__( 'WordPress.com Terms of Service', 'wpcom-migration' ) . '' ); ?> @@ -208,16 +206,15 @@ public function render_connect_button( $primary ) { } /** - * Renders the link to WordPress.com: the screen's primary button, or a - * plain link. It starts nothing on this site. - * - * @param bool $primary Whether it is the primary button. + * Renders the link to WordPress.com as the screen's button. It starts + * nothing on this site. */ - public function render_continue_button( $primary ) { + public function render_continue_button() { ?>

- target="_top" href=""> - + + +

" />

- - - - + +

+

+

plugin_file ), - array( 'wpcom-migration-variables', 'wpcom-migration-fonts', 'dashicons' ), + array( 'wpcom-migration-variables', 'wpcom-migration-fonts' ), $version ); } @@ -220,8 +220,7 @@ public function remove_admin_notices() { } /** - * Renders the screen: the old main screen's design, with the mode where - * that screen had its email form. + * Renders the screen: one centred column holding the mode. */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { @@ -238,17 +237,9 @@ public function render_page() { echo ''; } ?> -
- -
-
-

-

- render_mode( $mode, $state, $user_connected ); ?> + render_mode( $mode, $user_connected ); ?>
connection_section; switch ( $mode ) { case self::MODE_BLOCKED: + $this->render_pill_and_heading(); $this->render_sentence( __( 'The exporter runs on single sites only, so this site can\'t be migrated from here.', 'wpcom-migration' ) ); return; case self::MODE_BROKEN: + $this->render_pill_and_heading(); $this->render_sentence( __( 'The export credential no longer matches this site — its security salts changed. Start the migration again on WordPress.com.', 'wpcom-migration' ) ); if ( null !== $section && $user_connected ) { - $section->render_continue_button( true ); + $section->render_continue_button(); $section->render_disconnect_link(); } elseif ( null !== $section ) { - $section->render_connect_button( true ); + $section->render_connect_button(); } return; case self::MODE_READY: - $this->render_sentence( - sprintf( - /* translators: %s: time of day. */ - __( 'The exporter is on until %s. The migration runs from WordPress.com; each export keeps it on for another hour.', 'wpcom-migration' ), - self::window_closes_at( $state ) - ) - ); - if ( null !== $section && $user_connected ) { - $section->render_continue_button( false ); - $section->render_disconnect_link(); + $this->render_pill_and_heading( __( 'Site ready to migrate', 'wpcom-migration' ), 'info' ); + $this->render_sentence( __( 'Your site is now ready to be copied by WordPress.com. Your site stays online and unchanged while it\'s copied.', 'wpcom-migration' ) ); + $this->render_sentence( __( 'Keep this plugin active until the migration is done. We email you when it\'s finished.', 'wpcom-migration' ) ); + if ( null !== $section ) { + $section->render_continue_button(); } return; case self::MODE_CONNECTED_WAITING: - $email = null !== $section ? \Automattic\WPCOM_Migration\Connect_Page::connected_email() : null; - if ( null !== $email ) { - $this->render_sentence_html( - sprintf( - /* translators: %s: WordPress.com account email address, in bold. */ - esc_html__( 'Connected as %s. WordPress.com sets up the exporter when the migration starts.', 'wpcom-migration' ), - '' . esc_html( $email ) . '' - ) - ); - } else { - $this->render_sentence( __( 'Connected to WordPress.com. WordPress.com sets up the exporter when the migration starts.', 'wpcom-migration' ) ); - } + $this->render_pill_and_heading( __( 'Connected to WordPress.com', 'wpcom-migration' ), 'success' ); + $this->render_ready_to_migrate(); if ( null !== $section ) { - $section->render_continue_button( true ); + $section->render_continue_button(); $section->render_disconnect_link(); } return; case self::MODE_PROVISIONED_WAITING: - $this->render_sentence_html( - '' . esc_html__( 'This site is set up and ready.', 'wpcom-migration' ) . ' ' - . esc_html__( 'The exporter stays off until the migration starts on WordPress.com.', 'wpcom-migration' ) - ); + $this->render_pill_and_heading( __( 'Set up by WordPress.com', 'wpcom-migration' ), 'info' ); + $this->render_ready_to_migrate(); if ( null !== $section ) { - $section->render_connect_button( false ); + $section->render_continue_button(); } return; case self::MODE_NEEDS_CONNECTING: default: - $this->render_sentence( __( 'Log in with your WordPress.com account so WordPress.com can read this site and migrate it. Nothing is copied until you start the migration there.', 'wpcom-migration' ) ); + $this->render_pill_and_heading(); + $this->render_sentence( __( 'WordPress.com copies your posts, pages, media, themes, plugins, and settings. Your site stays online and unchanged while we copy it.', 'wpcom-migration' ) ); + $this->render_sentence( __( 'Log in with your WordPress.com account to connect this site. Nothing is copied until you start the migration on WordPress.com.', 'wpcom-migration' ) ); if ( null !== $section ) { - $section->render_connect_button( true ); + $section->render_connect_button(); } return; } } /** - * Renders the mode's one sentence. + * Renders the heading, with a status pill above it when given one. * - * @param string $text Plain text. + * @param string $pill Pill text, or '' for none. + * @param string $tone success (green) or info (blue). */ - private function render_sentence( $text ) { - $this->render_sentence_html( esc_html( $text ) ); + private function render_pill_and_heading( $pill = '', $tone = 'info' ) { + if ( '' !== $pill ) { + echo '

' . esc_html( $pill ) . '

'; + } + echo '

' . esc_html__( 'Migrate your site to WordPress.com', 'wpcom-migration' ) . '

'; } /** - * Renders the mode's one sentence from markup the caller has escaped. - * - * @param string $html Escaped HTML. + * Renders the sentence the two waiting modes share. */ - private function render_sentence_html( $html ) { - echo '

' . $html . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by the caller. + private function render_ready_to_migrate() { + $this->render_sentence( + sprintf( + /* translators: %s: the site's address, without the scheme. */ + __( '%s is ready to migrate. The migration runs from WordPress.com, so there\'s nothing else to do here.', 'wpcom-migration' ), + untrailingslashit( preg_replace( '#^https?://#i', '', home_url() ) ) + ) + ); } /** - * The time of day the exporter turns itself off, in the site's format. + * Renders one paragraph of the mode. * - * @param array $state Exporter::get_state(), with the window open. - * @return string + * @param string $text Plain text. */ - private static function window_closes_at( array $state ) { - $expires = $state['window_expires_at'] + (int) ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); - return date_i18n( get_option( 'time_format' ), $expires ); + private function render_sentence( $text ) { + echo '

' . esc_html( $text ) . '

'; } } diff --git a/plugin/reprint/settings-page.css b/plugin/reprint/settings-page.css index 0f7df3be..18229ec7 100644 --- a/plugin/reprint/settings-page.css +++ b/plugin/reprint/settings-page.css @@ -1,5 +1,5 @@ /* - * The Reprint migration screen: the old main screen's design, one column. + * The Reprint migration screen: one centred column. * * variables.css supplies the palette, fonts.css the Recoleta face. The old * screen's style.css is deliberately not a dependency — it is that screen's @@ -19,29 +19,21 @@ body { display: none; } -.wpcom-migration-header { - display: flex; - padding: 20px; -} - -.wpcom-migration-header__wpcom-logo span { - width: 24px; - height: 24px; - font-size: 24px; -} - .wpcom-migration-container { - box-sizing: border-box; - max-width: 476px; + box-sizing: content-box; + max-width: 660px; margin: 0 auto; padding: 40px 20px; - font-size: 14px; - color: var( --wpcom-migration-gray-70 ); + font-size: 15px; + line-height: 24px; + color: var( --wpcom-migration-gray-100 ); text-underline-offset: 4px; } .wpcom-migration-container h1 { - margin: 0; + max-width: 440px; + margin: 0 0 16px; + padding: 0; color: var( --wpcom-migration-gray-100 ); font-family: var( --wpcom-migration-heading-font-family ); font-size: 34px; @@ -50,8 +42,9 @@ body { } .wpcom-migration-container p { - margin: 8px 0; + margin: 16px 0; font-size: inherit; + line-height: inherit; } .wpcom-migration-container a:not( .wpcom-migration-button ) { @@ -63,24 +56,45 @@ body { text-decoration: none; } -.wpcom-migration-guidance { - margin-top: 24px; +.wpcom-migration-container .wpcom-migration-pill { + display: inline-block; + margin: 0 0 16px; + padding: 2px 8px; + border-radius: 6px; + font-size: 12px; + line-height: 20px; +} + +.wpcom-migration-pill--success { + color: #00450c; + background: #b8f5c8; +} + +.wpcom-migration-pill--info { + color: #0b2a6f; + background: #dfe8fd; } .wpcom-migration-button { display: inline-block; - margin: 4px 0; - padding: 12px 24px; + margin: 0; + padding: 10px 12px; border: 0; - border-radius: 4px; + border-radius: 2px; color: var( --wpcom-migration-white ); background: var( --wpcom-migration-primary-bg ); - font-size: 14px; - line-height: 1.4; + font-size: 13px; + font-weight: normal; + line-height: 20px; text-decoration: none; cursor: pointer; } +.wpcom-migration-button__icon { + margin-left: 2px; + vertical-align: 0; +} + .wpcom-migration-container a.wpcom-migration-button:hover, .wpcom-migration-button:hover, .wpcom-migration-button:focus { @@ -93,18 +107,25 @@ body { border: 0; background: none; color: var( --wpcom-migration-primary-bg ); - font-size: inherit; + font-size: 13px; + line-height: 20px; text-decoration: underline; + text-underline-offset: 4px; cursor: pointer; } -.wpcom-migration-link--delete { - color: #b32d2e; +.wpcom-migration-link--quiet { + color: var( --wpcom-migration-gray-100 ); } -.wpcom-migration-description { +.wpcom-migration-link:hover { + color: var( --wpcom-migration-link-hover-color ); + text-decoration: none; +} + +.wpcom-migration-container .wpcom-migration-description { color: var( --wpcom-migration-gray-50 ); - font-size: 12px; + font-size: 13px; line-height: 20px; } @@ -115,7 +136,7 @@ body { @media ( min-width: 961px ) { .wpcom-migration-container { - padding-top: 100px; + padding-top: 148px; } .wpcom-migration-container h1 { diff --git a/tests/e2e/blueprint-connection.json b/tests/e2e/blueprint-connection.json index 4cdcfc2d..72cf6783 100644 --- a/tests/e2e/blueprint-connection.json +++ b/tests/e2e/blueprint-connection.json @@ -54,10 +54,6 @@ "step": "runPHP", "code": "', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'wpcom-migration-status', $step ); break; @@ -346,8 +345,8 @@ function ( $location ) { $_GET[ Connect_Page::CODE_QUERY_ARG ] = 'e2e_blocked'; $html = wpcom_migration_e2e_render_connect_page(); wpcom_migration_e2e_expect_contains( $html, 'could not register', $step ); - if ( strpos( $html, 'could not register' ) > strpos( $html, 'class="wpcom-migration-header"' ) ) { - throw new RuntimeException( "Step '$step': the notice should sit above the screen's header, where wp-admin puts notices." ); + if ( strpos( $html, 'could not register' ) > strpos( $html, 'class="wpcom-migration-container"' ) ) { + throw new RuntimeException( "Step '$step': the notice should sit above the screen's column, where wp-admin puts notices." ); } wpcom_migration_e2e_expect_contains( $html, 'e2e_blocked', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'Blocked by the e2e scenario', $step ); @@ -386,35 +385,22 @@ function ( $location ) { case 'render-connected': wpcom_migration_e2e_expect_mode( Settings_Page::MODE_CONNECTED_WAITING, $step ); $html = wpcom_migration_e2e_render_connect_page(); - wpcom_migration_e2e_expect_contains( $html, 'Connected as e2e@example.com', $step ); - if ( 1 !== substr_count( $html, 'Connected as e2e@example.com' ) ) { - throw new RuntimeException( "Step '$step': expected 'Connected as e2e@example.com' exactly once: " . substr( $html, 0, 400 ) ); - } - wpcom_migration_e2e_expect_contains( $html, 'sets up the exporter when the migration starts', $step ); - wpcom_migration_e2e_expect_contains( $html, 'Continue on WordPress.com', $step ); - wpcom_migration_e2e_expect_contains( $html, 'https://wordpress.com/setup/site-migration?from=' . rawurlencode( home_url() ), $step ); + wpcom_migration_e2e_expect_contains( $html, 'wpcom-migration-pill--success">Connected to WordPress.com

', $step ); + wpcom_migration_e2e_expect_contains( $html, untrailingslashit( preg_replace( '#^https?://#i', '', home_url() ) ) . ' is ready to migrate.', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'e2e@example.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Go to WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'href="https://my.wordpress.com/sites"', $step ); wpcom_migration_e2e_expect_primary_count( $html, 1, $step ); wpcom_migration_e2e_expect_contains( $html, 'value="' . Connect_Page::DISCONNECT_ACTION . '"', $step ); - wpcom_migration_e2e_expect_contains( $html, 'wpcom-migration-link--delete', $step ); + wpcom_migration_e2e_expect_contains( $html, 's connection to WordPress.com.', $step ); wpcom_migration_e2e_expect_not_contains( $html, (string) WPCOM_MIGRATION_E2E_BLOG_ID, $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Log in with WordPress.com', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'Connect WordPress.com', $step ); // The blog ID lives on the by-hand screen now. $manual_html = wpcom_migration_e2e_render_manual_page(); wpcom_migration_e2e_expect_contains( $manual_html, (string) WPCOM_MIGRATION_E2E_BLOG_ID, $step ); break; - case 'render-connected-without-user-data': - delete_transient( 'jetpack_connected_user_data_1' ); - // Cache the failure the package would otherwise record after a - // remote call, so the render is offline and quick. - set_transient( 'jetpack_connected_user_data_1', 'error', 5 * MINUTE_IN_SECONDS ); - wpcom_migration_e2e_expect_mode( Settings_Page::MODE_CONNECTED_WAITING, $step ); - $html = wpcom_migration_e2e_render_connect_page(); - wpcom_migration_e2e_expect_contains( $html, 'Connected to WordPress.com', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Connected as', $step ); - break; - default: throw new RuntimeException( 'Unknown connection step: ' . $step ); } diff --git a/tests/e2e/screen-steps.php b/tests/e2e/screen-steps.php index 09eb48f2..11332c5b 100644 --- a/tests/e2e/screen-steps.php +++ b/tests/e2e/screen-steps.php @@ -8,6 +8,7 @@ * @package wpcom-migration */ +use Automattic\WPCOM_Migration\Connect_Page; use Automattic\WPCOM_Migration\Reprint\Exporter; use Automattic\WPCOM_Migration\Reprint\Manual_Page; use Automattic\WPCOM_Migration\Reprint\Settings_Page; @@ -35,9 +36,11 @@ function wpcom_migration_e2e_screen_step( $step ) { case 'render-unconfigured': wpcom_migration_e2e_expect_mode( Settings_Page::MODE_NEEDS_CONNECTING, $step ); $html = wpcom_migration_e2e_render( $page ); - wpcom_migration_e2e_expect_contains( $html, 'Log in with your WordPress.com account', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Connect this site to WordPress.com', $step ); - wpcom_migration_e2e_expect_contains( $html, 'Log in with WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Log in with your WordPress.com account to connect this site', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Connect WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'By connecting, you agree to the', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'Log in with WordPress.com', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'wpcom-migration-pill', $step ); wpcom_migration_e2e_expect_primary_count( $html, 1, $step ); wpcom_migration_e2e_expect_not_contains( $html, 'Set up Reprint manually', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'id="wpcom-migration-reprint-secret"', $step ); @@ -45,10 +48,10 @@ function wpcom_migration_e2e_screen_step( $step ) { wpcom_migration_e2e_expect_not_contains( $html, 'wpcom-migration-status', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'WordPress.com connection', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'Not configured yet', $step ); - wpcom_migration_e2e_expect_contains( $html, 'class="wpcom-migration-header"', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'class="wpcom-migration-header"', $step ); wpcom_migration_e2e_expect_contains( $html, 'class="wpcom-migration-container"', $step ); wpcom_migration_e2e_expect_contains( $html, 'Migrate your site to WordPress.com', $step ); - wpcom_migration_e2e_expect_contains( $html, 'WordPress.com copies your posts, pages, media and settings', $step ); + wpcom_migration_e2e_expect_contains( $html, 'WordPress.com copies your posts, pages, media, themes, plugins, and settings', $step ); wpcom_migration_e2e_expect_not_contains( $html, '
', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'Reprint migration', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'button-primary', $step ); @@ -60,8 +63,8 @@ function wpcom_migration_e2e_screen_step( $step ) { // No connection bootstrap: the mode renders without its controls. $bare = new Settings_Page( WP_PLUGIN_DIR . '/wpcom-migration/wpcom_migration.php' ); $html = wpcom_migration_e2e_render( $bare ); - wpcom_migration_e2e_expect_contains( $html, 'Log in with your WordPress.com account', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Log in with WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Log in with your WordPress.com account to connect this site', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'Connect WordPress.com', $step ); wpcom_migration_e2e_expect_primary_count( $html, 0, $step ); wpcom_migration_e2e_expect_not_contains( $html, 'id="wpcom-migration-reprint-secret"', $step ); break; @@ -90,12 +93,13 @@ function wpcom_migration_e2e_screen_step( $step ) { } wpcom_migration_e2e_expect_mode( Settings_Page::MODE_PROVISIONED_WAITING, $step ); $html = wpcom_migration_e2e_render( $page ); - wpcom_migration_e2e_expect_contains( $html, 'This site is set up and ready.', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Set up by WordPress.com', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Nothing to do here', $step ); - // The login stays, demoted to a link. - wpcom_migration_e2e_expect_contains( $html, 'Log in with WordPress.com', $step ); - wpcom_migration_e2e_expect_primary_count( $html, 0, $step ); + wpcom_migration_e2e_expect_contains( $html, 'wpcom-migration-pill--info">Set up by WordPress.com

', $step ); + wpcom_migration_e2e_expect_contains( $html, 'is ready to migrate. The migration runs from WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Go to WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'href="https://my.wordpress.com/sites"', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'Connect WordPress.com', $step ); + wpcom_migration_e2e_expect_not_contains( $html, Connect_Page::DISCONNECT_ACTION, $step ); + wpcom_migration_e2e_expect_primary_count( $html, 1, $step ); wpcom_migration_e2e_expect_not_contains( $html, 'id="wpcom-migration-reprint-api-url"', $step ); wpcom_migration_e2e_expect_not_contains( $html, '

Export secret

', $step ); wpcom_migration_e2e_expect_not_contains( $html, 'Turn the exporter on', $step ); @@ -193,7 +197,7 @@ function wpcom_migration_e2e_screen_step( $step ) { $html = wpcom_migration_e2e_render( $page ); wpcom_migration_e2e_expect_contains( $html, 'no longer matches this site', $step ); wpcom_migration_e2e_expect_contains( $html, 'its security salts changed', $step ); - wpcom_migration_e2e_expect_contains( $html, 'Log in with WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Connect WordPress.com', $step ); wpcom_migration_e2e_expect_primary_count( $html, 1, $step ); wpcom_migration_e2e_expect_not_contains( $html, 'Invalid: the site', $step ); break; @@ -210,12 +214,13 @@ function wpcom_migration_e2e_screen_step( $step ) { } wpcom_migration_e2e_expect_mode( Settings_Page::MODE_READY, $step ); $html = wpcom_migration_e2e_render( $page ); - wpcom_migration_e2e_expect_contains( $html, 'The exporter is on until', $step ); - wpcom_migration_e2e_expect_contains( $html, 'each export keeps it on for another hour', $step ); - wpcom_migration_e2e_expect_primary_count( $html, 0, $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Log in with WordPress.com', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'Continue on WordPress.com', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'This site is set up and ready', $step ); + wpcom_migration_e2e_expect_contains( $html, 'wpcom-migration-pill--info">Site ready to migrate

', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Your site is now ready to be copied by WordPress.com', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Keep this plugin active until the migration is done', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Go to WordPress.com', $step ); + wpcom_migration_e2e_expect_primary_count( $html, 1, $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'Connect WordPress.com', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'Set up by WordPress.com', $step ); break; case 'disable': @@ -230,8 +235,8 @@ function wpcom_migration_e2e_screen_step( $step ) { } wpcom_migration_e2e_expect_mode( Settings_Page::MODE_PROVISIONED_WAITING, $step ); $html = wpcom_migration_e2e_render( $page ); - wpcom_migration_e2e_expect_contains( $html, 'This site is set up and ready', $step ); - wpcom_migration_e2e_expect_not_contains( $html, 'The exporter is on until', $step ); + wpcom_migration_e2e_expect_contains( $html, 'Set up by WordPress.com', $step ); + wpcom_migration_e2e_expect_not_contains( $html, 'Site ready to migrate', $step ); break; case 'discard-secret': From b50527a379a5e56ba6afc0cdb44cbec35eb8aeb4 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Oct 2026 14:02:17 +0200 Subject: [PATCH 3/3] Count primary buttons by their exact class in the e2e The arrow's wpcom-migration-button__icon class also contains wpcom-migration-button, so each Go to WordPress.com button counted twice. Co-Authored-By: Claude Opus 5.5 --- tests/e2e/connection-steps.php | 4 ++-- tests/e2e/screen-steps.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/connection-steps.php b/tests/e2e/connection-steps.php index 89947348..09a0cbda 100644 --- a/tests/e2e/connection-steps.php +++ b/tests/e2e/connection-steps.php @@ -614,12 +614,12 @@ function wpcom_migration_e2e_expect_mode( $expected, $step ) { * buttons. The screen's primary is the branded button, not wp-admin's. * * @param string $html Rendered markup. - * @param int $expected Expected count of 'wpcom-migration-button'. + * @param int $expected Expected count of class="wpcom-migration-button". * @param string $step Name of the step, for the error message. * @throws RuntimeException When the count differs. */ function wpcom_migration_e2e_expect_primary_count( $html, $expected, $step ) { - $count = substr_count( $html, 'wpcom-migration-button' ); + $count = substr_count( $html, 'class="wpcom-migration-button"' ); if ( $expected !== $count ) { throw new RuntimeException( "Step '$step': expected $expected primary button(s), found $count." ); } diff --git a/tests/e2e/screen-steps.php b/tests/e2e/screen-steps.php index 11332c5b..0e72a713 100644 --- a/tests/e2e/screen-steps.php +++ b/tests/e2e/screen-steps.php @@ -444,12 +444,12 @@ function wpcom_migration_e2e_expect_mode( $expected, $step ) { * buttons. The screen's primary is the branded button, not wp-admin's. * * @param string $html Rendered markup. - * @param int $expected Expected count of 'wpcom-migration-button'. + * @param int $expected Expected count of class="wpcom-migration-button". * @param string $step Name of the step, for the error message. * @throws RuntimeException When the count differs. */ function wpcom_migration_e2e_expect_primary_count( $html, $expected, $step ) { - $count = substr_count( $html, 'wpcom-migration-button' ); + $count = substr_count( $html, 'class="wpcom-migration-button"' ); if ( $expected !== $count ) { throw new RuntimeException( "Step '$step': expected $expected primary button(s), found $count." ); }