Commit 1512be0ff42 for woocommerce

commit 1512be0ff427f8d2a6cda13a2171288d9de6a1ad
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Fri Sep 4 17:09:59 2026 +0300

    Add E2E coverage for the remaining Back in Stock Notifications signup and email branches (#68323)

    * test: cover remaining BIS signup and email configuration branches

    * test: make BIS signup describes self-contained and close admin contexts

    * test: reap accounts created by BIS signups through a fixture

    * test: assert refused BIS consent writes no signup, not just no account

    * test: pin createAccountOnSignup off in the BIS email suites

    * test: assert the BIS login prompt lands on the account page

    * test: match the BIS account-page landing by pathname, not regex

diff --git a/plugins/woocommerce/changelog/68079-add-bis-e2e-signup-and-email-branch-coverage b/plugins/woocommerce/changelog/68079-add-bis-e2e-signup-and-email-branch-coverage
new file mode 100644
index 00000000000..ac0a979d739
--- /dev/null
+++ b/plugins/woocommerce/changelog/68079-add-bis-e2e-signup-and-email-branch-coverage
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Add Playwright E2E coverage for the remaining Back in Stock Notifications signup and email branches (signups disabled, account creation on signup, requires-account login flow, logged-in email footer, frontend verify/unsubscribe notices, tampered and expired email links, invalid email/product/nonce rejections).
diff --git a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/README.md b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/README.md
index d1548d21e09..cc75b8b122a 100644
--- a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/README.md
+++ b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/README.md
@@ -2,9 +2,18 @@

 Covers the scenarios from the original plugin test plan that have a target in core:

-- `signing-up.spec.ts` — PDP form rendering + signup flow (logged-in, guest single-opt-in, guest double-opt-in, requires-account).
-- `receiving-confirmations.spec.ts` — verify email + verified email + unsubscribe flow (double opt-in).
-- `receiving-notifications.spec.ts` — back-in-stock email dispatch on restock + unsubscribe flow.
+- `signing-up.spec.ts` — PDP form rendering + signup flow across the settings
+  matrix: signups disabled, logged-in single opt-in (with the "Manage
+  notifications" CTA and the nonce rejection), guest single and double opt-in,
+  account creation on signup (consent checkbox, welcome email), and the
+  requires-account prompt through to a logged-in signup. Also the server-side
+  rejections for an invalid email and a tampered product id.
+- `receiving-confirmations.spec.ts` — verify email + verified email + unsubscribe
+  flow (double opt-in), the frontend verify/unsubscribe notices, the logged-in
+  footer of the verified email, and rejected verify links (tampered key, expired).
+- `receiving-notifications.spec.ts` — back-in-stock email dispatch on restock +
+  unsubscribe flow, the logged-in footer of the back-in-stock email, and a
+  rejected (tampered) unsubscribe link.
 - `managing-notifications.spec.ts` — admin list rendering + Resend on PENDING + Resend guard on ACTIVE + admin Cancel.
 - `variations.spec.ts` — variable products: the form following the selected
   variation, signup against a variation, the variation's attributes in the
@@ -31,6 +40,29 @@ Covers the scenarios from the original plugin test plan that have a target in co
   enabled (it defaults to `false`) and it resolves against the parent product on
   a variable PDP, not the selected variation.

+## Configuration-branch notes
+
+- The signup nonce is only verified when the
+  `woocommerce_customer_stock_notifications_personalization_enabled` filter is
+  on and the shopper is logged in (or an account is required), so guest forms
+  survive HTML caching. The nonce test turns personalization on through the
+  test helper's `e2e-filters` cookie (`setFilterValue()`), which is why it runs
+  in the logged-in describe.
+- Verify-link expiry is a filter
+  (`woocommerce_customer_stock_notifications_verification_expiration_time_threshold`),
+  not an option, so `expireVerificationLinks()` sets it to a negative value
+  through the same cookie. Both tests clear the cookie afterwards with
+  `clearFilters()`.
+- The email templates fork on `$is_guest`, which is "the signup has no
+  `WP_User`", not "the shopper was logged out": a guest signup with an email
+  that already belongs to an account, or one that created an account on signup,
+  also takes the logged-in branch. The specs cover it through the shared
+  `customer` account (`signUpAsCustomer()`).
+- Account-creation tests register a real customer for the guest's address. The
+  address comes from the `accountEmail` fixture, whose teardown looks it up and
+  deletes any account it finds, so a failed assertion (or a retry with a fresh
+  address) doesn't leave the account behind.
+
 ## Skipped scenarios

 Three scenarios from the original plugin test plan target features that didn't
diff --git a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-confirmations.spec.ts b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-confirmations.spec.ts
index 6603bc64322..71d858f3cb9 100644
--- a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-confirmations.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-confirmations.spec.ts
@@ -3,18 +3,27 @@
  */
 import { expect, request, tags } from '../../fixtures/fixtures';
 import { ADMIN_STATE_PATH } from '../../playwright.config';
+import { customer } from '../../test-data/data';
 import {
+	BIS_EMAIL_FOOTER,
 	BIS_EMAIL_LINKS,
 	bisAdminListUrl,
+	bisEmailBody,
 	bisEmailSubject,
+	bisNotice,
+	corruptEmailLinkKey,
+	expireVerificationLinks,
 	getEmailLinkById,
+	openEmailInMailLog,
 	resetBISOptions,
 	setBISOptions,
+	signUpAsCustomer,
 	signUpAsGuest,
 	test,
 	uniqueGuestEmail,
 } from '../../utils/back-in-stock-notifications';
 import { expectEmail, expectEmailContent } from '../../utils/email';
+import { clearFilters } from '../../utils/filters';

 test.describe(
 	'Back in Stock Notifications — receiving confirmations',
@@ -27,6 +36,7 @@ test.describe(
 				allowSignups: true,
 				doubleOptIn: true,
 				requireAccount: false,
+				createAccountOnSignup: false,
 			} );
 		} );

