From 4ef12ec41e07b3706e330aa87ef5d87b38dec005 Mon Sep 17 00:00:00 2001 From: Jorge M Date: Wed, 14 Aug 2024 18:03:53 +0200 Subject: [PATCH 1/9] Add shipping_rates_count to the settings endpoint --- .../MerchantCenter/SettingsController.php | 38 +++++++++++++++++-- .../RESTServiceProvider.php | 2 +- src/Shipping/ShippingZone.php | 10 +++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/API/Site/Controllers/MerchantCenter/SettingsController.php b/src/API/Site/Controllers/MerchantCenter/SettingsController.php index e9ffac77e4..aa0a9d4cfa 100644 --- a/src/API/Site/Controllers/MerchantCenter/SettingsController.php +++ b/src/API/Site/Controllers/MerchantCenter/SettingsController.php @@ -6,6 +6,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseOptionsController; use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface; +use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\ShippingZone; +use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer; use WP_REST_Request as Request; defined( 'ABSPATH' ) || exit; @@ -17,6 +19,22 @@ */ class SettingsController extends BaseOptionsController { + /** + * @var ShippingZone + */ + protected $shipping_zone; + + /** + * SettingsController constructor. + * + * @param RESTServer $server + * @param ShippingZone $shipping_zone + */ + public function __construct( RESTServer $server, ShippingZone $shipping_zone ) { + parent::__construct( $server ); + $this->shipping_zone = $shipping_zone; + } + /** * Register rest routes with WordPress. */ @@ -46,9 +64,10 @@ public function register_routes(): void { */ protected function get_settings_endpoint_read_callback(): callable { return function () { - $data = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] ); - $schema = $this->get_schema_properties(); - $items = []; + $data = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] ); + $data['shipping_rates_count'] = $this->shipping_zone->get_shipping_rates_count(); + $schema = $this->get_schema_properties(); + $items = []; foreach ( $schema as $key => $property ) { $items[ $key ] = $data[ $key ] ?? $property['default'] ?? null; } @@ -71,6 +90,9 @@ protected function get_settings_endpoint_edit_callback(): callable { } foreach ( $schema as $key => $property ) { + if ( ! in_array( 'edit', $property['context'] ?? [], true ) ) { + continue; + } $options[ $key ] = $request->get_param( $key ) ?? $options[ $key ] ?? $property['default'] ?? null; } @@ -178,6 +200,16 @@ protected function get_schema_properties(): array { 'validate_callback' => 'rest_validate_request_arg', 'default' => false, ], + 'shipping_rates_count' => [ + 'type' => 'number', + 'description' => __( + 'The number of shipping rates in WC ready to be used in the Merchant Center.', + 'google-listings-and-ads' + ), + 'context' => [ 'view' ], + 'validate_callback' => 'rest_validate_request_arg', + 'default' => 0, + ], ]; } diff --git a/src/Internal/DependencyManagement/RESTServiceProvider.php b/src/Internal/DependencyManagement/RESTServiceProvider.php index 635e91a038..6fac32f01e 100644 --- a/src/Internal/DependencyManagement/RESTServiceProvider.php +++ b/src/Internal/DependencyManagement/RESTServiceProvider.php @@ -105,7 +105,7 @@ public function provides( string $service ): bool { * @return void */ public function register() { - $this->share( SettingsController::class ); + $this->share( SettingsController::class, ShippingZone::class ); $this->share( ConnectionController::class ); $this->share( AdsAccountController::class, AdsAccountService::class ); $this->share( AdsCampaignController::class, AdsCampaign::class ); diff --git a/src/Shipping/ShippingZone.php b/src/Shipping/ShippingZone.php index 98444ef818..fd22ce1404 100644 --- a/src/Shipping/ShippingZone.php +++ b/src/Shipping/ShippingZone.php @@ -77,6 +77,16 @@ public function get_shipping_countries(): array { return array_values( $countries ); } + /** + * Get the number of shipping rates enable in WooCommerce. + * + * @return int + */ + public function get_shipping_rates_count(): int { + $this->parse_shipping_zones(); + return count( $this->location_rates ?? [] ); + } + /** * Returns the available shipping rates for a country and its subdivisions. * From c73ccf9c0aa76e720ebbd1cf64b4cfa1d017a81d Mon Sep 17 00:00:00 2001 From: Jorge M Date: Wed, 14 Aug 2024 18:04:50 +0200 Subject: [PATCH 2/9] Hide automatic method during onboarding and if shipping methods are 0 --- .../shipping-rate-section.js | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/js/src/components/shipping-rate-section/shipping-rate-section.js b/js/src/components/shipping-rate-section/shipping-rate-section.js index 3919aad820..6650ba95dd 100644 --- a/js/src/components/shipping-rate-section/shipping-rate-section.js +++ b/js/src/components/shipping-rate-section/shipping-rate-section.js @@ -14,6 +14,8 @@ import RadioHelperText from '.~/wcdl/radio-helper-text'; import AppDocumentationLink from '.~/components/app-documentation-link'; import VerticalGapLayout from '.~/components/vertical-gap-layout'; import FlatShippingRatesInputCards from './flat-shipping-rates-input-cards'; +import useSettings from '.~/components/free-listings/configure-product-listings/useSettings'; +import useMCSetup from '.~/hooks/useMCSetup'; /** * @fires gla_documentation_link_click with `{ context: 'setup-mc-shipping', link_id: 'shipping-read-more', href: 'https://support.google.com/merchants/answer/7050921' }` @@ -22,8 +24,18 @@ import FlatShippingRatesInputCards from './flat-shipping-rates-input-cards'; const ShippingRateSection = () => { const { getInputProps, values } = useAdaptiveFormContext(); + const { settings } = useSettings(); + const { hasFinishedResolution, data: mcSetup } = useMCSetup(); const inputProps = getInputProps( 'shipping_rate' ); + // Hide the automatic shipping rate option if there are no shipping rates and the merchant is onboarding. + const hideAutomatticShippingRate = + ! settings?.shipping_rates_count && + hasFinishedResolution && + mcSetup?.status === 'incomplete' + ? true + : false; + return (
{ - Recommended: Automatically sync my store’s shipping settings to Google.', - 'google-listings-and-ads' - ), - { - strong: , - } - ) } - value="automatic" - collapsible - > - - { __( - 'My current settings and any future changes to my store’s shipping rates and classes will be automatically synced to Google Merchant Center.', - 'google-listings-and-ads' + { ! hideAutomatticShippingRate && ( + , + } ) } - - + value="automatic" + collapsible + > + + { __( + 'My current settings and any future changes to my store’s shipping rates and classes will be automatically synced to Google Merchant Center.', + 'google-listings-and-ads' + ) } + + + ) } Date: Wed, 14 Aug 2024 18:05:08 +0200 Subject: [PATCH 3/9] Add default shipping rate flat --- js/src/setup-mc/setup-stepper/saved-setup-stepper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js index 000b0a9336..cee8bb0f55 100644 --- a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js +++ b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js @@ -79,7 +79,7 @@ const SavedSetupStepper = ( { savedStep } ) => { if ( settings?.shipping_rate === null ) { saveSettings( { ...settings, - shipping_rate: 'automatic', + shipping_rate: 'flat', shipping_time: 'flat', } ); } From 5b6b03875bb1e96a2f8cdb4af07b7268fec67d28 Mon Sep 17 00:00:00 2001 From: Jorge M Date: Wed, 14 Aug 2024 18:05:41 +0200 Subject: [PATCH 4/9] Add tests for ShippingRateSection --- .../shipping-rate-section.test.js | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 js/src/components/shipping-rate-section/shipping-rate-section.test.js diff --git a/js/src/components/shipping-rate-section/shipping-rate-section.test.js b/js/src/components/shipping-rate-section/shipping-rate-section.test.js new file mode 100644 index 0000000000..8471966c77 --- /dev/null +++ b/js/src/components/shipping-rate-section/shipping-rate-section.test.js @@ -0,0 +1,211 @@ +/** + * External dependencies + */ +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import useSettings from '.~/components/free-listings/configure-product-listings/useSettings'; +import useMCSetup from '.~/hooks/useMCSetup'; +import ShippingRateSection from './shipping-rate-section'; +//import FlatShippingRatesInputCards from './flat-shipping-rates-input-cards'; + +jest.mock( './flat-shipping-rates-input-cards', () => () => <> ); + +jest.mock( '.~/components/adaptive-form', () => ( { + useAdaptiveFormContext: jest + .fn() + .mockName( 'useAdaptiveFormContext' ) + .mockImplementation( () => { + return { + getInputProps: () => { + return { + checked: true, + className: '', + help: null, + onBlur: () => {}, + onChange: () => {}, + selected: 'flat', + value: 'flat', + }; + }, + values: { + countries: [ 'ES' ], + language: 'English', + locale: 'en_US', + location: 'selected', + offer_free_shipping: false, + shipping_country_rates: [], + shipping_country_times: [], + shipping_rate: 'flat', + shipping_time: 'flat', + tax_rate: null, + }, + }; + } ), +} ) ); + +jest.mock( + '.~/components/free-listings/configure-product-listings/useSettings' +); +jest.mock( '.~/hooks/useMCSetup' ); + +describe( 'ShippingRateSection', () => { + it( 'shouldnt render automatic rates if there are not shipping rates and it is onboarding', () => { + useMCSetup.mockImplementation( () => { + return { + hasFinishedResolution: true, + data: { + status: 'incomplete', + }, + }; + } ); + + useSettings.mockImplementation( () => { + return { + settings: { + shipping_rates_count: 0, + }, + }; + } ); + + const { getByText, queryByRole } = render( ); + + expect( + getByText( + 'My shipping settings are simple. I can manually estimate flat shipping rates.' + ) + ).toBeInTheDocument(); + + expect( + getByText( + 'My shipping settings are complex. I will enter my shipping rates and times manually in Google Merchant Center.' + ) + ).toBeInTheDocument(); + + expect( + queryByRole( + 'Automatically sync my store’s shipping settings to Google.' + ) + ).not.toBeInTheDocument(); + } ); + + it( 'should render automatic rates if there are shipping rates and it is onboarding', () => { + useMCSetup.mockImplementation( () => { + return { + hasFinishedResolution: true, + data: { + status: 'incomplete', + }, + }; + } ); + + useSettings.mockImplementation( () => { + return { + settings: { + shipping_rates_count: 1, + }, + }; + } ); + + const { getByText } = render( ); + + expect( + getByText( + 'My shipping settings are simple. I can manually estimate flat shipping rates.' + ) + ).toBeInTheDocument(); + + expect( + getByText( + 'My shipping settings are complex. I will enter my shipping rates and times manually in Google Merchant Center.' + ) + ).toBeInTheDocument(); + + expect( + getByText( + 'Automatically sync my store’s shipping settings to Google.' + ) + ).toBeInTheDocument(); + } ); + + it( 'should render automatic rates if there are not shipping rates and it is not onboarding', () => { + useMCSetup.mockImplementation( () => { + return { + hasFinishedResolution: true, + data: { + status: 'completed', + }, + }; + } ); + + useSettings.mockImplementation( () => { + return { + settings: { + shipping_rates_count: 0, + }, + }; + } ); + + const { getByText } = render( ); + + expect( + getByText( + 'My shipping settings are simple. I can manually estimate flat shipping rates.' + ) + ).toBeInTheDocument(); + + expect( + getByText( + 'My shipping settings are complex. I will enter my shipping rates and times manually in Google Merchant Center.' + ) + ).toBeInTheDocument(); + + expect( + getByText( + 'Automatically sync my store’s shipping settings to Google.' + ) + ).toBeInTheDocument(); + } ); + + it( 'should render automatic rates if there are shipping rates and it is not onboarding', () => { + useMCSetup.mockImplementation( () => { + return { + hasFinishedResolution: true, + data: { + status: 'completed', + }, + }; + } ); + + useSettings.mockImplementation( () => { + return { + settings: { + shipping_rates_count: 1, + }, + }; + } ); + + const { getByText } = render( ); + + expect( + getByText( + 'My shipping settings are simple. I can manually estimate flat shipping rates.' + ) + ).toBeInTheDocument(); + + expect( + getByText( + 'My shipping settings are complex. I will enter my shipping rates and times manually in Google Merchant Center.' + ) + ).toBeInTheDocument(); + + expect( + getByText( + 'Automatically sync my store’s shipping settings to Google.' + ) + ).toBeInTheDocument(); + } ); +} ); From 3c17297cbbce4a9dbcf5e8e16d44945ed1755438 Mon Sep 17 00:00:00 2001 From: Jorge M Date: Wed, 14 Aug 2024 18:06:47 +0200 Subject: [PATCH 5/9] Add tests for Settings Controller --- .../MerchantCenter/SettingsControllerTest.php | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/Unit/API/Site/Controllers/MerchantCenter/SettingsControllerTest.php diff --git a/tests/Unit/API/Site/Controllers/MerchantCenter/SettingsControllerTest.php b/tests/Unit/API/Site/Controllers/MerchantCenter/SettingsControllerTest.php new file mode 100644 index 0000000000..788422da2e --- /dev/null +++ b/tests/Unit/API/Site/Controllers/MerchantCenter/SettingsControllerTest.php @@ -0,0 +1,99 @@ +shipping_zone = $this->createMock( ShippingZone::class ); + $this->options = $this->createMock( OptionsInterface::class ); + $this->controller = new SettingsController( $this->server, $this->shipping_zone ); + $this->controller->set_options_object( $this->options ); + $this->controller->register(); + } + + public function test_get_settings() { + $options = [ + 'shipping_rate' => 'flat', + 'shipping_time' => 'flat', + 'tax_rate' => null, + 'website_live' => true, + 'checkout_process_secure' => true, + 'payment_methods_visible' => true, + 'refund_tos_visible' => true, + 'contact_info_visible' => true, + + ]; + + $this->options->expects( $this->once() )->method( 'get' )->willReturn( + $options + ); + $this->shipping_zone->expects( $this->once() )->method( 'get_shipping_rates_count' )->willReturn( 1 ); + + $expected = $options + [ + 'shipping_rates_count' => 1, + ]; + + $response = $this->do_request( self::ROUTE, 'GET' ); + + $this->assertEquals( $expected, $response->get_data() ); + $this->assertEquals( 200, $response->get_status() ); + } + + public function test_edit_settings() { + $options = [ + 'shipping_rate' => 'flat', + 'shipping_time' => 'flat', + 'tax_rate' => null, + 'website_live' => true, + 'checkout_process_secure' => true, + 'payment_methods_visible' => true, + 'refund_tos_visible' => true, + 'contact_info_visible' => true, + + ]; + + $this->options->expects( $this->once() )->method( 'get' )->willReturn( + $options + ); + + $this->options->expects( $this->once() )->method( 'update' )->with( OptionsInterface::MERCHANT_CENTER, array_merge( $options, [ 'shipping_time' => 'manual' ] ) ); + + $response = $this->do_request( + self::ROUTE, + 'POST', + [ + 'shipping_time' => 'manual', + ] + ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'success', $response->get_data()['status'] ); + } +} From 4d90755565892f977d3effb84dc425f816685513 Mon Sep 17 00:00:00 2001 From: Jorge M Date: Wed, 14 Aug 2024 18:07:07 +0200 Subject: [PATCH 6/9] Add tests for shipping rates count --- tests/Unit/Shipping/ShippingZoneTest.php | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Unit/Shipping/ShippingZoneTest.php b/tests/Unit/Shipping/ShippingZoneTest.php index 235846ef9d..54130962d4 100644 --- a/tests/Unit/Shipping/ShippingZoneTest.php +++ b/tests/Unit/Shipping/ShippingZoneTest.php @@ -152,6 +152,50 @@ public function test_ignores_zones_with_no_locations() { $this->assertEmpty( $this->shipping_zone->get_shipping_countries() ); } + public function test_get_shipping_rates_count() { + $this->wc->expects( $this->exactly( 1 ) ) + ->method( 'get_shipping_zones' ) + ->willReturn( [ [ 'zone_id' => 1 ] ] ); + + $this->locations_parser->expects( $this->once() ) + ->method( 'parse' ) + ->willReturn( + [ + new ShippingLocation( 21137, 'US', 'CA' ), + ] + ); + $this->methods_parser->expects( $this->once() ) + ->method( 'parse' ) + ->willReturn( + [ + new ShippingRate( 10 ), + ] + ); + + $this->assertEquals( 1, $this->shipping_zone->get_shipping_rates_count() ); + } + + public function test_get_shipping_rates_count_with_no_rates() { + $this->wc->expects( $this->exactly( 1 ) ) + ->method( 'get_shipping_zones' ) + ->willReturn( [ [ 'zone_id' => 1 ] ] ); + + $this->locations_parser->expects( $this->once() ) + ->method( 'parse' ) + ->willReturn( + [ + new ShippingLocation( 21137, 'US', 'CA' ), + ] + ); + $this->methods_parser->expects( $this->once() ) + ->method( 'parse' ) + ->willReturn( + [] + ); + + $this->assertEquals( 0, $this->shipping_zone->get_shipping_rates_count() ); + } + /** * Runs before each test is executed. */ From 53d8f4ac57c64da68feb7e2a0c8904772d93625d Mon Sep 17 00:00:00 2001 From: Jorge M Date: Wed, 14 Aug 2024 18:08:23 +0200 Subject: [PATCH 7/9] Adjust e2e tests --- .../setup-mc/step-2-product-listings.test.js | 35 ++++++++++++++++--- tests/e2e/utils/pages/edit-free-listings.js | 2 +- .../pages/setup-mc/step-2-product-listings.js | 2 +- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/tests/e2e/specs/setup-mc/step-2-product-listings.test.js b/tests/e2e/specs/setup-mc/step-2-product-listings.test.js index 25cd591a51..08155d24dc 100644 --- a/tests/e2e/specs/setup-mc/step-2-product-listings.test.js +++ b/tests/e2e/specs/setup-mc/step-2-product-listings.test.js @@ -64,12 +64,13 @@ test.describe( 'Configure product listings', () => { // Mock MC settings productListingsPage.fulfillSettings( { - shipping_rate: 'automatic', + shipping_rate: 'flat', website_live: false, checkout_process_secure: false, payment_methods_visible: false, refund_tos_visible: false, contact_info_visible: false, + shipping_rates_count: 0, }, 200, [ 'GET' ] @@ -185,12 +186,24 @@ test.describe( 'Configure product listings', () => { await expect( taxRateSection ).not.toBeVisible(); } ); + test.describe( 'Automatic rate', () => { + test.beforeAll( async () => { + await page.reload(); + } ); + + test( 'shouldnt display automatic rate if no shipping methods are set up, shipping_rates_count = 0', async () => { + await expect( + productListingsPage.getRecommendedShippingRateRadioRow() + ).toHaveCount( 0 ); + } ); + } ); + test.describe( 'Shipping rate is simple', () => { test.beforeAll( async () => { await page.reload(); // Check another shipping rate first in case the simple shipping rate radio button is already checked. - await productListingsPage.checkRecommendedShippingRateRadioButton(); + await productListingsPage.checkComplexShippingRateRadioButton(); } ); test( 'should send settings POST request after checking simple shipping rate radio button', async () => { @@ -267,9 +280,6 @@ test.describe( 'Configure product listings', () => { productListingsPage.getEstimatedShippingRatesCard(); estimatedTimesCard = productListingsPage.getEstimatedShippingTimesCard(); - - // Check another shipping rate first in case the complex shipping rate radio button is already checked. - await productListingsPage.checkRecommendedShippingRateRadioButton(); } ); test( 'should send settings POST request after checking complex shipping rate radio button', async () => { @@ -306,6 +316,21 @@ test.describe( 'Configure product listings', () => { test.describe( 'Shipping rate is recommended', () => { test.beforeAll( async () => { + productListingsPage.fulfillSettings( + { + shipping_rate: 'flat', + website_live: false, + checkout_process_secure: false, + payment_methods_visible: false, + refund_tos_visible: false, + contact_info_visible: false, + shipping_rates_count: 1, // Set shipping rates count to 1 to show the recommended shipping rate radio button. + }, + 200, + [ 'GET' ] + ); + + await page.reload(); // Check another shipping rate first in case the recommended shipping rate radio button is already checked. await productListingsPage.checkSimpleShippingRateRadioButton(); } ); diff --git a/tests/e2e/utils/pages/edit-free-listings.js b/tests/e2e/utils/pages/edit-free-listings.js index aea8797814..5f2c742b8f 100644 --- a/tests/e2e/utils/pages/edit-free-listings.js +++ b/tests/e2e/utils/pages/edit-free-listings.js @@ -42,7 +42,7 @@ export default class EditFreeListingsPage extends MockRequests { async checkRecommendShippingSettings() { return this.page .locator( - 'text=Recommended: Automatically sync my store’s shipping settings to Google.' + 'text=Automatically sync my store’s shipping settings to Google.' ) .check(); } diff --git a/tests/e2e/utils/pages/setup-mc/step-2-product-listings.js b/tests/e2e/utils/pages/setup-mc/step-2-product-listings.js index ecd2d84116..36affb1b22 100644 --- a/tests/e2e/utils/pages/setup-mc/step-2-product-listings.js +++ b/tests/e2e/utils/pages/setup-mc/step-2-product-listings.js @@ -80,7 +80,7 @@ export default class ProductListingsPage extends MockRequests { */ getRecommendedShippingRateRadioRow() { return this.page.getByRole( 'radio', { - name: 'Recommended: Automatically sync my store’s shipping settings to Google.', + name: 'Automatically sync my store’s shipping settings to Google.', exact: true, } ); } From c238ba10ae1c1088e7224d36bb0f60b76d824969 Mon Sep 17 00:00:00 2001 From: Jorge M Date: Tue, 20 Aug 2024 11:19:03 +0200 Subject: [PATCH 8/9] Simplify logic condition --- .../components/shipping-rate-section/shipping-rate-section.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/js/src/components/shipping-rate-section/shipping-rate-section.js b/js/src/components/shipping-rate-section/shipping-rate-section.js index 6650ba95dd..2d95798cd8 100644 --- a/js/src/components/shipping-rate-section/shipping-rate-section.js +++ b/js/src/components/shipping-rate-section/shipping-rate-section.js @@ -32,9 +32,7 @@ const ShippingRateSection = () => { const hideAutomatticShippingRate = ! settings?.shipping_rates_count && hasFinishedResolution && - mcSetup?.status === 'incomplete' - ? true - : false; + mcSetup?.status === 'incomplete'; return (
Date: Tue, 20 Aug 2024 11:19:37 +0200 Subject: [PATCH 9/9] Remove unsused createInterpolateElement --- .../shipping-rate-section/shipping-rate-section.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/js/src/components/shipping-rate-section/shipping-rate-section.js b/js/src/components/shipping-rate-section/shipping-rate-section.js index 2d95798cd8..9c58755cfe 100644 --- a/js/src/components/shipping-rate-section/shipping-rate-section.js +++ b/js/src/components/shipping-rate-section/shipping-rate-section.js @@ -64,14 +64,9 @@ const ShippingRateSection = () => { { ! hideAutomatticShippingRate && ( , - } + label={ __( + 'Automatically sync my store’s shipping settings to Google.', + 'google-listings-and-ads' ) } value="automatic" collapsible