Commit f70cb1bd1d2 for woocommerce
commit f70cb1bd1d2b656e48038fd6d4fa27aa666b3d45
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Tue Oct 20 16:47:03 2026 +0300
[tests] Make the product export and email browser titles assert what their names claim (#68889)
* test(e2e): Isolate the order key check and assert download links are entitlements
The known-shopper title made every bad-key visit while logged out, so the
owner check hid the page before the key check could be observed. It now
visits a wrong-key URL as the order's owner, and again as a guest with the
owner requirement lifted by the filter plugin, so the key check stands on
its own.
The downloadable title matched each link href against /.+/, which fails
only on an empty string. It now requires the download file, order and key
parameters in the href and that the href is not the product page.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the shipping rate a shopper picks reaches the cart
With a single flat rate configured, the default selection and a chosen one
look identical. The topology helper now offers a second flat rate at a
different cost, and the test picks the non-default one and asserts the
radio, the order summary shipping total and the Store API cart all report
it. Switching to Pickup selects the pickup location, and switching back
returns the package's first rate, which is what the block does; the title
says that rather than promising the earlier choice is preserved.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the error notice banner under the block notices filter
Only the success template was proven, twice. The body now produces a
non-coupon error by putting the cart's product out of stock, and asserts
the error banner with its icon on a stable page load. The title names the
two templates the body covers.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert saved Related by criteria decide the frontend related list
Album has only a category, so toggling Tags changed nothing and the
frontend was read only with both criteria checked. Album now also carries
the Recommended tag, giving each criterion its own set of relatives, and
the test saves with Tags unchecked and asserts the frontend list drops the
tag-only relatives. The docblock names the query-context behaviour this
title is the only browser owner of.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the frontend inherit list and the on-sale custom criteria
The inherit half was read only in the editor, and the custom half was
asserted as 'a different list'. The test now saves in the inherit state and
asserts the shop page renders the catalog query it captured, and requires
every product in the custom list to be one the seed puts on sale. The
docblock names the post-context Query type control this title is the only
browser owner of.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert Product Rating and Stock Indicator render their product's data
The Product Elements title inserted a rating and a stock indicator but
asserted neither, and checked the post-reload editor by product count. It
now asserts the four inserted elements by block name after the reload, the
Hoodie card's rating, and an out-of-stock card's stock indicator.
The empty-collection title checked the frontend for 'No products to
display', an editor-only string that never appears there. That is replaced
by a count of the collections that reach the page.
The specific-request title asserts the shape of the editor's outgoing REST
request and nothing about what comes back, so its name now says so. The
Product Elements docblock names the per-product rendering it owns.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Document the template-slug to collection-location mapping this title owns
The preview-label title is the only browser coverage for a template slug
resolving to the product or cart location a registered collection requires.
The docblock says so, so a later move of this coverage has to account for
it.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Name the Product Filters title after the inner filters it asserts
The default template has six inner filters; the body asserts the Active
and Color ones in the canvas and their List View entries. The title now
says that instead of promising the whole structure.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the inserted Related Products variation carries the related collection
The title proved only that a Product Collection block appears and the
legacy block does not, which any collection would satisfy. It now reads the
inserted block's collection attribute and requires the related variation.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Wait for the inserter list before asserting Product Gallery is absent
The negative half ran straight after the inserter toggle, so toBeHidden
passed against a list that had yet to render. The test now waits for the
WooCommerce list to hold an option before requiring the Product Gallery
option to be absent.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the Cart block shipping row is absent when store shipping is off
On the Cart block the row is labelled 'Shipping', so a check for hidden
'Delivery' text matched nothing either way. The assertion now targets the
Cart block's own shipping totals row.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the checkout link builds exactly the cart it describes
The logged-in title read only the two product names out of the order
summary, so quantities, line totals, the discount, the total and the cart
emptying that precedes the link were all unobserved. The fixture now
creates a third, unrelated product that goes into the cart first, and the
link carries an explicit quantity for one product and the default for the
other. The summary is asserted item by item, together with the coupon
discount line and the order total, and the pre-seeded product has to be
gone. The products are virtual so the total is the prices and the coupon.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the generated CSV holds only the selected products
No export was ever run: the title stopped at the export screen. It now
clicks Generate CSV, reads the downloaded file, and requires its non-
variation rows to be exactly the two selected products. The title says what
the body covers, including the clearing step it ends on.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert Apply honours a keep-yours choice alongside a use-core one
With one conflict switched to use-core and nothing left on keep-yours, an
Apply that took core wholesale passed. The fixture now carries merchant
edits on two blocks, the second conflict stays on keep-yours, and the
persisted content is checked for both: core for the switched conflict and
the merchant text for the kept one.
The third block is core-only, so the drawer lists it as auto-resolved and
says the update will apply, while the applier defaults it to keep_yours.
That contradiction is named in a comment and left unasserted.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the preview email reaches the mailer and the header image URL
The send-preview title's only post-click assertion was the generic failure
message, which a broken send path also produces. It now also requires a
logged message addressed to the entered recipient with the previewed
email's subject, so the preview has to reach the mailer. The recipient is
unique per run because the core store keeps its mail log.
The image title checked only that the preview img appears and the upload
icon goes away. It now requires the img src and the
woocommerce_email_header_image input to hold the picked media URL, and the
input to be empty after Remove.
The live-preview title also covers the initial render and the preview-type
switch, so its name now says so.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Guard the WP-CLI product IDs and parse the download link URL
Three specs took a product ID from WP-CLI output with `/\d+/g` and
`.pop()`. npm writes its own banner to the same stdout, so any digits
in it could pick the wrong product, and an empty result passed
`undefined` into the next command. Take the line that holds nothing but
digits and throw with the raw output when there is none, as the
featured-product spec does.
The order confirmation spec matched the download link against a regex
that fixed the order of `download_file`, `order` and `key`. Parse the
href and assert each parameter, so a reordered URL still passes and the
failure names the missing parameter.
The product card lookup used `new RegExp` on the product title, which
reads title characters as pattern syntax. Match the title text exactly.
Say in a comment which two formats the rating name expression accepts.
The guard proved itself locally: against a database without the Blocks
seed it stopped the test with "Failed to find Belt". The download link
mutation (the block printing the product URL) still turns the test red,
now with "The download link should carry download_file".
Refs #68889
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert a classic theme renders the classic notice templates
The only browser title that shows a cart notice under a classic theme
activates the child theme that opts in to block notices, so it renders
the block banners. Since the notices coverage moved to PHPUnit, no
browser test loads a page where the classic success and error templates
print, and switching the opt-in filter's default to true left every
browser title green.
Add a title that stays on Storefront without the opt-in: applying a
coupon prints `.woocommerce-message` inside the notices wrapper, an
out-of-stock cart item prints `.woocommerce-error` there, and no block
notice banner is on the page. The out-of-stock setup moves into a helper
both titles use.
The title fails with the filter default set to true (the success
notice is a block banner) and with the error template's class changed
(the error notice is not found); the opt-in title passes under both.
Refs TESTOPS-234
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Assert the edit order screen shows its heading and order number
The removed "can view single order" title was the only browser check
that the order edit screen prints its "Edit order" heading. Since then
the heading is asserted only by the optional k6 performance job, which
runs when tests/performance changes. Printing the "Add new order" label
on the edit screen left all thirteen titles of the spec green.
Add a title that opens the order created for the spec and asserts the
page heading and the "Order #<id>" heading of the order data box. With
the edit screen printing the add-new label, this title alone fails.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): Describe the three remaining specs in the changelog entry
The entry still described the PR before its two trims: sixteen titles,
two new titles for the classic notice templates and the edit order
screen heading, and five documented sole-owner titles. The 2026-09-28
trim dropped twelve specs and both new titles, and merging trunk on
2026-10-07 dropped the Checkout Link spec after trunk's #69096 deleted
the title this PR hardened.
Name the three specs that remain: product export, the email settings
preview and the email editor review drawer.
Refs TESTOPS-234
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(changelog): Make the retained browser titles entry comment-only
The entry carried a body, so it would have shipped to the public
changelog as a "Dev -" line. The change only tightens test assertions
and has no merchant-facing effect.
Switch it to a Comment so reviewers still see it but the release notes
stay clean, as requested in review.
Refs TESTOPS-234
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/testops-234-harden-retained-browser-titles b/plugins/woocommerce/changelog/testops-234-harden-retained-browser-titles
new file mode 100644
index 00000000000..f18e916ebcf
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-harden-retained-browser-titles
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Tighten product export and email browser title tests; no production change.
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts
index 9726cd75336..64e268b89ac 100644
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts
@@ -81,10 +81,11 @@ test.describe(
'<!-- wp:paragraph --><p>OLD BLOCK B</p><!-- /wp:paragraph -->' +
'<!-- wp:paragraph --><p>OLD BLOCK C</p><!-- /wp:paragraph -->';
- const customized = oldHtml.replace(
- 'OLD BLOCK A',
- 'MERCHANT EDITED A'
- );
+ // Two merchant edits, so the drawer offers a choice per conflict, and one
+ // block the merchant leaves alone.
+ const customized = oldHtml
+ .replace( 'OLD BLOCK A', 'MERCHANT EDITED A' )
+ .replace( 'OLD BLOCK B', 'MERCHANT EDITED B' );
const newCanonical =
'<!-- wp:paragraph --><p>NEW CORE A</p><!-- /wp:paragraph -->' +
@@ -139,11 +140,13 @@ test.describe(
drawer.getByRole( 'heading', { name: /needs your attention/i } )
).toBeVisible( { timeout: 15000 } );
- const firstRadioGroup = drawer
- .getByRole( 'radiogroup', {
- name: /choose which version to apply/i,
- } )
- .first();
+ const radioGroups = drawer.getByRole( 'radiogroup', {
+ name: /choose which version to apply/i,
+ } );
+ await expect( radioGroups ).toHaveCount( 2 );
+
+ const firstRadioGroup = radioGroups.first();
+ const secondRadioGroup = radioGroups.nth( 1 );
await expect(
firstRadioGroup.getByRole( 'radio', { name: /keep yours/i } )
).toHaveAttribute( 'aria-checked', 'true' );
@@ -154,13 +157,18 @@ test.describe(
firstRadioGroup.getByRole( 'radio', { name: /use core/i } )
).toHaveAttribute( 'aria-checked', 'true' );
- // B and C were changed by core only. Assert the drawer's promise about
- // them before applying, so the claim and the result stay tied together.
+ // The second conflict stays on keep-yours, so Apply has to honour both
+ // choices rather than one of them.
+ await expect(
+ secondRadioGroup.getByRole( 'radio', { name: /keep yours/i } )
+ ).toHaveAttribute( 'aria-checked', 'true' );
+
+ // C was changed by core only, so the drawer promises to apply its update.
await expect(
drawer.getByText(
/your version was unchanged, so the update will apply/i
)
- ).toHaveCount( 2 );
+ ).toHaveCount( 1 );
await drawer.getByRole( 'button', { name: /^apply/i } ).click();
await expect( drawer ).toBeHidden( { timeout: 15000 } );
@@ -169,7 +177,8 @@ test.describe(
expect( content ).toContain( 'NEW CORE A' );
expect( content ).not.toContain( 'MERCHANT EDITED A' );
- expect( content ).toContain( 'NEW CORE B' );
+ expect( content ).toContain( 'MERCHANT EDITED B' );
+ expect( content ).not.toContain( 'NEW CORE B' );
expect( content ).toContain( 'NEW CORE C' );
expect( content ).not.toContain( 'OLD BLOCK B' );
expect( content ).not.toContain( 'OLD BLOCK C' );
diff --git a/plugins/woocommerce/tests/e2e/tests/email/settings-email.spec.ts b/plugins/woocommerce/tests/e2e/tests/email/settings-email.spec.ts
index 78a11d2f306..5481351b799 100644
--- a/plugins/woocommerce/tests/e2e/tests/email/settings-email.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email/settings-email.spec.ts
@@ -10,6 +10,7 @@ import { setFeatureEmailImprovementsFlag } from './helpers/set-email-improvement
import { disableEmailEditor } from '../email-editor/helpers/enable-email-editor-feature';
import { tags, locks } from '../../fixtures/fixtures';
import { ADMIN_STATE_PATH } from '../../playwright.config';
+import { expectEmail } from '../../utils/email';
const pickImageFromLibrary = async ( page: Page, imageName: string ) => {
await page.getByRole( 'tab', { name: 'Media Library' } ).click();
@@ -35,7 +36,7 @@ test.describe(
} );
test(
- 'Live preview when changing email settings',
+ 'Email preview renders, switches type, and updates live when settings change',
{ tag: [ tags.SKIP_ON_EXTERNAL_ENV ] },
async ( { page, baseURL } ) => {
await setFeatureEmailImprovementsFlag( baseURL, 'no' );
@@ -146,8 +147,10 @@ test.describe(
} );
await expect( sendButton ).toBeDisabled();
- // Fill in the email address field
- const email = 'test@example.com';
+ // Fill in the email address field. The recipient is unique per run so the
+ // mail log assertion below finds this run's message on a store that keeps
+ // the ones before it.
+ const email = `preview-${ Date.now() }@example.com`;
const emailInput = modal.getByLabel( 'Send to' );
await emailInput.fill( email );
@@ -155,12 +158,20 @@ test.describe(
await expect( sendButton ).toBeEnabled();
await sendButton.click();
- // Sending fails in the test env (no mail server); the backend returns
+ // Delivery fails in the test env (no mail server); the backend returns
// `woocommerce_rest_email_preview_not_sent`, which hits the generic fallback in friendlyEmailSendError.
const message = modal.locator(
"text=Couldn't send the test email. Check your email settings and try again."
);
await expect( message ).toBeVisible();
+
+ // The preview email still reaches the mailer, addressed to the entered
+ // recipient and carrying the previewed email's own subject.
+ await expectEmail(
+ page,
+ email,
+ new RegExp( `Your ${ storeName } order has been received!` )
+ );
} );
test( 'Choose image in email image url field', async ( { page } ) => {
@@ -175,6 +186,18 @@ test.describe(
await expect( page.locator( logoImageElement ) ).toBeVisible();
await expect( page.locator( uploadIconElement ) ).toBeHidden();
+ // The preview and the setting both hold the URL of the picked image.
+ const headerImageInput = page.locator(
+ '#woocommerce_email_header_image'
+ );
+ const pickedImageUrl = await page
+ .locator( logoImageElement )
+ .getAttribute( 'src' );
+ expect( pickedImageUrl ).toMatch( /image-03/ );
+ await expect( headerImageInput ).toHaveValue(
+ pickedImageUrl as string
+ );
+
// Remove an image
await page
.locator( '#wc_settings_email_image_url_slotfill' )
@@ -182,6 +205,7 @@ test.describe(
.click();
await expect( page.locator( logoImageElement ) ).toBeHidden();
await expect( page.locator( uploadIconElement ) ).toBeVisible();
+ await expect( headerImageInput ).toHaveValue( '' );
} );
}
);
diff --git a/plugins/woocommerce/tests/e2e/tests/product/product-export.spec.ts b/plugins/woocommerce/tests/e2e/tests/product/product-export.spec.ts
index 26f71a16138..9af44821202 100644
--- a/plugins/woocommerce/tests/e2e/tests/product/product-export.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/product/product-export.spec.ts
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import { readFile } from 'node:fs/promises';
import { WC_API_PATH } from '@woocommerce/e2e-utils-playwright';
/**
@@ -68,7 +69,7 @@ const test = baseTest.extend( {
} );
test.describe( 'Product > Export Selected Products', () => {
- test( 'preserves multiple selection through export and clear', async ( {
+ test( 'carries a multi-product selection into the export and the generated CSV, then clears it', async ( {
page,
productsFixture,
} ) => {
@@ -136,6 +137,41 @@ test.describe( 'Product > Export Selected Products', () => {
).toBeVisible();
} );
+ await test.step( 'Generate the CSV and verify it holds only the selected products', async () => {
+ const downloadPromise = page.waitForEvent( 'download' );
+ await page.getByRole( 'button', { name: 'Generate CSV' } ).click();
+ const download = await downloadPromise;
+ const csv = await readFile( await download.path(), 'utf8' );
+
+ // Keep only rows that start with an id, so a quoted field holding a
+ // newline cannot be read as a row of its own.
+ const dataRows = csv
+ .trim()
+ .split( '\n' )
+ .slice( 1 )
+ .filter( ( row ) => /^\d+,/.test( row ) );
+ const parentIds = dataRows
+ .map( ( row ) => row.split( ',' ) )
+ .filter(
+ ( columns ) =>
+ // The Type column is a quoted list for a variation that
+ // is also virtual or downloadable.
+ ! columns[ 1 ]
+ .replace( /^"/, '' )
+ .startsWith( 'variation' )
+ )
+ .map( ( columns ) => columns[ 0 ] );
+
+ expect( parentIds.toSorted() ).toEqual(
+ [
+ String( simpleProduct.id ),
+ String( variableProduct.id ),
+ ].toSorted()
+ );
+ expect( csv ).toContain( simpleProduct.name );
+ expect( csv ).toContain( variableProduct.name );
+ } );
+
await test.step( 'Clear the selected products', async () => {
await page
.getByRole( 'link', { name: 'clear your selection' } )