@@ -102,6 +112,11 @@ test.describe(

 			await page.goto( verifyLink );

+			// The link redirects to the shop with a notice naming the product.
+			await expect(
+				page.getByText( bisNotice.verified( product.name ) )
+			).toBeVisible();
+
 			// Confirmation email should be dispatched after successful verification (RSM-438).
 			await expectEmail(
 				page,
@@ -130,6 +145,17 @@ test.describe(
 			expect( unsubscribeUrl.searchParams.get( 'utm_medium' ) ).toBe(
 				'email'
 			);
+
+			// A guest signup gets the unsubscribe wording in the footer, not
+			// the account-management one.
+			await openEmailInMailLog(
+				page,
+				email,
+				bisEmailSubject.verified( product.name )
+			);
+			await expect(
+				bisEmailBody( page ).locator( 'body' )
+			).toContainText( BIS_EMAIL_FOOTER.guest );
 		} );

 		test( 'following the unsubscribe link cancels the notification', async ( {
@@ -157,6 +183,11 @@ test.describe(
 			);
 			await page.goto( unsubscribeLink );

+			// The shopper sees the outcome on the shop page they are sent to.
+			await expect(
+				page.getByText( bisNotice.unsubscribed( email, product.name ) )
+			).toBeVisible();
+
 			// Verify via the admin notifications list: the row should now show Cancelled.
 			await page.goto( bisAdminListUrl( product.id ) );
 			const row = page
@@ -165,5 +196,120 @@ test.describe(

 			await expect( row.getByText( /Cancelled/i ) ).toBeVisible();
 		} );
+
+		test( "a logged-in customer's confirmation email offers account management instead of unsubscribing", async ( {
+			page,
+			product,
+			browser,
+		} ) => {
+			await signUpAsCustomer( browser, product.permalink );
+
+			const verifyLink = await getEmailLinkById(
+				page,
+				customer.email,
+				bisEmailSubject.verify( product.name ),
+				BIS_EMAIL_LINKS.actionButton
+			);
+			await page.goto( verifyLink );
+
+			await openEmailInMailLog(
+				page,
+				customer.email,
+				bisEmailSubject.verified( product.name )
+			);
+
+			const body = bisEmailBody( page ).locator( 'body' );
+
+			// The templates fork on whether the signup belongs to an account;
+			// every other email test signs up as a guest, so this is the only
+			// place the account branch renders.
+			await expect( body ).toContainText( BIS_EMAIL_FOOTER.loggedIn );
+			await expect( body ).not.toContainText( BIS_EMAIL_FOOTER.guest );
+
+			// The wording changes; the link it wraps still has to be there.
+			await expect(
+				bisEmailBody( page ).locator(
+					`a${ BIS_EMAIL_LINKS.unsubscribe }`
+				)
+			).toHaveAttribute( 'href', /email_link_action=unsubscribe/ );
+		} );
+
+		test( 'a verify link with a tampered key leaves the signup pending', async ( {
+			page,
+			product,
+			browser,
+		} ) => {
+			const email = uniqueGuestEmail( 'bis-confirm-bad-key' );
+
+			await signUpAsGuest( browser, product.permalink, email );
+
+			const verifyLink = await getEmailLinkById(
+				page,
+				email,
+				bisEmailSubject.verify( product.name ),
+				BIS_EMAIL_LINKS.actionButton
+			);
+
+			await page.goto( corruptEmailLinkKey( verifyLink ) );
+
+			// A rejected key is a no-op: no redirect to the shop, no notice.
+			await expect( page ).toHaveURL( /notification_id=/ );
+			await expect(
+				page.getByText( bisNotice.verified( product.name ) )
+			).toHaveCount( 0 );
+
+			await page.goto( bisAdminListUrl( product.id ) );
+			const row = page
+				.getByRole( 'row' )
+				.filter( { has: page.getByText( email, { exact: true } ) } );
+			await expect( row.getByText( /Pending/i ) ).toBeVisible();
+
+			// Positive control: the untouched link still verifies, so the
+			// signup above stayed pending because of the key and not because
+			// verification is broken.
+			await page.goto( verifyLink );
+			await expect(
+				page.getByText( bisNotice.verified( product.name ) )
+			).toBeVisible();
+		} );
+
+		test( 'an expired verify link is rejected', async ( {
+			page,
+			product,
+			browser,
+		} ) => {
+			const email = uniqueGuestEmail( 'bis-confirm-expired' );
+
+			await signUpAsGuest( browser, product.permalink, email );
+
+			const verifyLink = await getEmailLinkById(
+				page,
+				email,
+				bisEmailSubject.verify( product.name ),
+				BIS_EMAIL_LINKS.actionButton
+			);
+
+			await expireVerificationLinks( page );
+			await page.goto( verifyLink );
+
+			await expect( page ).toHaveURL( /notification_id=/ );
+			await expect(
+				page.getByText( bisNotice.verified( product.name ) )
+			).toHaveCount( 0 );
+
+			await page.goto( bisAdminListUrl( product.id ) );
+			const row = page
+				.getByRole( 'row' )
+				.filter( { has: page.getByText( email, { exact: true } ) } );
+			await expect( row.getByText( /Pending/i ) ).toBeVisible();
+
+			// Positive control: back on the default threshold the very same
+			// link verifies, so the expiry filter is what rejected it.
+			await clearFilters( page );
+			await page.goto( verifyLink );
+			await expect(
+				page.getByText( bisNotice.verified( product.name ) )
+			).toBeVisible();
+		} );
 	}
 );
diff --git a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-notifications.spec.ts b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-notifications.spec.ts
index f382444971c..4d6a5bccaab 100644
--- a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-notifications.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/receiving-notifications.spec.ts
@@ -3,14 +3,21 @@
  */
 import { expect, request, tags } from '../../fixtures/fixtures';
 import { ADMIN_STATE_PATH } from '../../playwright.config';
+import { customer } from '../../test-data/data';
 import {
+	BIS_EMAIL_FOOTER,
 	BIS_EMAIL_LINKS,
 	bisAdminListUrl,
+	bisEmailBody,
 	bisEmailSubject,
+	bisNotice,
+	corruptEmailLinkKey,
 	getEmailLinkById,
+	openEmailInMailLog,
 	resetBISOptions,
 	restockProduct,
 	setBISOptions,
+	signUpAsCustomer,
 	signUpAsGuest,
 	test,
 	triggerStockNotificationsBatch,
@@ -31,6 +38,7 @@ test.describe(
 				allowSignups: true,
 				doubleOptIn: false,
 				requireAccount: false,
+				createAccountOnSignup: false,
 			} );
 		} );

