From 07bf254333d0d53065dfbce3cedb645199024029 Mon Sep 17 00:00:00 2001 From: Valerii Naida Date: Thu, 19 Jun 2025 11:52:24 -0500 Subject: [PATCH 1/4] feat: added support for smart grouping of CWV recommendations --- src/support/slack/commands.js | 2 + .../commands/assign-cwv-template-groups.js | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/support/slack/commands/assign-cwv-template-groups.js diff --git a/src/support/slack/commands.js b/src/support/slack/commands.js index 4015e2887..9d86a29c7 100644 --- a/src/support/slack/commands.js +++ b/src/support/slack/commands.js @@ -27,6 +27,7 @@ import toggleSiteAudit from './commands/toggle-site-audit.js'; import onboard from './commands/onboard.js'; import setSiteOrganizationCommand from './commands/set-ims-org.js'; import toggleSiteImport from './commands/toggle-site-import.js'; +import AssignCwvTemplateGroups from './commands/assign-cwv-template-groups.js'; /** * Returns all commands. @@ -52,4 +53,5 @@ export default (context) => [ onboard(context), setSiteOrganizationCommand(context), toggleSiteImport(context), + AssignCwvTemplateGroups(context), ]; diff --git a/src/support/slack/commands/assign-cwv-template-groups.js b/src/support/slack/commands/assign-cwv-template-groups.js new file mode 100644 index 000000000..93f73dcde --- /dev/null +++ b/src/support/slack/commands/assign-cwv-template-groups.js @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { isString } from '@adobe/spacecat-shared-utils'; +import BaseCommand from './base.js'; +import { extractURLFromSlackInput } from '../../../utils/slack/base.js'; + +const PHRASE = 'assign cwv template groups'; +const SUCCESS_MESSAGE_PREFIX = ':white_check_mark: '; +const ERROR_MESSAGE_PREFIX = ':x: '; + +export default (context) => { + const baseCommand = BaseCommand({ + id: 'configurations-sites--assign-cwv-template-groups', + name: 'Assign Template-Based Page Groups', + description: 'Automatically groups pages by URL pattern based on the latest CWV audit. Falls back to manual grouping if needed.', + phrases: [PHRASE], + usageText: `${PHRASE} {site}`, + }); + + const { log, dataAccess } = context; + const { Site } = dataAccess; + + const handleExecution = async (args, slackContext) => { + const { say } = slackContext; + const [baseURLInput] = args; + + try { + const baseURL = extractURLFromSlackInput(baseURLInput); + + if (isString(baseURL) === false || baseURL.length === 0) { + await say(`${ERROR_MESSAGE_PREFIX}The site URL is missing or in the wrong format.`); + return; + } + + const site = await Site.findByBaseURL(baseURL); + if (!site) { + await say(`${ERROR_MESSAGE_PREFIX}Site with baseURL "${baseURL}" not found.`); + return; + } + + const siteConfig = site.getConfig(); + await say(`${SUCCESS_MESSAGE_PREFIX}${JSON.stringify(siteConfig, null, 2)}`); + } catch (error) { + log.error(error); + await say(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to automatically group pages by URL pattern: ${error.message}`); + } + }; + + baseCommand.init(context); + + return { + ...baseCommand, + handleExecution, + }; +}; From 45f95ecca280d89881a650d29b1215f5810e2a2e Mon Sep 17 00:00:00 2001 From: Valerii Naida Date: Thu, 19 Jun 2025 13:06:28 -0500 Subject: [PATCH 2/4] feat: added support for smart grouping of CWV recommendations -- test coverage --- .../commands/assign-cwv-template-groups.js | 2 +- .../assign-cwv-template-groups.test.js | 173 ++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 test/support/slack/commands/assign-cwv-template-groups.test.js diff --git a/src/support/slack/commands/assign-cwv-template-groups.js b/src/support/slack/commands/assign-cwv-template-groups.js index 93f73dcde..a4668a4d2 100644 --- a/src/support/slack/commands/assign-cwv-template-groups.js +++ b/src/support/slack/commands/assign-cwv-template-groups.js @@ -51,7 +51,7 @@ export default (context) => { await say(`${SUCCESS_MESSAGE_PREFIX}${JSON.stringify(siteConfig, null, 2)}`); } catch (error) { log.error(error); - await say(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to automatically group pages by URL pattern: ${error.message}`); + await say(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to automatically group pages by URL pattern: ${error.message}.`); } }; diff --git a/test/support/slack/commands/assign-cwv-template-groups.test.js b/test/support/slack/commands/assign-cwv-template-groups.test.js new file mode 100644 index 000000000..174414299 --- /dev/null +++ b/test/support/slack/commands/assign-cwv-template-groups.test.js @@ -0,0 +1,173 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import sinon from 'sinon'; +import { expect } from 'chai'; +import AssignCwvTemplateGroupsCommand from '../../../../src/support/slack/commands/assign-cwv-template-groups.js'; + +const SUCCESS_MESSAGE_PREFIX = ':white_check_mark: '; +const ERROR_MESSAGE_PREFIX = ':x: '; + +describe('AssignCwvTemplateGroups', () => { + const sandbox = sinon.createSandbox(); + + const site = { + // getId: () => 'site0', + // getBaseURL: () => 'https://site0.com', + // getDeliveryType: () => 'aem_edge', + getConfig: () => ({ test: 'value' }), + }; + + let configurationMock; + let dataAccessMock; + let logMock; + let contextMock; + let slackContextMock; + const exceptsAtBadRequest = () => { + expect( + configurationMock.save.called, + 'Expected updateConfiguration to not be called, but it was', + ).to.be.false; + }; + + beforeEach(async () => { + configurationMock = { + getVersion: sandbox.stub(), + getJobs: sandbox.stub(), + getHandlers: sandbox.stub().returns(), + getQueues: sandbox.stub(), + save: sandbox.stub(), + }; + + dataAccessMock = { + Site: { + findByBaseURL: sandbox.stub().resolves(), + }, + }; + + logMock = { + error: sandbox.stub(), + }; + + contextMock = { + log: logMock, + dataAccess: dataAccessMock, + env: { + SLACK_BOT_TOKEN: 'mock-token', + }, + }; + + slackContextMock = { + say: sinon.stub(), + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('Assign Template-Based Page Groups', async () => { + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = AssignCwvTemplateGroupsCommand(contextMock); + const args = ['https://site0.com']; + await command.handleExecution(args, slackContextMock); + + expect( + dataAccessMock.Site.findByBaseURL.calledWith('https://site0.com'), + 'Expected dataAccess.getSiteByBaseURL to be called with "https://site0.com", but it was not', + ).to.be.true; + expect( + slackContextMock.say.calledWith(`${SUCCESS_MESSAGE_PREFIX}${JSON.stringify({ test: 'value' }, null, 2)}`), + 'Expected Slack message to be sent confirming "some_audit" was enabled for "https://site0.com", but it was not', + ).to.be.true; + }); + + it('if site base URL without scheme should be added "https://"', async () => { + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + + const command = AssignCwvTemplateGroupsCommand(contextMock); + const args = ['site0.com']; + await command.handleExecution(args, slackContextMock); + + expect( + dataAccessMock.Site.findByBaseURL.calledWith('https://site0.com'), + 'Expected dataAccess.getSiteByBaseURL to be called with "https://site0.com", but it was not', + ).to.be.true; + }); + + describe('Internal errors', () => { + it('error during execution', async () => { + const error = new Error('Test error'); + dataAccessMock.Site.findByBaseURL.rejects(error); + + const command = AssignCwvTemplateGroupsCommand(contextMock); + const args = ['http://site0.com']; + await command.handleExecution(args, slackContextMock); + + expect( + contextMock.log.error.calledWith(error), + 'Expected log.error to be called with the provided error, but it was not', + ).to.be.true; + expect( + slackContextMock.say.calledWith(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to automatically group pages by URL pattern: Test error.`), + `Expected say method to be called with error message "${ERROR_MESSAGE_PREFIX}An error occurred while trying to automatically group pages by URL pattern: Test error."`, + ).to.be.true; + }); + }); + + describe('Bad Request Errors', () => { + it('if "baseURL" is not provided', async () => { + const command = AssignCwvTemplateGroupsCommand(contextMock); + const args = ['']; + + await command.handleExecution(args, slackContextMock); + + exceptsAtBadRequest(); + expect( + slackContextMock.say.calledWith(`${ERROR_MESSAGE_PREFIX}The site URL is missing or in the wrong format.`), + `Expected say method to be called with error message "${ERROR_MESSAGE_PREFIX}The site URL is missing or in the wrong format.", but it was not called with that message.`, + ).to.be.true; + }); + + it('if "baseURL" has wrong site format', async () => { + const command = AssignCwvTemplateGroupsCommand(contextMock); + const args = ['wrong_site_format']; + + await command.handleExecution(args, slackContextMock); + + exceptsAtBadRequest(); + expect( + slackContextMock.say.calledWith(`${ERROR_MESSAGE_PREFIX}The site URL is missing or in the wrong format.`), + `Expected say method to be called with error message "${ERROR_MESSAGE_PREFIX}The site URL is missing or in the wrong format.", but it was not called with that message.`, + ).to.be.true; + }); + + it('if a site is not found', async () => { + const baseURL = 'https://site0.com'; + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(null); + + const command = AssignCwvTemplateGroupsCommand(contextMock); + const args = [baseURL]; + + await command.handleExecution(args, slackContextMock); + + exceptsAtBadRequest(); + expect( + slackContextMock.say.calledWith(`${ERROR_MESSAGE_PREFIX}Site with baseURL "${baseURL}" not found.`), + 'Expected slackContextMock.say to be called with the specified error message, but it was not.', + ).to.be.true; + }); + }); +}); From bf8f5a9592d28caf30b5caca30ead2bd25ac7f4e Mon Sep 17 00:00:00 2001 From: Valerii Naida Date: Thu, 19 Jun 2025 16:27:43 -0500 Subject: [PATCH 3/4] feat: added support for smart grouping of CWV recommendations -- test coverage --- .../commands/assign-cwv-template-groups.js | 22 ++++++++ .../assign-cwv-template-groups.test.js | 52 ++++++++++++++----- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/support/slack/commands/assign-cwv-template-groups.js b/src/support/slack/commands/assign-cwv-template-groups.js index a4668a4d2..d58abc9fc 100644 --- a/src/support/slack/commands/assign-cwv-template-groups.js +++ b/src/support/slack/commands/assign-cwv-template-groups.js @@ -9,7 +9,9 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +import { Audit } from '@adobe/spacecat-shared-data-access'; import { isString } from '@adobe/spacecat-shared-utils'; +import { Config } from '@adobe/spacecat-shared-data-access/src/models/site/config.js'; import BaseCommand from './base.js'; import { extractURLFromSlackInput } from '../../../utils/slack/base.js'; @@ -49,6 +51,26 @@ export default (context) => { const siteConfig = site.getConfig(); await say(`${SUCCESS_MESSAGE_PREFIX}${JSON.stringify(siteConfig, null, 2)}`); + + const groupedURLs = [{ pattern: 'test' }]; + const currentGroupedURLs = siteConfig.getGroupedURLs(Audit.AUDIT_TYPES.CWV) || []; + let patchedGroupedURLs = []; + if (groupedURLs.length !== 0) { + patchedGroupedURLs = Object.values( + [...currentGroupedURLs, ...groupedURLs].reduce((acc, item) => { + acc[item.pattern] = item; + return acc; + }, {}), + ); + } + + // if objects are not equal + siteConfig.updateGroupedURLs(Audit.AUDIT_TYPES.CWV, patchedGroupedURLs); + site.setConfig(Config.toDynamoItem(siteConfig)); + // await site.save(); + + const groupCount = 0; + await say(`${SUCCESS_MESSAGE_PREFIX}Found ${groupCount} new group(s) for site "${baseURL}" and added them to the configuration. Please re-run the CWV audit to see the results.`); } catch (error) { log.error(error); await say(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to automatically group pages by URL pattern: ${error.message}.`); diff --git a/test/support/slack/commands/assign-cwv-template-groups.test.js b/test/support/slack/commands/assign-cwv-template-groups.test.js index 174414299..12f678fc0 100644 --- a/test/support/slack/commands/assign-cwv-template-groups.test.js +++ b/test/support/slack/commands/assign-cwv-template-groups.test.js @@ -14,6 +14,7 @@ import sinon from 'sinon'; import { expect } from 'chai'; +import { Audit } from '@adobe/spacecat-shared-data-access'; import AssignCwvTemplateGroupsCommand from '../../../../src/support/slack/commands/assign-cwv-template-groups.js'; const SUCCESS_MESSAGE_PREFIX = ':white_check_mark: '; @@ -22,11 +23,20 @@ const ERROR_MESSAGE_PREFIX = ':x: '; describe('AssignCwvTemplateGroups', () => { const sandbox = sinon.createSandbox(); - const site = { - // getId: () => 'site0', - // getBaseURL: () => 'https://site0.com', - // getDeliveryType: () => 'aem_edge', - getConfig: () => ({ test: 'value' }), + const siteConfigMock = { + getGroupedURLs: sandbox.stub(), + updateGroupedURLs: sandbox.stub(), + getSlackConfig: () => {}, + getHandlers: () => (({ [Audit.AUDIT_TYPES.CWV]: {} })), + getContentAiConfig: () => {}, + getImports: () => [], + getFetchConfig: () => {}, + getBrandConfig: () => {}, + }; + + const siteMock = { + getConfig: sandbox.stub().returns(siteConfigMock), + setConfig: sandbox.stub(), }; let configurationMock; @@ -78,24 +88,42 @@ describe('AssignCwvTemplateGroups', () => { }); it('Assign Template-Based Page Groups', async () => { - dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + const baseUrl = 'https://site0.com'; + const groupedUrls = [{ pattern: 'test' }]; + const groupCount = 0; + + dataAccessMock.Site.findByBaseURL.withArgs(baseUrl).resolves(siteMock); + siteConfigMock.getGroupedURLs.withArgs(Audit.AUDIT_TYPES.CWV).returns(groupedUrls); const command = AssignCwvTemplateGroupsCommand(contextMock); - const args = ['https://site0.com']; + const args = [baseUrl]; await command.handleExecution(args, slackContextMock); expect( - dataAccessMock.Site.findByBaseURL.calledWith('https://site0.com'), - 'Expected dataAccess.getSiteByBaseURL to be called with "https://site0.com", but it was not', + dataAccessMock.Site.findByBaseURL.calledWith(baseUrl), + `Expected dataAccess.getSiteByBaseURL to be called with "${baseUrl}", but it was not`, + ).to.be.true; + expect( + siteConfigMock.getGroupedURLs.calledWith(Audit.AUDIT_TYPES.CWV), + `Expected siteConfig.getGroupedURLs to be called with "${Audit.AUDIT_TYPES.CWV}", but it was not`, + ).to.be.true; + expect( + siteConfigMock.updateGroupedURLs.calledWith(Audit.AUDIT_TYPES.CWV, groupedUrls), + 'Expected siteConfig.updateGroupedURLs to be called , but it was not', + ).to.be.true; + expect( + siteMock.setConfig.calledOnce, + 'Expected site.setConfig to be called once, but it was not', ).to.be.true; + const expectedMessage = `${SUCCESS_MESSAGE_PREFIX}Found ${groupCount} new group(s) for site "${baseUrl}" and added them to the configuration. Please re-run the CWV audit to see the results.`; expect( - slackContextMock.say.calledWith(`${SUCCESS_MESSAGE_PREFIX}${JSON.stringify({ test: 'value' }, null, 2)}`), - 'Expected Slack message to be sent confirming "some_audit" was enabled for "https://site0.com", but it was not', + slackContextMock.say.calledWith(expectedMessage), + `Expected say method to be called with message: "${expectedMessage}"`, ).to.be.true; }); it('if site base URL without scheme should be added "https://"', async () => { - dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(site); + dataAccessMock.Site.findByBaseURL.withArgs('https://site0.com').resolves(siteMock); const command = AssignCwvTemplateGroupsCommand(contextMock); const args = ['site0.com']; From c569b368aeb53e5ee8e0a65e6db1e359a758cc38 Mon Sep 17 00:00:00 2001 From: Valerii Naida Date: Thu, 19 Jun 2025 16:44:58 -0500 Subject: [PATCH 4/4] feat: added support for smart grouping of CWV recommendations -- test coverage --- .../slack/commands/assign-cwv-template-groups.js | 9 +++++---- .../commands/assign-cwv-template-groups.test.js | 12 +++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/support/slack/commands/assign-cwv-template-groups.js b/src/support/slack/commands/assign-cwv-template-groups.js index d58abc9fc..666f80d60 100644 --- a/src/support/slack/commands/assign-cwv-template-groups.js +++ b/src/support/slack/commands/assign-cwv-template-groups.js @@ -52,12 +52,12 @@ export default (context) => { const siteConfig = site.getConfig(); await say(`${SUCCESS_MESSAGE_PREFIX}${JSON.stringify(siteConfig, null, 2)}`); - const groupedURLs = [{ pattern: 'test' }]; + const suggestedGroupedUrls = [{ pattern: 'test' }]; const currentGroupedURLs = siteConfig.getGroupedURLs(Audit.AUDIT_TYPES.CWV) || []; let patchedGroupedURLs = []; - if (groupedURLs.length !== 0) { + if (suggestedGroupedUrls.length !== 0) { patchedGroupedURLs = Object.values( - [...currentGroupedURLs, ...groupedURLs].reduce((acc, item) => { + [...currentGroupedURLs, ...suggestedGroupedUrls].reduce((acc, item) => { acc[item.pattern] = item; return acc; }, {}), @@ -70,7 +70,8 @@ export default (context) => { // await site.save(); const groupCount = 0; - await say(`${SUCCESS_MESSAGE_PREFIX}Found ${groupCount} new group(s) for site "${baseURL}" and added them to the configuration. Please re-run the CWV audit to see the results.`); + await say(`${SUCCESS_MESSAGE_PREFIX}Found ${groupCount} new group(s) for site "${baseURL}" and added them` + + ' to the configuration. Please re-run the CWV audit to see the results.'); } catch (error) { log.error(error); await say(`${ERROR_MESSAGE_PREFIX}An error occurred while trying to automatically group pages by URL pattern: ${error.message}.`); diff --git a/test/support/slack/commands/assign-cwv-template-groups.test.js b/test/support/slack/commands/assign-cwv-template-groups.test.js index 12f678fc0..7e151b0a2 100644 --- a/test/support/slack/commands/assign-cwv-template-groups.test.js +++ b/test/support/slack/commands/assign-cwv-template-groups.test.js @@ -87,13 +87,14 @@ describe('AssignCwvTemplateGroups', () => { sandbox.restore(); }); - it('Assign Template-Based Page Groups', async () => { + it('Assigns page groups when the configuration has no patterns but suggestions are available', async () => { const baseUrl = 'https://site0.com'; - const groupedUrls = [{ pattern: 'test' }]; + const currentGroupedUrls = undefined; + const suggestedGroupedUrls = [{ pattern: 'test' }]; const groupCount = 0; dataAccessMock.Site.findByBaseURL.withArgs(baseUrl).resolves(siteMock); - siteConfigMock.getGroupedURLs.withArgs(Audit.AUDIT_TYPES.CWV).returns(groupedUrls); + siteConfigMock.getGroupedURLs.withArgs(Audit.AUDIT_TYPES.CWV).returns(currentGroupedUrls); const command = AssignCwvTemplateGroupsCommand(contextMock); const args = [baseUrl]; @@ -108,14 +109,15 @@ describe('AssignCwvTemplateGroups', () => { `Expected siteConfig.getGroupedURLs to be called with "${Audit.AUDIT_TYPES.CWV}", but it was not`, ).to.be.true; expect( - siteConfigMock.updateGroupedURLs.calledWith(Audit.AUDIT_TYPES.CWV, groupedUrls), + siteConfigMock.updateGroupedURLs.calledWith(Audit.AUDIT_TYPES.CWV, suggestedGroupedUrls), 'Expected siteConfig.updateGroupedURLs to be called , but it was not', ).to.be.true; expect( siteMock.setConfig.calledOnce, 'Expected site.setConfig to be called once, but it was not', ).to.be.true; - const expectedMessage = `${SUCCESS_MESSAGE_PREFIX}Found ${groupCount} new group(s) for site "${baseUrl}" and added them to the configuration. Please re-run the CWV audit to see the results.`; + const expectedMessage = `${SUCCESS_MESSAGE_PREFIX}Found ${groupCount} new group(s) for site "${baseUrl}"` + + ' and added them to the configuration. Please re-run the CWV audit to see the results.'; expect( slackContextMock.say.calledWith(expectedMessage), `Expected say method to be called with message: "${expectedMessage}"`,