[Blocks] Migrate Attribute filter E2E tests to playwright (#46591)

* try db reset in page teardown

* move reset to setup step

* Use wp db cli

* Fix global setup
That part is overriding the logged in user state and wipes out the nonce and rootUrl fields.

* Try importing instead from a generated dump

* Revert "Try importing instead from a generated dump"

This reverts commit 987dc471c9.

* Revert "Revert "Try importing instead from a generated dump""

This reverts commit c8d008cb20.

* Don't bypass visitSiteEditor so that the Welcome Guide is closed

* use createNewPost

* Revert "Revert "Revert "Try importing instead from a generated dump"""

This reverts commit 2684273582.

* [Blocks]: Fix E2E tests (#46344)

* Load local pickup enabled setting as bool not string

* Add changelog

* Remove unused import

* Update plugins/woocommerce-blocks/assets/js/extensions/shipping-methods/pickup-location/utils.ts

Co-authored-by: Niels Lange <info@nielslange.de>

* Use strict equality test

* try now

* try now

* add a new sql file

* remove default.sql

* fix welcome guide tour

* fix E2E tests

* Fix the template revert tests
...where the template is unreachable due to pagination.

* Add changelog entry

* improve logic to closeWelcomeGuideModal

* fix cart checkout tests

* improve flakiness

* improve flakiness

* fix flaky test

* fix company field

* fix E2E test

* fix E2E tests

---------

Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com>
Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
Co-authored-by: Niels Lange <info@nielslange.de>
Co-authored-by: Bart Kalisz <bartlomiej.kalisz@gmail.com>

* Replace beforeAll w/ beforeEach + remove all after* hooks

* Fix broken hooks

* Activate plugins via requestUtils API

* [Blocks - E2E]: Add `playwright/no-hooks` ESlint rule (#46432)

add ESLint configuration

* Clean up console logs

* Remove obsolete language setup steps

* Remove more unnecessary setup steps

* Remove even more obsolete setup steps

* tmp: add the LYS fix

* Try stabilizing the company field test

* Use canvas param instead of manually entering edit mode

* Remove double site editor redirect

* Blocks: Migrate attribute filter E2E tests to playwright

* Add changefile(s) from automation for the following project(s): woocommerce-blocks

* Revert "Use canvas param instead of manually entering edit mode"

This reverts commit 5e6cc17154.

* Revert "Remove double site editor redirect"

This reverts commit 69a57a82a8.

* Fix flaky products sorting test

* Blocks: Fix ESLint errors (#46595)

fix eslint error

* update path

* Fix ESLint errors (#46626)

* Fix ESLint errors

* Add changefile(s) from automation for the following project(s): woocommerce-blocks

---------

Co-authored-by: github-actions <github-actions@github.com>

* address feedback

* fix visitSiteEditor

* fix description

* remove not necessary changelog

---------

Co-authored-by: Bart Kalisz <bartlomiej.kalisz@gmail.com>
Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com>
Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
Co-authored-by: Niels Lange <info@nielslange.de>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Luigi Teschio 2024-04-26 14:35:03 +02:00 committed by GitHub
parent 8d2f88da71
commit 45a4817772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 300 additions and 535 deletions

View File

@ -1,192 +0,0 @@
/**
* External dependencies
*/
import {
switchBlockInspectorTab,
switchUserToAdmin,
} from '@wordpress/e2e-test-utils';
import {
visitBlockPage,
saveOrPublish,
selectBlockByName,
} from '@woocommerce/blocks-test-utils';
/**
* Internal dependencies
*/
import { openSettingsSidebar } from '../../utils.js';
const block = {
name: 'Filter by Attribute',
slug: 'woocommerce/attribute-filter',
class: '.wc-block-attribute-filter',
};
describe( `${ block.name } Block`, () => {
beforeAll( async () => {
await switchUserToAdmin();
await visitBlockPage( `${ block.name } Block` );
await page.waitForSelector( 'span.woocommerce-search-list__item-name' );
// eslint-disable-next-line jest/no-standalone-expect
await expect( page ).toClick(
'span.woocommerce-search-list__item-name',
{ text: 'Capacity' }
);
// eslint-disable-next-line jest/no-standalone-expect
await expect( page ).toClick(
'span.woocommerce-search-list__item-name',
{ text: 'Capacity' }
);
// eslint-disable-next-line jest/no-standalone-expect
await expect( page ).toClick(
'.wp-block-woocommerce-attribute-filter button',
{ text: 'Done' }
);
await page.waitForTimeout( 30000 );
await page.waitForNetworkIdle();
} );
it( 'renders without crashing', async () => {
await expect( page ).toRenderBlock( block );
} );
it( 'renders correctly', async () => {
expect(
await page.$$eval(
'.wc-block-attribute-filter-list li',
( attributes ) => attributes.length
)
// our test data loads 2 for the Capacity attribute.
).toBeGreaterThanOrEqual( 2 );
} );
describe( 'Attributes', () => {
beforeEach( async () => {
await openSettingsSidebar();
await selectBlockByName( block.slug );
await switchBlockInspectorTab( 'Settings' );
} );
it( "allows changing the block's title", async () => {
const textareaSelector =
'.wp-block-woocommerce-filter-wrapper .wp-block-heading';
await expect( page ).toFill( textareaSelector, 'New Title' );
await expect( page ).toMatchElement(
'.wp-block-woocommerce-filter-wrapper',
{ text: 'New Title' }
);
await expect( page ).toFill(
textareaSelector,
'Filter by Capacity'
);
} );
it( 'can hide product count', async () => {
await expect( page ).not.toMatchElement(
'.wc-filter-element-label-list-count'
);
await expect( page ).toClick( 'label', {
text: 'Display product count',
} );
await expect( page ).toMatchElement(
'.wc-filter-element-label-list-count'
);
// reset
await expect( page ).toClick( 'label', {
text: 'Display product count',
} );
} );
it( 'can toggle go button', async () => {
await expect( page ).not.toMatchElement(
'.wc-block-filter-submit-button'
);
await expect( page ).toClick( 'label', {
text: "Show 'Apply filters' button",
} );
await expect( page ).toMatchElement(
'.wc-block-filter-submit-button'
);
// reset
await expect( page ).toClick( 'label', {
text: "Show 'Apply filters' button",
} );
} );
it( 'can switch attribute', async () => {
await expect( page ).toClick( 'button', {
text: 'Content Settings',
} );
await expect( page ).toClick(
'span.woocommerce-search-list__item-name',
{
text: 'Capacity',
}
);
await page.waitForSelector(
'.wc-block-attribute-filter-list:not(.is-loading)'
);
expect(
await page.$$eval(
'.wc-block-attribute-filter-list li',
( reviews ) => reviews.length
)
// Capacity has only 2 attributes
).toEqual( 2 );
await expect( page ).toClick(
'span.woocommerce-search-list__item-name',
{
text: 'Shade',
}
);
//needed for attributes list to load correctly
await page.waitForTimeout( 1000 );
// reset
await expect( page ).toClick(
'span.woocommerce-search-list__item-name',
{
text: 'Capacity',
}
);
//needed for attributes list to load correctly
await page.waitForTimeout( 1000 );
} );
it( 'renders on the frontend', async () => {
await saveOrPublish();
const link = await page.evaluate( () =>
wp.data.select( 'core/editor' ).getPermalink()
);
await page.goto( link, { waitUntil: 'networkidle2' } );
await page.waitForSelector(
'.wp-block-woocommerce-attribute-filter'
);
await expect( page ).toMatchElement(
'.wp-block-woocommerce-filter-wrapper',
{
text: 'Filter by Capacity',
}
);
await page.waitForSelector(
'.wc-block-checkbox-list:not(.is-loading)'
);
expect(
await page.$$eval(
'.wc-block-attribute-filter-list li',
( reviews ) => reviews.length
)
// Capacity has only two attributes
).toEqual( 2 );
} );
} );
} );

View File

@ -1,343 +0,0 @@
/**
* External dependencies
*/
import {
canvas,
createNewPost,
deleteAllTemplates,
insertBlock,
switchUserToAdmin,
publishPost,
} from '@wordpress/e2e-test-utils';
import { selectBlockByName } from '@woocommerce/blocks-test-utils';
/**
* Internal dependencies
*/
import {
BASE_URL,
enableApplyFiltersButton,
goToTemplateEditor,
insertAllProductsBlock,
saveTemplate,
useTheme,
waitForAllProductsBlockLoaded,
waitForCanvas,
} from '../../utils';
import { saveOrPublish } from '../../../utils';
const block = {
name: 'Filter by Attribute',
slug: 'woocommerce/attribute-filter',
class: '.wc-block-attribute-filter',
selectors: {
editor: {
firstAttributeInTheList:
'.woocommerce-search-list__list > li > label > input.woocommerce-search-list__item-input',
doneButton: '.wc-block-attribute-filter__selection > button',
},
frontend: {
firstAttributeInTheList:
'.wc-block-attribute-filter-list > li:not([class^="is-loading"])',
productsList: '.wc-block-grid__products > li',
queryProductsList: '.wp-block-post-template > li',
classicProductsList: '.products.columns-3 > li',
filter: "input[id='128gb']",
submitButton: '.wc-block-components-filter-submit-button',
},
},
urlSearchParamWhenFilterIsApplied:
'?filter_capacity=128gb&query_type_capacity=or',
foundProduct: '128GB USB Stick',
};
const { selectors } = block;
const goToShopPage = () =>
page.goto( BASE_URL + '/shop', {
waitUntil: 'networkidle0',
} );
describe( `${ block.name } Block`, () => {
const insertFilterByAttributeBlock = async () => {
await insertBlock( block.name );
const canvasEl = canvas();
// It seems that .click doesn't work well with radio input element.
await canvasEl.$eval(
block.selectors.editor.firstAttributeInTheList,
( el ) => ( el as HTMLInputElement ).click()
);
await canvasEl.click( selectors.editor.doneButton );
};
describe( 'with All Products Block', () => {
beforeAll( async () => {
await switchUserToAdmin();
await createNewPost( {
postType: 'post',
title: block.name,
} );
await insertAllProductsBlock();
await insertFilterByAttributeBlock();
await publishPost();
const link = await page.evaluate( () =>
wp.data.select( 'core/editor' ).getPermalink()
);
await page.goto( link );
} );
it.skip( 'should render products', async () => {
await waitForAllProductsBlockLoaded();
const products = await page.$$( selectors.frontend.productsList );
expect( products ).toHaveLength( 5 );
} );
it.skip( 'should show only products that match the filter', async () => {
const isRefreshed = jest.fn( () => void 0 );
page.on( 'load', isRefreshed );
await page.waitForSelector( selectors.frontend.filter );
await page.click( selectors.frontend.filter );
await waitForAllProductsBlockLoaded();
const products = await page.$$( selectors.frontend.productsList );
expect( isRefreshed ).not.toBeCalled();
expect( products ).toHaveLength( 1 );
await expect( page ).toMatch( block.foundProduct );
} );
} );
describe.skip( 'with PHP classic template (Products Block and Classic Template Block)', () => {
const productCatalogTemplateId =
'woocommerce/woocommerce//archive-product';
useTheme( 'emptytheme' );
beforeAll( async () => {
await deleteAllTemplates( 'wp_template' );
await deleteAllTemplates( 'wp_template_part' );
await goToTemplateEditor( {
postId: productCatalogTemplateId,
} );
await insertBlock( 'WooCommerce Product Grid Block' );
await insertFilterByAttributeBlock();
await saveTemplate();
} );
afterAll( async () => {
await deleteAllTemplates( 'wp_template' );
await deleteAllTemplates( 'wp_template_part' );
} );
beforeEach( async () => {
await goToShopPage();
} );
it( 'should render products', async () => {
const products = await page.$$(
selectors.frontend.classicProductsList
);
const productsBlockProductsList = await page.$$(
selectors.frontend.queryProductsList
);
expect( productsBlockProductsList ).toHaveLength( 5 );
expect( products ).toHaveLength( 5 );
} );
it( 'should show only products that match the filter', async () => {
const isRefreshed = jest.fn( () => void 0 );
page.on( 'load', isRefreshed );
await page.waitForSelector( block.class + '.is-loading', {
hidden: true,
} );
expect( isRefreshed ).not.toBeCalled();
await page.waitForSelector( selectors.frontend.filter );
await Promise.all( [
page.click( selectors.frontend.filter ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
const products = await page.$$(
selectors.frontend.classicProductsList
);
const pageURL = page.url();
const parsedURL = new URL( pageURL );
const productsBlockProductsList = await page.$$(
selectors.frontend.queryProductsList
);
expect( isRefreshed ).toBeCalledTimes( 1 );
expect( products ).toHaveLength( 1 );
expect( productsBlockProductsList ).toHaveLength( 1 );
await expect( page ).toMatch( block.foundProduct );
expect( parsedURL.search ).toEqual(
block.urlSearchParamWhenFilterIsApplied
);
} );
it.skip( 'should refresh the page only if the user clicks on button', async () => {
await goToTemplateEditor( {
postId: productCatalogTemplateId,
} );
await waitForCanvas();
await selectBlockByName( block.slug );
await enableApplyFiltersButton();
await saveTemplate();
await goToShopPage();
const isRefreshed = jest.fn( () => void 0 );
page.on( 'load', isRefreshed );
await page.waitForSelector( block.class + '.is-loading', {
hidden: true,
} );
await page.waitForSelector( selectors.frontend.filter );
await page.click( selectors.frontend.filter );
expect( isRefreshed ).not.toBeCalled();
await Promise.all( [
page.waitForNavigation( {
waitUntil: 'networkidle0',
} ),
page.click( selectors.frontend.submitButton ),
] );
const products = await page.$$(
selectors.frontend.classicProductsList
);
const productsBlockProductsList = await page.$$(
selectors.frontend.queryProductsList
);
const pageURL = page.url();
const parsedURL = new URL( pageURL );
expect( isRefreshed ).toBeCalledTimes( 1 );
expect( products ).toHaveLength( 1 );
expect( productsBlockProductsList ).toHaveLength( 1 );
await expect( page ).toMatch( block.foundProduct );
expect( parsedURL.search ).toEqual(
block.urlSearchParamWhenFilterIsApplied
);
} );
} );
describe( 'with Product Query Block', () => {
let editorPageUrl = '';
let frontedPageUrl = '';
useTheme( 'emptytheme' );
beforeAll( async () => {
await switchUserToAdmin();
await createNewPost( {
postType: 'post',
title: block.name,
} );
await insertBlock( 'Products (Beta)' );
await insertFilterByAttributeBlock();
await publishPost();
editorPageUrl = page.url();
frontedPageUrl = await page.evaluate( () =>
wp.data.select( 'core/editor' ).getPermalink()
);
await page.goto( frontedPageUrl, { waitUntil: 'networkidle2' } );
} );
it.skip( 'should render products', async () => {
const products = await page.$$(
selectors.frontend.queryProductsList
);
expect( products ).toHaveLength( 5 );
} );
it( 'should show only products that match the filter', async () => {
const isRefreshed = jest.fn( () => void 0 );
page.on( 'load', isRefreshed );
await page.waitForSelector( block.class + '.is-loading', {
hidden: true,
} );
expect( isRefreshed ).not.toBeCalled();
await page.waitForSelector( selectors.frontend.filter );
await Promise.all( [
page.click( selectors.frontend.filter ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
const products = await page.$$(
selectors.frontend.queryProductsList
);
const pageURL = page.url();
const parsedURL = new URL( pageURL );
expect( isRefreshed ).toBeCalledTimes( 1 );
expect( products ).toHaveLength( 1 );
await expect( page ).toMatch( block.foundProduct );
expect( parsedURL.search ).toEqual(
block.urlSearchParamWhenFilterIsApplied
);
} );
it( 'should refresh the page only if the user clicks on button', async () => {
await page.goto( editorPageUrl );
await waitForCanvas();
await selectBlockByName( block.slug );
await enableApplyFiltersButton();
await saveOrPublish();
await page.goto( frontedPageUrl );
const isRefreshed = jest.fn( () => void 0 );
page.on( 'load', isRefreshed );
await page.waitForSelector( block.class + '.is-loading', {
hidden: true,
} );
await page.waitForSelector( selectors.frontend.filter );
await page.click( selectors.frontend.filter );
expect( isRefreshed ).not.toBeCalled();
await Promise.all( [
page.waitForNavigation( {
waitUntil: 'networkidle0',
} ),
page.click( selectors.frontend.submitButton ),
] );
const products = await page.$$(
selectors.frontend.queryProductsList
);
const pageURL = page.url();
const parsedURL = new URL( pageURL );
expect( isRefreshed ).toBeCalledTimes( 1 );
expect( products ).toHaveLength( 1 );
await expect( page ).toMatch( block.foundProduct );
expect( parsedURL.search ).toEqual(
block.urlSearchParamWhenFilterIsApplied
);
} );
} );
} );

View File

@ -0,0 +1,296 @@
/**
* External dependencies
*/
import { test as base, expect } from '@woocommerce/e2e-playwright-utils';
import { cli } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
import ProductCollectionPage from '../product-collection/product-collection.page';
const blockData = {
name: 'Filter by Attribute',
slug: 'woocommerce/attribute-filter',
urlSearchParamWhenFilterIsApplied: 'filter_size=small&query_type_size=or',
};
const test = base.extend< {
productCollectionPageObject: ProductCollectionPage;
} >( {
productCollectionPageObject: async (
{ page, admin, editor, templateApiUtils, editorUtils },
use
) => {
const pageObject = new ProductCollectionPage( {
page,
admin,
editor,
templateApiUtils,
editorUtils,
} );
await use( pageObject );
},
} );
test.describe( `${ blockData.name } Block`, () => {
test.beforeEach( async ( { admin, editor, editorUtils } ) => {
await admin.createNewPost();
await editor.insertBlock( {
name: 'woocommerce/filter-wrapper',
attributes: {
filterType: 'attribute-filter',
heading: 'Filter By Attribute',
},
} );
const attributeFilter = await editorUtils.getBlockByName(
blockData.slug
);
await attributeFilter.getByText( 'Size' ).click();
await attributeFilter.getByText( 'Done' ).click();
await editor.openDocumentSettingsSidebar();
} );
test( "should allow changing the block's title", async ( { page } ) => {
const textSelector =
'.wp-block-woocommerce-filter-wrapper .wp-block-heading';
const title = 'New Title';
await page.locator( textSelector ).fill( title );
await expect( page.locator( textSelector ) ).toHaveText( title );
} );
test( 'should allow changing the display style', async ( {
page,
editorUtils,
editor,
} ) => {
const attributeFilter = await editorUtils.getBlockByName(
blockData.slug
);
await editor.selectBlocks( attributeFilter );
await expect(
page.getByRole( 'checkbox', { name: 'Small' } )
).toBeVisible();
await page.getByLabel( 'DropDown' ).click();
await expect(
attributeFilter.getByRole( 'checkbox', {
name: 'Small',
} )
).toBeHidden();
await expect(
page.getByRole( 'checkbox', { name: 'Small' } )
).toBeHidden();
await expect( page.getByRole( 'combobox' ) ).toBeVisible();
} );
test( 'should allow toggling the visibility of the filter button', async ( {
page,
editorUtils,
editor,
} ) => {
const attributeFilter = await editorUtils.getBlockByName(
blockData.slug
);
await editor.selectBlocks( attributeFilter );
await expect(
attributeFilter.getByRole( 'button', {
name: 'Apply',
} )
).toBeHidden();
await page.getByText( "Show 'Apply filters' button" ).click();
await expect(
attributeFilter.getByRole( 'button', {
name: 'Apply',
} )
).toBeVisible();
} );
} );
test.describe( `${ blockData.name } Block - with PHP classic template`, () => {
test.beforeEach( async ( { admin, page, editor, editorUtils } ) => {
await cli(
'npm run wp-env run tests-cli -- wp option update wc_blocks_use_blockified_product_grid_block_as_template false'
);
await admin.visitSiteEditor( {
postId: 'woocommerce/woocommerce//archive-product',
postType: 'wp_template',
} );
await editorUtils.enterEditMode();
await editor.insertBlock( {
name: 'woocommerce/filter-wrapper',
attributes: {
filterType: 'attribute-filter',
heading: 'Filter By Attribute',
},
} );
const attributeFilter = await editorUtils.getBlockByName(
blockData.slug
);
await attributeFilter.getByText( 'Size' ).click();
await attributeFilter.getByText( 'Done' ).click();
await editor.saveSiteEditorEntities();
await page.goto( `/shop` );
} );
test( 'should show all products', async ( { frontendUtils, page } ) => {
const legacyTemplate = await frontendUtils.getBlockByName(
'woocommerce/legacy-template'
);
const products = legacyTemplate
.getByRole( 'list' )
.locator( '.product' );
await expect( products ).toHaveCount( 16 );
await expect(
page.getByRole( 'checkbox', { name: 'Small' } )
).toBeVisible();
await expect(
page.getByRole( 'checkbox', { name: 'Medium' } )
).toBeVisible();
await expect(
page.getByRole( 'checkbox', { name: 'Large' } )
).toBeVisible();
} );
test( 'should show only products that match the filter', async ( {
frontendUtils,
page,
} ) => {
await page.getByRole( 'checkbox', { name: 'Small' } ).click();
const legacyTemplate = await frontendUtils.getBlockByName(
'woocommerce/legacy-template'
);
const products = legacyTemplate
.getByRole( 'list' )
.locator( '.product' );
await expect( page ).toHaveURL(
new RegExp( blockData.urlSearchParamWhenFilterIsApplied )
);
await expect( products ).toHaveCount( 1 );
} );
} );
test.describe( `${ blockData.name } Block - with Product Collection`, () => {
test.beforeEach(
async ( {
admin,
editorUtils,
productCollectionPageObject,
editor,
} ) => {
await admin.createNewPost();
await productCollectionPageObject.insertProductCollection();
await productCollectionPageObject.chooseCollectionInPost(
'productCatalog'
);
await editor.insertBlock( {
name: 'woocommerce/filter-wrapper',
attributes: {
filterType: 'attribute-filter',
heading: 'Filter By Attribute',
},
} );
const attributeFilter = await editorUtils.getBlockByName(
blockData.slug
);
await attributeFilter.getByText( 'Size' ).click();
await attributeFilter.getByText( 'Done' ).click();
await editorUtils.publishAndVisitPost();
}
);
test( 'should show all products', async ( { page } ) => {
const products = page
.locator( '.wp-block-woocommerce-product-template' )
.getByRole( 'listitem' );
await expect( products ).toHaveCount( 9 );
} );
test( 'should show only products that match the filter', async ( {
page,
} ) => {
await page.getByRole( 'checkbox', { name: 'Small' } ).click();
await expect( page ).toHaveURL(
new RegExp( blockData.urlSearchParamWhenFilterIsApplied )
);
const products = page
.locator( '.wp-block-woocommerce-product-template' )
.getByRole( 'listitem' );
await expect( products ).toHaveCount( 1 );
} );
test( 'should refresh the page only if the user clicks on button', async ( {
page,
admin,
editor,
editorUtils,
productCollectionPageObject,
} ) => {
await admin.createNewPost();
await productCollectionPageObject.insertProductCollection();
await productCollectionPageObject.chooseCollectionInPost(
'productCatalog'
);
await editor.insertBlock( {
name: 'woocommerce/filter-wrapper',
attributes: {
filterType: 'attribute-filter',
heading: 'Filter By Attribute',
},
} );
const attributeFilterControl = await editorUtils.getBlockByName(
blockData.slug
);
await attributeFilterControl.getByText( 'Size' ).click();
await attributeFilterControl.getByText( 'Done' ).click();
await editor.selectBlocks( attributeFilterControl );
await editor.openDocumentSettingsSidebar();
await page.getByText( "Show 'Apply filters' button" ).click();
await editorUtils.publishAndVisitPost();
await page.getByRole( 'checkbox', { name: 'Small' } ).click();
await page.getByRole( 'button', { name: 'Apply' } ).click();
await expect( page ).toHaveURL(
new RegExp( blockData.urlSearchParamWhenFilterIsApplied )
);
const products = page
.locator( '.wp-block-woocommerce-product-template' )
.getByRole( 'listitem' );
await expect( products ).toHaveCount( 1 );
} );
} );

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Comment: Migrate Attribute filter tests to Playwright