@@ -82,6 +90,48 @@ test.describe(
 			expect( linkUrl.toString() ).toBe(
 				new URL( product.permalink ).toString()
 			);
+
+			// A guest signup gets the unsubscribe wording in the footer.
+			await openEmailInMailLog(
+				page,
+				email,
+				bisEmailSubject.backInStock( product.name )
+			);
+			await expect(
+				bisEmailBody( page ).locator( 'body' )
+			).toContainText( BIS_EMAIL_FOOTER.guest );
+		} );
+
+		test( "a logged-in customer's back-in-stock email offers account management instead of unsubscribing", async ( {
+			page,
+			product,
+			restApi,
+			browser,
+		} ) => {
+			await signUpAsCustomer( browser, product.permalink );
+
+			await restockProduct( restApi, product.id );
+			await triggerStockNotificationsBatch( page );
+
+			await openEmailInMailLog(
+				page,
+				customer.email,
+				bisEmailSubject.backInStock( product.name )
+			);
+
+			const body = bisEmailBody( page ).locator( 'body' );
+
+			// The template forks on whether the signup belongs to an account;
+			// the guest fork is what every other email test exercises.
+			await expect( body ).toContainText( BIS_EMAIL_FOOTER.loggedIn );
+			await expect( body ).not.toContainText( BIS_EMAIL_FOOTER.guest );
+
+			// The wording changes; the link it wraps still has to be there.
+			await expect(
+				bisEmailBody( page ).locator(
+					`a${ BIS_EMAIL_LINKS.unsubscribe }`
+				)
+			).toHaveAttribute( 'href', /email_link_action=unsubscribe/ );
 		} );

 		test( 'unsubscribe link in the back-in-stock email cancels the notification', async ( {
@@ -111,11 +161,60 @@ test.describe(
 			);
 			await page.goto( unsubscribeLink );

+			// The shopper sees the outcome on the shop page they are sent to.
+			await expect(
+				page.getByText( bisNotice.unsubscribed( email, product.name ) )
+			).toBeVisible();
+
 			await page.goto( bisAdminListUrl( product.id ) );
 			const row = page
 				.getByRole( 'row' )
 				.filter( { has: page.getByText( email, { exact: true } ) } );
 			await expect( row.getByText( /Cancelled/i ) ).toBeVisible();
 		} );
+
+		test( 'an unsubscribe link with a tampered key changes nothing', async ( {
+			page,
+			product,
+			restApi,
+			browser,
+		} ) => {
+			const email = uniqueGuestEmail( 'bis-restock-bad-key' );
+
+			await signUpAsGuest( browser, product.permalink, email );
+
+			await restockProduct( restApi, product.id );
+			await triggerStockNotificationsBatch( page );
+
+			const unsubscribeLink = await getEmailLinkById(
+				page,
+				email,
+				bisEmailSubject.backInStock( product.name ),
+				BIS_EMAIL_LINKS.unsubscribe
+			);
+
+			await page.goto( corruptEmailLinkKey( unsubscribeLink ) );
+
+			// A rejected key is a no-op: no redirect to the shop, no notice.
+			await expect( page ).toHaveURL( /notification_id=/ );
+			await expect(
+				page.getByText( bisNotice.unsubscribed( email, product.name ) )
+			).toHaveCount( 0 );
+
+			await page.goto( bisAdminListUrl( product.id ) );
+			const row = page
+				.getByRole( 'row' )
+				.filter( { has: page.getByText( email, { exact: true } ) } );
+			await expect( row ).toHaveCount( 1 );
+			await expect( row.getByText( /Cancelled/i ) ).toHaveCount( 0 );
+
+			// Positive control: the untouched link still unsubscribes, so the
+			// row above survived because of the key and not because
+			// unsubscribing is broken.
+			await page.goto( unsubscribeLink );
+			await expect(
+				page.getByText( bisNotice.unsubscribed( email, product.name ) )
+			).toBeVisible();
+		} );
 	}
 );
diff --git a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/signing-up.spec.ts b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/signing-up.spec.ts
index 6520bf4d0e3..b4d508a4577 100644
--- a/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/signing-up.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/back-in-stock-notifications/signing-up.spec.ts
@@ -2,16 +2,24 @@
  * Internal dependencies
  */
 import { expect, request, tags } from '../../fixtures/fixtures';
-import { ADMIN_STATE_PATH, CUSTOMER_STATE_PATH } from '../../playwright.config';
+import { CUSTOMER_STATE_PATH } from '../../playwright.config';
+import { customer } from '../../test-data/data';
 import {
+	bisConsentCheckbox,
 	bisEmailSubject,
+	bisFormLocator,
+	bisNotice,
+	bisTargetProductInput,
+	expectEmailAsAdmin,
+	expectNoSignupAsAdmin,
+	findCustomerByEmail,
 	resetBISOptions,
 	setBISOptions,
 	signUpOnProductPage,
 	test,
 	uniqueGuestEmail,
 } from '../../utils/back-in-stock-notifications';
-import { expectEmail } from '../../utils/email';
+import { clearFilters, setFilterValue } from '../../utils/filters';

 test.describe(
 	'Back in Stock Notifications — signing up',
@@ -21,6 +29,44 @@ test.describe(
 			await resetBISOptions( request, baseURL! );
 		} );

+		test.describe( 'Signups disabled', () => {
+			test.beforeAll( async ( { baseURL } ) => {
+				await setBISOptions( request, baseURL!, {
+					allowSignups: true,
+					doubleOptIn: false,
+					requireAccount: false,
+					createAccountOnSignup: false,
+				} );
+			} );
+
+			test( 'turning signups off removes the form from the product page', async ( {
+				page,
+				product,
+				baseURL,
+			} ) => {
+				// Positive control: the form is there while signups are on, so
+				// a product page that renders no form for an unrelated reason
+				// cannot pass the assertion below.
+				await page.goto( product.permalink );
+				await expect( bisFormLocator( page ) ).toHaveCount( 1 );
+
+				// Flipped inside the test rather than in `beforeAll` so the
+				// control above and the assertion below run against the same
+				// product. The next describe's `beforeAll` sets it back.
+				await setBISOptions( request, baseURL!, {
+					allowSignups: false,
+				} );
+
+				await page.goto( product.permalink );
+				await expect( bisFormLocator( page ) ).toHaveCount( 0 );
+				await expect(
+					page.getByRole( 'heading', {
+						name: /Want to be notified when this product is back in stock\?/i,
+					} )
+				).toHaveCount( 0 );
+			} );
+		} );
+
 		test.describe( 'Logged-in customer, single opt-in', () => {
 			test.use( { storageState: CUSTOMER_STATE_PATH } );

@@ -29,6 +75,7 @@ test.describe(
 					allowSignups: true,
 					doubleOptIn: false,
 					requireAccount: false,
+					createAccountOnSignup: false,
 				} );
 			} );

