From 6b16e2afb1edcd4b9c019c6276af6071266f7c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20P=C3=A9rez=20Pellicer?= <5908855+puntope@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:35:25 +0400 Subject: [PATCH 1/3] Add e2e for Scheduling notifications --- .../notifications-banner.test.js} | 10 +- .../notifications-schedule.test.js | 115 ++++++++++++++++++ tests/e2e/test-data/test-data.php | 42 +++++++ tests/e2e/utils/api.js | 14 +++ tests/e2e/utils/product-editor.js | 44 +++++++ 5 files changed, 220 insertions(+), 5 deletions(-) rename tests/e2e/specs/{notifications.test.js => notifications/notifications-banner.test.js} (94%) create mode 100644 tests/e2e/specs/notifications/notifications-schedule.test.js diff --git a/tests/e2e/specs/notifications.test.js b/tests/e2e/specs/notifications/notifications-banner.test.js similarity index 94% rename from tests/e2e/specs/notifications.test.js rename to tests/e2e/specs/notifications/notifications-banner.test.js index 85bcc6d344..cd58c563a7 100644 --- a/tests/e2e/specs/notifications.test.js +++ b/tests/e2e/specs/notifications/notifications-banner.test.js @@ -6,16 +6,16 @@ import { expect, test } from '@playwright/test'; /** * Internal dependencies */ -import SettingsPage from '../utils/pages/settings'; -import { clearOnboardedMerchant, setOnboardedMerchant } from '../utils/api'; -import { LOAD_STATE } from '../utils/constants'; +import SettingsPage from '../../utils/pages/settings'; +import { clearOnboardedMerchant, setOnboardedMerchant } from '../../utils/api'; +import { LOAD_STATE } from '../../utils/constants'; test.use( { storageState: process.env.ADMINSTATE } ); test.describe.configure( { mode: 'serial' } ); /** - * @type {import('../utils/pages/settings.js').default } settingsPage + * @type {import('../../utils/pages/settings.js').default } settingsPage */ let settingsPage = null; @@ -24,7 +24,7 @@ let settingsPage = null; */ let page = null; -test.describe( 'Notifications Feature', () => { +test.describe( 'Notifications Banner', () => { test.beforeAll( async ( { browser } ) => { page = await browser.newPage(); settingsPage = new SettingsPage( page ); diff --git a/tests/e2e/specs/notifications/notifications-schedule.test.js b/tests/e2e/specs/notifications/notifications-schedule.test.js new file mode 100644 index 0000000000..1d6a406c4d --- /dev/null +++ b/tests/e2e/specs/notifications/notifications-schedule.test.js @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { expect, test } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { getClassicProductEditorUtils } from '../../utils/product-editor'; +import MockRequests from '../../utils/mock-requests'; +import { + setNotificationsReady, + clearOnboardedMerchant, + setOnboardedMerchant, +} from '../../utils/api'; + +test.use( { storageState: process.env.ADMINSTATE } ); + +test.describe.configure( { mode: 'serial' } ); + +/** + * @type {import('../../utils/mock-requests.js').default } mockRequests + */ +let mockRequests = null; + +/** + * @type {import('../../utils/product-editor.js').default } productEditor + */ +let productEditor = null; + +/** + * @type {import('@playwright/test').Page} page + */ +let page = null; + +const actionSchedulerLink = + 'wp-admin/admin.php?page=wc-status&tab=action-scheduler&orderby=schedule&order=desc'; + +test.describe( 'Notifications Schedule', () => { + test.beforeAll( async ( { browser } ) => { + page = await browser.newPage(); + productEditor = getClassicProductEditorUtils( page ); + mockRequests = new MockRequests( page ); + await mockRequests.mockMCConnected( 1234, true, 'approved' ); + await setOnboardedMerchant(); + await Promise.all( [ + // Mock Jetpack as connected + mockRequests.mockJetpackConnected(), + + // Mock google as connected. + mockRequests.mockGoogleConnected(), + ] ); + } ); + + test.afterAll( async () => { + await clearOnboardedMerchant(); + await page.close(); + } ); + + test( 'When access is granted and Product is created - Notifications are scheduled', async () => { + await setNotificationsReady(); + // Create a new fresh product + await productEditor.gotoAddProductPage(); + await productEditor.fillProductName(); + await productEditor.publish(); + const id = productEditor.getPostID(); + + // Check the product.create job is scheduled. + await page.goto( actionSchedulerLink ); + let row = page.getByRole( 'row', { + name: `gla/jobs/notifications/products/process_item Run | Cancel Pending 0 => array ( 'item_id' => ${ id }, 'topic' => 'product.create'`, + } ); + await expect( row ).toBeVisible(); + + // Hover the row, so the Run button gets visible + await row.hover( { force: true } ); + await row.getByRole( 'link' ).first().click(); + + // Wait for the page to refresh and see that pending job is not there anymore. + await page.waitForURL( actionSchedulerLink ); + await expect( row ).not.toBeVisible(); + + // edit the product and set it as notified + await productEditor.gotoEditProductPage( id ); + await productEditor.mockNotificationStatus( 'created' ); + await productEditor.fillProductName( 'updated product' ); + await productEditor.save(); + + // Check if the product.update job is there. + await page.goto( actionSchedulerLink ); + row = page.getByRole( 'row', { + name: `gla/jobs/notifications/products/process_item Run | Cancel Pending 0 => array ( 'item_id' => ${ id }, 'topic' => 'product.update'`, + } ); + await expect( row ).toBeVisible(); + await row.hover( { force: true } ); + await row.getByRole( 'link' ).first().click(); + await page.waitForURL( actionSchedulerLink ); + await expect( row ).not.toBeVisible(); + + // change to external type. It will trigger the product.delete + await productEditor.gotoEditProductPage( id ); + await productEditor.changeToExternalProduct(); + await productEditor.save(); + // Check if the product.delete job is there. + await page.goto( actionSchedulerLink ); + row = page.getByRole( 'row', { + name: `gla/jobs/notifications/products/process_item Run | Cancel Pending 0 => array ( 'item_id' => ${ id }, 'topic' => 'product.delete'`, + } ); + await expect( row ).toBeVisible(); + await row.hover( { force: true } ); + await row.getByRole( 'link' ).first().click(); + await page.waitForURL( actionSchedulerLink ); + await expect( row ).not.toBeVisible(); + } ); +} ); diff --git a/tests/e2e/test-data/test-data.php b/tests/e2e/test-data/test-data.php index 55de84dcaa..c0fd6fdd98 100644 --- a/tests/e2e/test-data/test-data.php +++ b/tests/e2e/test-data/test-data.php @@ -10,8 +10,10 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\TestData; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface; +use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; add_action( 'rest_api_init', __NAMESPACE__ . '\register_routes' ); +apply_filters( 'woocommerce_gla_notify', false); // avoid any request to google in the tests /** * Register routes for setting test data. @@ -49,6 +51,22 @@ function register_routes() { ], ], ); + register_rest_route( + 'wc/v3', + 'gla-test/notifications-ready', + [ + [ + 'methods' => 'POST', + 'callback' => __NAMESPACE__ . '\set_notifications_ready', + 'permission_callback' => __NAMESPACE__ . '\permissions', + ], + [ + 'methods' => 'DELETE', + 'callback' => __NAMESPACE__ . '\clear_notifications_ready', + 'permission_callback' => __NAMESPACE__ . '\permissions', + ], + ], + ); } /** @@ -113,3 +131,27 @@ function clear_conversion_id() { function permissions() { return current_user_can( 'manage_woocommerce' ); } + +/** + * Set the Notifications Service as ready. + */ +function set_notifications_ready() { + /** @var OptionsInterface $options */ + $options = woogle_get_container()->get( OptionsInterface::class ); + $transients = woogle_get_container()->get( TransientsInterface::class ); + $transients->set( TransientsInterface::URL_MATCHES, 'yes' ); + $options->update( + OptionsInterface::WPCOM_REST_API_STATUS, 'approved' + ); +} +/** + * Clear the Notifications Service. + */ +function clear_notifications_ready() { + /** @var OptionsInterface $options */ + $options = woogle_get_container()->get( OptionsInterface::class ); + $transients = woogle_get_container()->get( TransientsInterface::class ); + $transients->delete( TransientsInterface::URL_MATCHES ); + $options->delete( OptionsInterface::WPCOM_REST_API_STATUS ); +} + diff --git a/tests/e2e/utils/api.js b/tests/e2e/utils/api.js index 5e20da3706..1dbe0d6fe7 100644 --- a/tests/e2e/utils/api.js +++ b/tests/e2e/utils/api.js @@ -112,3 +112,17 @@ export async function setOnboardedMerchant() { export async function clearOnboardedMerchant() { await api().delete( 'gla-test/onboarded-merchant' ); } + +/** + * Set Notifications Ready. + */ +export async function setNotificationsReady() { + await api().post( 'gla-test/notifications-ready' ); +} + +/** + * Clear Onboarded Merchant. + */ +export async function clearNotificationsReady() { + await api().delete( 'gla-test/notifications-ready' ); +} diff --git a/tests/e2e/utils/product-editor.js b/tests/e2e/utils/product-editor.js index f81ee503fd..23f00c70d6 100644 --- a/tests/e2e/utils/product-editor.js +++ b/tests/e2e/utils/product-editor.js @@ -151,6 +151,11 @@ export function getClassicProductEditorUtils( page ) { return page.locator( '.gla_attributes_multipack_field input' ); }, + getPostID() { + const url = new URL( page.url() ); + return url.searchParams.get( 'post' ); + }, + async getAvailableProductAttributesWithTestValues( locator = page ) { return getAvailableProductAttributesWithTestValues( locator, @@ -165,6 +170,11 @@ export function getClassicProductEditorUtils( page ) { await this.waitForInteractionReady(); }, + async gotoEditProductPage( id ) { + await page.goto( `/wp-admin/post.php?post=${ id }&action=edit` ); + await this.waitForInteractionReady(); + }, + async gotoEditVariableProductPage() { const variableId = await api.createVariableWithVariationProducts(); @@ -210,6 +220,30 @@ export function getClassicProductEditorUtils( page ) { await this.waitForInteractionReady(); }, + clickPublish() { + return page + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + }, + + async publish() { + const observer = page.waitForResponse( ( response ) => { + const url = new URL( response.url() ); + + return ( + url.pathname === '/wp-admin/post.php' && + url.searchParams.has( 'post' ) && + url.searchParams.has( 'action', 'edit' ) && + response.ok() && + response.request().method() === 'GET' + ); + } ); + + await this.clickPublish(); + await observer; + await this.waitForInteractionReady(); + }, + waitForInteractionReady() { // Avoiding tests may start to operate the UI before jQuery interactions are initialized, // leading to random failures. @@ -273,6 +307,16 @@ export function getClassicProductEditorUtils( page ) { ], } ); }, + async mockNotificationStatus( status ) { + const url = new URL( page.url() ); + const productId = url.searchParams.get( 'post' ); + + await api.api().put( `products/${ productId }`, { + meta_data: [ + { key: '_wc_gla_notification_status', value: status }, + ], + } ); + }, }; return { From 73c6e4202db95f1eacf1b1f238773e9b3ee253be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20P=C3=A9rez=20Pellicer?= <5908855+puntope@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:39:05 +0400 Subject: [PATCH 2/3] Change test name --- tests/e2e/specs/notifications/notifications-schedule.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/specs/notifications/notifications-schedule.test.js b/tests/e2e/specs/notifications/notifications-schedule.test.js index 1d6a406c4d..96886d3101 100644 --- a/tests/e2e/specs/notifications/notifications-schedule.test.js +++ b/tests/e2e/specs/notifications/notifications-schedule.test.js @@ -57,7 +57,7 @@ test.describe( 'Notifications Schedule', () => { await page.close(); } ); - test( 'When access is granted and Product is created - Notifications are scheduled', async () => { + test( 'When access is granted Notifications are scheduled', async () => { await setNotificationsReady(); // Create a new fresh product await productEditor.gotoAddProductPage(); From bd200ed195caf7ddb62a92ff005fb06f8a170cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20P=C3=A9rez=20Pellicer?= <5908855+puntope@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:30:44 +0400 Subject: [PATCH 3/3] TWeak e2e test logic for notifications --- .../notifications-banner.test.js | 24 +++++++++---------- .../notifications-schedule.test.js | 6 +++-- tests/e2e/test-data/test-data.php | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/e2e/specs/notifications/notifications-banner.test.js b/tests/e2e/specs/notifications/notifications-banner.test.js index cd58c563a7..e63a183d68 100644 --- a/tests/e2e/specs/notifications/notifications-banner.test.js +++ b/tests/e2e/specs/notifications/notifications-banner.test.js @@ -52,7 +52,7 @@ test.describe( 'Notifications Banner', () => { test( 'Grant Access button is visible on Settings page when notifications service is enabled', async () => { // Mock Merchant Center as connected - settingsPage.mockMCConnected( 1234, true ); + await settingsPage.mockMCConnected( 1234, true ); const button = settingsPage.getGrantAccessBtn(); await expect( button ).toBeVisible(); @@ -61,8 +61,8 @@ test.describe( 'Notifications Banner', () => { test( 'When click on Grant Access button redirect to Auth page', async () => { const mockAuthURL = 'https://example.com'; // Mock Merchant Center as connected - settingsPage.mockMCConnected( 1234, true ); - settingsPage.fulfillRESTApiAuthorize( { auth_url: mockAuthURL } ); + await settingsPage.mockMCConnected( 1234, true ); + await settingsPage.fulfillRESTApiAuthorize( { auth_url: mockAuthURL } ); const button = settingsPage.getGrantAccessBtn(); button.click(); @@ -72,8 +72,8 @@ test.describe( 'Notifications Banner', () => { } ); test( 'When REST API is Approved it shows a success notice in MC and allows to disable it', async () => { - settingsPage.goto(); - settingsPage.mockMCConnected( 1234, true, 'approved' ); + await settingsPage.goto(); + await settingsPage.mockMCConnected( 1234, true, 'approved' ); const grantedAccessMessage = page .locator( '#woocommerce-layout__primary' ) .getByText( @@ -99,24 +99,24 @@ test.describe( 'Notifications Banner', () => { } ); await expect( disableDataFetchButton ).toBeVisible(); - disableDataFetchButton.click(); + await disableDataFetchButton.click(); await expect( modalConfirmBtn ).toBeDisabled(); await expect( modalDismissBtn ).toBeEnabled(); await expect( modalCheck ).toBeVisible(); - modalCheck.check(); + await modalCheck.check(); await expect( modalConfirmBtn ).toBeEnabled(); - modalConfirmBtn.click(); + await modalConfirmBtn.click(); await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); await page.waitForURL( /path=%2Fgoogle%2Fsettings/ ); await expect( modalConfirmBtn ).not.toBeVisible(); } ); test( 'When REST API is Error it shows a waring notice in MC and allows to grant access', async () => { - settingsPage.goto(); - settingsPage.mockMCConnected( 1234, true, 'error' ); + await settingsPage.goto(); + await settingsPage.mockMCConnected( 1234, true, 'error' ); const mockAuthURL = 'https://example.com'; - settingsPage.fulfillRESTApiAuthorize( { auth_url: mockAuthURL } ); + await settingsPage.fulfillRESTApiAuthorize( { auth_url: mockAuthURL } ); const errorAccessMessage = page .locator( '#woocommerce-layout__primary' ) .getByText( @@ -128,7 +128,7 @@ test.describe( 'Notifications Banner', () => { } ); await expect( errorAccessMessage ).toBeVisible(); await expect( grantAccessBtn ).toBeVisible(); - grantAccessBtn.click(); + await grantAccessBtn.click(); await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); await page.waitForURL( mockAuthURL ); expect( page.url() ).toMatch( mockAuthURL ); diff --git a/tests/e2e/specs/notifications/notifications-schedule.test.js b/tests/e2e/specs/notifications/notifications-schedule.test.js index 96886d3101..d9a31419e2 100644 --- a/tests/e2e/specs/notifications/notifications-schedule.test.js +++ b/tests/e2e/specs/notifications/notifications-schedule.test.js @@ -12,6 +12,7 @@ import { setNotificationsReady, clearOnboardedMerchant, setOnboardedMerchant, + clearNotificationsReady, } from '../../utils/api'; test.use( { storageState: process.env.ADMINSTATE } ); @@ -41,24 +42,25 @@ test.describe( 'Notifications Schedule', () => { page = await browser.newPage(); productEditor = getClassicProductEditorUtils( page ); mockRequests = new MockRequests( page ); - await mockRequests.mockMCConnected( 1234, true, 'approved' ); await setOnboardedMerchant(); + await setNotificationsReady(); await Promise.all( [ // Mock Jetpack as connected mockRequests.mockJetpackConnected(), // Mock google as connected. mockRequests.mockGoogleConnected(), + mockRequests.mockMCConnected( 1234, true, 'approved' ), ] ); } ); test.afterAll( async () => { await clearOnboardedMerchant(); + await clearNotificationsReady(); await page.close(); } ); test( 'When access is granted Notifications are scheduled', async () => { - await setNotificationsReady(); // Create a new fresh product await productEditor.gotoAddProductPage(); await productEditor.fillProductName(); diff --git a/tests/e2e/test-data/test-data.php b/tests/e2e/test-data/test-data.php index c0fd6fdd98..e28b008ffa 100644 --- a/tests/e2e/test-data/test-data.php +++ b/tests/e2e/test-data/test-data.php @@ -13,7 +13,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; add_action( 'rest_api_init', __NAMESPACE__ . '\register_routes' ); -apply_filters( 'woocommerce_gla_notify', false); // avoid any request to google in the tests +add_filter( 'woocommerce_gla_notify', '__return_false'); // avoid any request to google in the tests /** * Register routes for setting test data.