@@ -55,7 +102,7 @@ test.describe(
 				).toBeVisible();
 			} );

-			test( 'submitting the form surfaces a success notice', async ( {
+			test( 'submitting the form surfaces a success notice with a link to manage notifications', async ( {
 				page,
 				product,
 			} ) => {
@@ -63,8 +110,14 @@ test.describe(
 				await signUpOnProductPage( page );

 				await expect(
-					page.getByText( /You have successfully signed up/i )
+					page.getByText( bisNotice.success( product.name ) )
 				).toBeVisible();
+
+				// Logged-in signups get a "Manage notifications" CTA in front
+				// of the notice, pointing at the account endpoint.
+				await expect(
+					page.getByRole( 'link', { name: 'Manage notifications' } )
+				).toHaveAttribute( 'href', /stock-notifications/ );
 			} );

 			test( 'a repeated signup surfaces the "already joined" notice', async ( {
@@ -78,7 +131,7 @@ test.describe(
 				// below can't race it. This is the first signup for a freshly
 				// created product, so it always succeeds.
 				await expect(
-					page.getByText( /You have successfully signed up/i )
+					page.getByText( bisNotice.success( product.name ) )
 				).toBeVisible();

 				// Submitting the form a second time (the "already joined"
@@ -89,9 +142,49 @@ test.describe(
 				await page.goto( product.permalink );
 				await signUpOnProductPage( page );
 				await expect(
-					page.getByText( /You have already joined this waitlist/i )
+					page.getByText( bisNotice.alreadyJoined )
 				).toBeVisible();
 			} );
+
+			test( 'a missing nonce is rejected once nonce checks are on', async ( {
+				page,
+				product,
+			} ) => {
+				// Core only verifies the signup nonce when personalization is
+				// on and the shopper is logged in (or an account is required),
+				// so guest forms survive HTML caching. Turn personalization on
+				// through the test helper's filter cookie to reach that branch.
+				await setFilterValue(
+					page,
+					'woocommerce_customer_stock_notifications_personalization_enabled',
+					true
+				);
+
+				await page.goto( product.permalink );
+
+				// Blank the nonce the form posts, the way a stale cached page
+				// or a forged request would.
+				await page
+					.locator( 'input[name="wc_bis_nonce"]' )
+					.evaluate( ( input: HTMLInputElement ) => {
+						input.value = '';
+					} );
+				await signUpOnProductPage( page );
+
+				await expect(
+					page.getByText( bisNotice.errors.failed )
+				).toBeVisible();
+
+				// Positive control on the same page: with the nonce intact the
+				// signup goes through, so the rejection above was the nonce.
+				await page.goto( product.permalink );
+				await signUpOnProductPage( page );
+				await expect(
+					page.getByText( bisNotice.success( product.name ) )
+				).toBeVisible();
+
+				await clearFilters( page );
+			} );
 		} );

 		test.describe( 'Guest — single opt-in', () => {
@@ -100,6 +193,7 @@ test.describe(
 					allowSignups: true,
 					doubleOptIn: false,
 					requireAccount: false,
+					createAccountOnSignup: false,
 				} );
 			} );

@@ -117,6 +211,60 @@ test.describe(
 				await expect(
 					page.getByRole( 'button', { name: /Notify me/i } )
 				).toBeVisible();
+
+				// The consent checkbox only belongs to the account-creation
+				// setup, which is off here.
+				await expect( bisConsentCheckbox( page ) ).toHaveCount( 0 );
+			} );
+
+			test( 'submitting the form surfaces a success notice', async ( {
+				page,
+				product,
+			} ) => {
+				await page.goto( product.permalink );
+				await signUpOnProductPage( page, {
+					email: uniqueGuestEmail( 'bis-guest-single' ),
+				} );
+
+				await expect(
+					page.getByText( bisNotice.success( product.name ) )
+				).toBeVisible();
+			} );
+
+			test( 'an invalid email address is rejected', async ( {
+				page,
+				product,
+			} ) => {
+				await page.goto( product.permalink );
+
+				// The form opts out of browser validation (`novalidate`), so
+				// the malformed address reaches the server and it is the
+				// server's rejection that renders.
+				await signUpOnProductPage( page, { email: 'not-an-email' } );
+
+				await expect(
+					page.getByText( bisNotice.errors.invalidEmail )
+				).toBeVisible();
+			} );
+
+			test( 'a product id that does not exist is rejected', async ( {
+				page,
+				product,
+			} ) => {
+				await page.goto( product.permalink );
+
+				await bisTargetProductInput( page ).evaluate(
+					( input: HTMLInputElement ) => {
+						input.value = '999999999';
+					}
+				);
+				await signUpOnProductPage( page, {
+					email: uniqueGuestEmail( 'bis-guest-bad-product' ),
+				} );
+
+				await expect(
+					page.getByText( bisNotice.errors.invalidProduct )
+				).toBeVisible();
 			} );
 		} );

@@ -126,10 +274,10 @@ test.describe(
 					allowSignups: true,
 					doubleOptIn: true,
 					requireAccount: false,
+					createAccountOnSignup: false,
 				} );
 			} );

-			// eslint-disable-next-line playwright/expect-expect -- `expectEmail()` asserts on the mail log.
 			test( 'submitting signup dispatches a verification email', async ( {
 				page,
 				product,
@@ -140,18 +288,123 @@ test.describe(
 				await page.goto( product.permalink );
 				await signUpOnProductPage( page, { email } );

-				// Switch to an admin context to inspect the mail log —
-				// WP Mail Logging is an admin-only screen.
-				const adminContext = await browser.newContext( {
-					storageState: ADMIN_STATE_PATH,
-				} );
-				const adminPage = await adminContext.newPage();
-				await expectEmail(
-					adminPage,
+				await expect(
+					page.getByText( bisNotice.doubleOptIn )
+				).toBeVisible();
+
+				// WP Mail Logging is an admin-only screen, so the log is read
+				// from a separate admin context.
+				await expectEmailAsAdmin(
+					browser,
 					email,
 					bisEmailSubject.verify( product.name )
 				);
-				await adminContext.close();
+			} );
+		} );
+
+		test.describe( 'Guest — create account on signup', () => {
+			test.describe( 'Single opt-in', () => {
+				test.beforeAll( async ( { baseURL } ) => {
+					await setBISOptions( request, baseURL!, {
+						allowSignups: true,
+						doubleOptIn: false,
+						requireAccount: false,
+						createAccountOnSignup: true,
+					} );
+				} );
+
+				test( 'the consent checkbox renders and the signup is refused until it is ticked', async ( {
+					page,
+					product,
+					restApi,
+					browser,
+					accountEmail: email,
+				} ) => {
+					await page.goto( product.permalink );
+
+					const consent = bisConsentCheckbox( page );
+					await expect( consent ).toBeVisible();
+					await expect( consent ).not.toBeChecked();
+
+					await signUpOnProductPage( page, { email } );
+
+					await expect(
+						page.getByText( bisNotice.errors.missingConsent )
+					).toBeVisible();
+
+					// The refusal has to happen before anything is written:
+					// no account for the address, and no signup either.
+					expect(
+						await findCustomerByEmail( restApi, email )
+					).toBeUndefined();
+					await expectNoSignupAsAdmin( browser, product.id, email );
+				} );
+
+				test( 'ticking consent signs up, registers an account and sends the welcome email', async ( {
+					page,
+					product,
+					restApi,
+					browser,
+					accountEmail: email,
+				} ) => {
+					await page.goto( product.permalink );
+					await signUpOnProductPage( page, { email, consent: true } );
+
+					await expect(
+						page.getByText(
+							bisNotice.accountCreated( product.name )
+						)
+					).toBeVisible();
+
+					expect(
+						await findCustomerByEmail( restApi, email )
+					).toBeDefined();
+
+					// The "check your e-mail for details" in the notice is
+					// WooCommerce's own new-account email, sent with the
+					// generated password.
+					await expectEmailAsAdmin(
+						browser,
+						email,
+						/account has been created!/
+					);
+				} );
+			} );
+
+			test.describe( 'Double opt-in', () => {
+				test.beforeAll( async ( { baseURL } ) => {
+					await setBISOptions( request, baseURL!, {
+						allowSignups: true,
+						doubleOptIn: true,
+						requireAccount: false,
+						createAccountOnSignup: true,
+					} );
+				} );
+
+				test( 'ticking consent registers an account and still asks for email verification', async ( {
+					page,
+					product,
+					restApi,
+					browser,
+					accountEmail: email,
+				} ) => {
+					await page.goto( product.permalink );
+					await signUpOnProductPage( page, { email, consent: true } );
+
+					await expect(
+						page.getByText( bisNotice.accountCreatedDoubleOptIn )
+					).toBeVisible();
+
+					expect(
+						await findCustomerByEmail( restApi, email )
+					).toBeDefined();
+
+					await expectEmailAsAdmin(
+						browser,
+						email,
+						bisEmailSubject.verify( product.name )
+					);
+				} );
 			} );
 		} );

@@ -161,6 +414,7 @@ test.describe(
 					allowSignups: true,
 					doubleOptIn: false,
 					requireAccount: true,
+					createAccountOnSignup: false,
 				} );
 			} );

@@ -175,13 +429,57 @@ test.describe(
 						name: /Email address to be notified/i,
 					} )
 				).toHaveCount( 0 );
+				await expect(
+					page.getByRole( 'button', { name: /Notify me/i } )
+				).toHaveCount( 0 );

 				// Pair the absence with the prompt core renders in its place,
 				// so a 404 or a failed render can't pass as account gating.
 				await expect(
-					page.getByText(
-						/Please log in to sign up for stock notifications/i
-					)
+					page.getByText( bisNotice.accountRequired )
+				).toBeVisible();
+			} );
+
+			test( 'logging in from the prompt lets the customer sign up', async ( {
+				page,
+				product,
+			} ) => {
+				await page.goto( product.permalink );
+
+				await page.getByRole( 'link', { name: 'log in' } ).click();
+
+				// The prompt's link points at a non-existent endpoint, so
+				// WordPress guesses the 404 back to the account page. Assert
+				// the landing, or a lost guess would only show up as a
+				// timeout on the login form below.
+				await expect( page ).toHaveURL( ( url ) => {
+					const path = url.pathname.endsWith( '/' )
+						? url.pathname.slice( 0, -1 )
+						: url.pathname;
+
+					return path.endsWith( '/my-account' );
+				} );
+
+				await page.locator( '#username' ).fill( customer.username );
+				await page.locator( '#password' ).fill( customer.password );
+				await page
+					.getByRole( 'button', { name: 'Log in', exact: true } )
+					.click();
+
+				// Login lands on the account dashboard, so go back to the
+				// product to find the form now rendered for the customer.
+				await page.goto( product.permalink );
+				await expect(
+					page.getByText( bisNotice.accountRequired )
+				).toHaveCount( 0 );
+
+				await signUpOnProductPage( page );
+
+				await expect(
+					page.getByText( bisNotice.success( product.name ) )
+				).toBeVisible();
+				await expect(
+					page.getByRole( 'link', { name: 'Manage notifications' } )
 				).toBeVisible();
 			} );
 		} );
diff --git a/plugins/woocommerce/tests/e2e/utils/back-in-stock-notifications.ts b/plugins/woocommerce/tests/e2e/utils/back-in-stock-notifications.ts
index bafb4e4f6d7..f3ef921f239 100644
--- a/plugins/woocommerce/tests/e2e/utils/back-in-stock-notifications.ts
+++ b/plugins/woocommerce/tests/e2e/utils/back-in-stock-notifications.ts
@@ -13,9 +13,11 @@ import {
  */
 import { deleteOption, setOption } from './options';
 import { expectEmail } from './email';
+import { setFilterValue } from './filters';
 import { wpCLI } from './cli';
 import { expect, test as baseTest } from '../fixtures/fixtures';
 import { admin } from '../test-data/data';
+import { ADMIN_STATE_PATH, CUSTOMER_STATE_PATH } from '../playwright.config';

 /**
  * Names of the Back in Stock Notifications options in core.
@@ -27,6 +29,8 @@ export const BIS_OPTIONS = {
 	doubleOptIn:
 		'woocommerce_customer_stock_notifications_require_double_opt_in',
 	requireAccount: 'woocommerce_customer_stock_notifications_require_account',
+	createAccountOnSignup:
+		'woocommerce_customer_stock_notifications_create_account_on_signup',
 } as const;

 /**
@@ -83,12 +87,13 @@ export async function assertBISEnvReady(): Promise< void > {
 /**
  * Configure the BIS feature options for a test. Omitted keys are left untouched.
  *
- * @param {APIRequest} request                  Playwright request fixture.
- * @param {string}     baseURL                  Test site base URL.
- * @param {Object}     options                  BIS option toggles.
- * @param {boolean}    [options.allowSignups]   Whether the signup form is rendered on product pages.
- * @param {boolean}    [options.doubleOptIn]    Whether signups require email verification before activating.
- * @param {boolean}    [options.requireAccount] Whether signups are limited to logged-in users.
+ * @param {APIRequest} request                         Playwright request fixture.
+ * @param {string}     baseURL                         Test site base URL.
+ * @param {Object}     options                         BIS option toggles.
+ * @param {boolean}    [options.allowSignups]          Whether the signup form is rendered on product pages.
+ * @param {boolean}    [options.doubleOptIn]           Whether signups require email verification before activating.
+ * @param {boolean}    [options.requireAccount]        Whether signups are limited to logged-in users.
+ * @param {boolean}    [options.createAccountOnSignup] Whether a guest signup also registers a customer account.
  */
 export async function setBISOptions(
 	request: APIRequest,
@@ -97,6 +102,7 @@ export async function setBISOptions(
 		allowSignups?: boolean;
 		doubleOptIn?: boolean;
 		requireAccount?: boolean;
+		createAccountOnSignup?: boolean;
 	}
 ): Promise< void > {
 	const toYesNo = ( v: boolean | undefined ): string | undefined => {
@@ -110,6 +116,10 @@ export async function setBISOptions(
 		[ BIS_OPTIONS.allowSignups, toYesNo( options.allowSignups ) ],
 		[ BIS_OPTIONS.doubleOptIn, toYesNo( options.doubleOptIn ) ],
 		[ BIS_OPTIONS.requireAccount, toYesNo( options.requireAccount ) ],
+		[
+			BIS_OPTIONS.createAccountOnSignup,
+			toYesNo( options.createAccountOnSignup ),
+		],
 	];

 	for ( const [ name, value ] of entries ) {
@@ -478,17 +488,31 @@ export async function selectVariation(
 	);
 }

+/**
+ * Locator for the account-creation consent checkbox on the PDP sign-up form.
+ *
+ * Its label is the store's registration privacy text, which the merchant can
+ * edit, so it is located by name rather than by that label.
+ *
+ * @param {Page} page Playwright page on the product detail.
+ */
+export function bisConsentCheckbox( page: Page ) {
+	return page.locator( 'input[name="wc_bis_opt_in"]' );
+}
+
 /**
  * Submit the PDP sign-up form. Caller must already have the product page loaded.
  *
- * @param {Page}   page         Playwright page on the product detail.
- * @param {Object} [opts]       Fill options.
- * @param {string} [opts.email] Email address to enter (guest flow only; logged-in PDP hides the field).
+ * @param {Page}    page           Playwright page on the product detail.
+ * @param {Object}  [opts]         Fill options.
+ * @param {string}  [opts.email]   Email address to enter (guest flow only; logged-in PDP hides the field).
+ * @param {boolean} [opts.consent] Tick the account-creation consent checkbox (only rendered with `createAccountOnSignup`).
  */
 export async function signUpOnProductPage(
 	page: Page,
 	opts: {
 		email?: string;
+		consent?: boolean;
 	} = {}
 ): Promise< void > {
 	if ( opts.email !== undefined ) {
@@ -499,9 +523,81 @@ export async function signUpOnProductPage(
 			.fill( opts.email );
 	}

+	if ( opts.consent ) {
+		await bisConsentCheckbox( page ).check();
+	}
+
 	await page.getByRole( 'button', { name: /Notify me/i } ).click();
 }

+/**
+ * Submit the PDP signup form in a fresh browser context and wait for the success notice.
+ *
+ * Runs in its own context so the caller's page (usually an admin session that
+ * goes on to read the mail log) is left untouched.
+ *
+ * @param {Browser} browser                          The test's browser fixture.
+ * @param {string}  permalink                        The product permalink.
+ * @param {Object}  opts                             Signup options.
+ * @param {Object}  [opts.storageState]              Storage state for the signup context; a logged-out guest by default.
+ * @param {string}  [opts.email]                     Email to enter; omit for a logged-in signup, where the field isn't rendered.
+ * @param {boolean} [opts.consent]                   Tick the account-creation consent checkbox before submitting.
+ * @param {RegExp}  [opts.expectedNotice]            Notice to wait for after the post; a generic success match by default.
+ * @param {Object}  [opts.selectVariation]           Variation to pick before submitting, for variable products.
+ * @param {Object}  [opts.selectVariation.product]   The variable product handle.
+ * @param {Object}  [opts.selectVariation.variation] The variation to select.
+ */
+export async function signUpInNewContext(
+	browser: Browser,
+	permalink: string,
+	opts: {
+		storageState?: string | { cookies: []; origins: [] };
+		email?: string;
+		consent?: boolean;
+		expectedNotice?: RegExp;
+		selectVariation?: {
+			product: BISVariableProduct;
+			variation: BISVariation;
+		};
+	} = {}
+): Promise< void > {
+	const context = await browser.newContext( {
+		storageState: opts.storageState ?? { cookies: [], origins: [] },
+	} );
+	const page = await context.newPage();
+
+	// Closed in `finally`: these specs run on a single worker, so a context
+	// left open by a failed signup would otherwise outlive the test.
+	try {
+		await page.goto( permalink );
+
+		if ( opts.selectVariation ) {
+			await selectVariation(
+				page,
+				opts.selectVariation.product,
+				opts.selectVariation.variation
+			);
+		}
+
+		await signUpOnProductPage( page, {
+			email: opts.email,
+			consent: opts.consent,
+		} );
+
+		// The form posts and reloads the PDP with a notice. Wait for that notice
+		// before closing the context, or the submission can be aborted mid-flight
+		// and the spec fails later, looking like a missing email.
+		await expect(
+			page.getByText(
+				opts.expectedNotice ??
+					/You have successfully signed up|Thanks for signing up/i
+			)
+		).toBeVisible();
+	} finally {
+		await context.close();
+	}
+}
+
 /**
  * Submit the PDP signup form as a logged-out guest, regardless of the test's storageState.
  *
@@ -524,34 +620,110 @@ export async function signUpAsGuest(
 		};
 	} = {}
 ): Promise< void > {
-	const guestContext = await browser.newContext( {
-		storageState: { cookies: [], origins: [] },
+	await signUpInNewContext( browser, permalink, { email, ...opts } );
+}
+
+/**
+ * Submit the PDP signup form as the shared logged-in customer, regardless of the test's storageState.
+ *
+ * The signup binds to the customer's account, which is what makes the emails
+ * take their logged-in branch.
+ *
+ * @param {Browser} browser   The test's browser fixture.
+ * @param {string}  permalink The product permalink.
+ */
+export async function signUpAsCustomer(
+	browser: Browser,
+	permalink: string
+): Promise< void > {
+	await signUpInNewContext( browser, permalink, {
+		storageState: CUSTOMER_STATE_PATH,
 	} );
-	const guestPage = await guestContext.newPage();
-	await guestPage.goto( permalink );
-
-	if ( opts.selectVariation ) {
-		await selectVariation(
-			guestPage,
-			opts.selectVariation.product,
-			opts.selectVariation.variation
-		);
-	}
+}
+
+/**
+ * Find the customer account registered for an email address, if any.
+ *
+ * @param {ApiClient} restApi WP REST client.
+ * @param {string}    email   The email address.
+ */
+export async function findCustomerByEmail(
+	restApi: ApiClient,
+	email: string
+): Promise< BISCustomer | undefined > {
+	const response = await restApi.get< BISCustomer[] >(
+		`${ WC_API_PATH }/customers`,
+		{
+			email,
+			role: 'all',
+		}
+	);

-	await signUpOnProductPage( guestPage, { email } );
+	return response.data.find(
+		( customer: BISCustomer ) => customer.email === email
+	);
+}

-	// The form posts and reloads the PDP with a notice. Wait for that notice
-	// before closing the context, or the submission can be aborted mid-flight
-	// and the spec fails later, looking like a missing email.
-	await expect(
-		guestPage.getByText(
-			/You have successfully signed up|Thanks for signing up/i
-		)
-	).toBeVisible();
+/**
+ * A customer account, as far as these specs need to know it.
+ */
+type BISCustomer = { id: number; email: string; username: string };
+
+/**
+ * Permanently delete a customer account created by a signup.
+ *
+ * @param {ApiClient} restApi    WP REST client.
+ * @param {number}    customerId The customer id.
+ */
+export async function deleteCustomer(
+	restApi: ApiClient,
+	customerId: number
+): Promise< void > {
+	await restApi.delete( `${ WC_API_PATH }/customers/${ customerId }`, {
+		force: true,
+	} );
+}
+
+/**
+ * Make every verification link look expired to the server for this page's context.
+ *
+ * Expiry is filter-driven rather than an option, so it is set through the
+ * `e2e-filters` cookie the test helper plugin reads. A negative threshold
+ * makes `time() - timestamp > threshold` true for any link, however fresh.
+ *
+ * @param {Page} page Playwright page whose context will follow the link.
+ */
+export async function expireVerificationLinks( page: Page ): Promise< void > {
+	await setFilterValue(
+		page,
+		'woocommerce_customer_stock_notifications_verification_expiration_time_threshold',
+		-1
+	);
+}

-	await guestContext.close();
+/**
+ * Replace the action key in an email link with one that cannot match.
+ *
+ * @param {string} link The verify or unsubscribe link from the email.
+ */
+export function corruptEmailLinkKey( link: string ): string {
+	const url = new URL( link );
+	url.searchParams.set( 'email_link_action_key', 'not-the-real-key' );
+	return url.toString();
 }

+/**
+ * Text the email footer renders for a signup bound to an account.
+ *
+ * @see templates/emails/customer-stock-notification.php
+ * @see templates/emails/customer-stock-notification-verified.php
+ */
+export const BIS_EMAIL_FOOTER = {
+	loggedIn:
+		/To manage your notifications, click here to log in to your account\./,
+	guest: /To stop receiving these messages, click here to unsubscribe\./,
+} as const;
+
 /**
  * Build the admin notifications-list URL, optionally filtered to one product.
  *
@@ -564,6 +736,50 @@ export function bisAdminListUrl( productId: number ): string {
 	return `wp-admin/admin.php?page=wc-customer-stock-notifications&customer_stock_notifications_product_filter=${ productId }`;
 }

+/**
+ * Assert the product's notifications list holds no row for an email address.
+ *
+ * Opens its own admin context because the list is an admin-only screen, and
+ * closes it in `finally` so a failure doesn't leak a context into the run.
+ *
+ * @param {Browser} browser   The test's browser fixture.
+ * @param {number}  productId Product the list is filtered by.
+ * @param {string}  email     The signup email address that must not be listed.
+ */
+export async function expectNoSignupAsAdmin(
+	browser: Browser,
+	productId: number,
+	email: string
+): Promise< void > {
+	const adminContext = await browser.newContext( {
+		storageState: ADMIN_STATE_PATH,
+	} );
+
+	try {
+		const adminPage = await adminContext.newPage();
+		await adminPage.goto( bisAdminListUrl( productId ) );
+
+		await expect(
+			adminPage.getByRole( 'row' ).filter( {
+				has: adminPage.getByText( email, { exact: true } ),
+			} )
+		).toHaveCount( 0 );
+	} finally {
+		await adminContext.close();
+	}
+}
+
+/**
+ * Generate a unique guest email address for a test so mail-log assertions don't collide.
+ *
+ * @param {string} prefix Short descriptor of the test.
+ */
+export function uniqueGuestEmail( prefix = 'bis' ): string {
+	return `${ prefix }-${ Date.now() }-${ Math.floor(
+		Math.random() * 1000
+	) }@example.com`;
+}
+
 /**
  * Ids of products created by the `product` fixture, deleted in one batch when
  * the worker finishes. A per-test DELETE costs ~0.45s, which is pure overhead
@@ -602,6 +818,7 @@ export const test = baseTest.extend<
 		product: BISProduct;
 		variableProduct: BISVariableProduct;
 		anyAttributeVariableProduct: BISVariableProduct;
+		accountEmail: string;
 	},
 	{ bisEnvReady: void }
 >( {
@@ -655,6 +872,24 @@ export const test = baseTest.extend<
 		await use( product );
 		productsToReap.push( product.id );
 	},
+
+	/**
+	 * A guest email address that a signup may register an account for.
+	 *
+	 * Teardown looks the address up and deletes the account if one exists, so
+	 * the customer is reaped whether the test failed on the notice, on the
+	 * lookup, or on the email — the test itself never has to hold the id.
+	 */
+	accountEmail: async ( { restApi }, use ) => {
+		const email = uniqueGuestEmail( 'bis-account' );
+		// eslint-disable-next-line react-hooks/rules-of-hooks -- Playwright's fixture `use`, not a React hook.
+		await use( email );
+
+		const account = await findCustomerByEmail( restApi, email );
+		if ( account ) {
+			await deleteCustomer( restApi, account.id );
+		}
+	},
 } );

 /**
@@ -706,6 +941,79 @@ export function escapeRegExp( value: string ): string {
 	return value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
 }

+/**
+ * Sign-up notices core prints on the PDP after the form posts.
+ *
+ * Success notices are bound to the product name where core interpolates it,
+ * so a notice for the wrong product fails instead of passing.
+ *
+ * @see SignupService::get_signup_user_message()
+ * @see SignupService::get_error_message()
+ * @see EmailActionController
+ */
+export const bisNotice = {
+	/**
+	 * Single opt-in success.
+	 *
+	 * @param {string} productName The product name.
+	 */
+	success: ( productName: string ): RegExp =>
+		new RegExp(
+			`You have successfully signed up! You will be notified when "${ escapeRegExp(
+				productName
+			) }" is back in stock\\.`
+		),
+	doubleOptIn:
+		/Thanks for signing up! Please complete the sign-up process by following the verification link sent to your e-mail\./,
+	/**
+	 * Single opt-in success where the signup also registered an account.
+	 *
+	 * @param {string} productName The product name.
+	 */
+	accountCreated: ( productName: string ): RegExp =>
+		new RegExp(
+			`You have successfully signed up and will be notified when "${ escapeRegExp(
+				productName
+			) }" is back in stock! Note that a new account has been created for you; please check your e-mail for details\\.`
+		),
+	accountCreatedDoubleOptIn:
+		/Thanks for signing up! An account has been created for you\. Please complete the sign-up process by following the verification link sent to your e-mail\./,
+	alreadyJoined: /You have already joined this waitlist\./,
+	accountRequired: /Please log in to sign up for stock notifications\./,
+	/**
+	 * Printed on the shop page after a verify link is followed.
+	 *
+	 * @param {string} productName The product name.
+	 */
+	verified: ( productName: string ): RegExp =>
+		new RegExp(
+			`Successfully verified stock notifications for "${ escapeRegExp(
+				productName
+			) }"\\.`
+		),
+	/**
+	 * Printed on the shop page after an unsubscribe link is followed.
+	 *
+	 * @param {string} email       The unsubscribed email address.
+	 * @param {string} productName The product name.
+	 */
+	unsubscribed: ( email: string, productName: string ): RegExp =>
+		new RegExp(
+			`Successfully unsubscribed ${ escapeRegExp(
+				email
+			) }\\. You will not receive a notification when "${ escapeRegExp(
+				productName
+			) }" becomes available\\.`
+		),
+	errors: {
+		invalidEmail: /Invalid email address\./,
+		invalidProduct: /Invalid product\./,
+		missingConsent:
+			/To proceed, please consent to the creation of a new account with your e-mail\./,
+		failed: /Failed to sign up\. Please try again\./,
+	},
+} as const;
+
 /**
  * Subject matchers for the three BIS emails, bound to a specific product.
  *
@@ -743,6 +1051,41 @@ export const bisEmailSubject = {
 		subjectMatcher( `"${ productName }" is back in stock!` ),
 } as const;

+/**
+ * Assert an email landed in the mail log, from a throwaway admin context.
+ *
+ * For specs whose own page is a guest or customer session: WP Mail Logging
+ * is an admin-only screen. The context is closed in `finally` so a missing
+ * email fails the test without leaking a context into the rest of the run.
+ *
+ * @param {Browser} browser              The test's browser fixture.
+ * @param {string}  receiverEmailAddress The recipient email address.
+ * @param {RegExp}  subject              The email subject (regular expression).
+ * @param {number}  [expectedCount]      Expected number of matching rows. Defaults to 1.
+ */
+export async function expectEmailAsAdmin(
+	browser: Browser,
+	receiverEmailAddress: string,
+	subject: RegExp,
+	expectedCount = 1
+): Promise< void > {
+	const adminContext = await browser.newContext( {
+		storageState: ADMIN_STATE_PATH,
+	} );
+
+	try {
+		const adminPage = await adminContext.newPage();
+		await expectEmail(
+			adminPage,
+			receiverEmailAddress,
+			subject,
+			expectedCount
+		);
+	} finally {
+		await adminContext.close();
+	}
+}
+
 /**
  * Open the WP Mail Logging entry for a given recipient and subject, leaving its modal open.
  *
@@ -849,14 +1192,3 @@ export async function triggerStockNotificationsBatch(
 ): Promise< void > {
 	await page.goto( '?process-waiting-actions' );
 }
-
-/**
- * Generate a unique guest email address for a test so mail-log assertions don't collide.
- *
- * @param {string} prefix Short descriptor of the test.
- */
-export function uniqueGuestEmail( prefix = 'bis' ): string {
-	return `${ prefix }-${ Date.now() }-${ Math.floor(
-		Math.random() * 1000
-	) }@example.com`;
-}