diff --git a/packages/quantic/force-app/main/default/lwc/quanticTab/__tests__/quanticTab.test.js b/packages/quantic/force-app/main/default/lwc/quanticTab/__tests__/quanticTab.test.js index 6007955d7e..f8ae7b064c 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticTab/__tests__/quanticTab.test.js +++ b/packages/quantic/force-app/main/default/lwc/quanticTab/__tests__/quanticTab.test.js @@ -17,7 +17,7 @@ const defaultOptions = { engineId: exampleEngine.id, label: 'Example Tab', expression: 'exampleExpression', - isActive: true, + isActive: false, }; const selectors = { @@ -25,35 +25,37 @@ const selectors = { tabButton: 'button', }; -const mockSearchStatusState = { +const defaultSearchStatusState = { hasResults: true, + firstSearchExecuted: true, }; +let searchStatusState = defaultSearchStatusState; -const mockBuildTabState = { +const defaultTabState = { isActive: false, }; - -const mockSearchStatus = { - state: mockSearchStatusState, - subscribe: jest.fn((callback) => { - mockSearchStatus.callback = callback; - return jest.fn(); - }), -}; +let tabState = defaultTabState; const functionsMocks = { buildTab: jest.fn(() => ({ - state: mockBuildTabState, - subscribe: functionsMocks.subscribe, + state: tabState, + subscribe: functionsMocks.tabStateSubscriber, select: functionsMocks.select, })), - buildSearchStatus: jest.fn(() => mockSearchStatus), - subscribe: jest.fn((cb) => { + buildSearchStatus: jest.fn(() => ({ + state: searchStatusState, + subscribe: functionsMocks.searchStatusStateSubscriber, + })), + tabStateSubscriber: jest.fn((cb) => { + cb(); + return functionsMocks.tabStateUnsubscriber; + }), + searchStatusStateSubscriber: jest.fn((cb) => { cb(); - return functionsMocks.unsubscribe; + return functionsMocks.searchStatusStateUnsubscriber; }), - unsubscribe: jest.fn(() => {}), - unsubscribeSearchStatus: jest.fn(() => {}), + tabStateUnsubscriber: jest.fn(), + searchStatusStateUnsubscriber: jest.fn(), exampleTabRendered: jest.fn(), select: jest.fn(), }; @@ -61,8 +63,6 @@ const functionsMocks = { const expectedActiveTabClass = 'slds-is-active'; function createTestComponent(options = defaultOptions) { - prepareHeadlessState(); - const element = createElement('c-quantic-tab', { is: QuanticTab, }); @@ -83,15 +83,6 @@ function prepareHeadlessState() { }; } -function simulateSearchStatusUpdate( - hasResults = true, - firstSearchExecuted = true -) { - mockSearchStatus.state.hasResults = hasResults; - mockSearchStatus.state.firstSearchExecuted = firstSearchExecuted; - mockSearchStatus.callback(); -} - // Helper function to wait until the microtask queue is empty. function flushPromises() { return new Promise((resolve) => setTimeout(resolve, 0)); @@ -135,13 +126,16 @@ function cleanup() { describe('c-quantic-tab', () => { beforeEach(() => { mockSuccessfulHeadlessInitialization(); + prepareHeadlessState(); }); afterEach(() => { + tabState = defaultTabState; + searchStatusState = defaultSearchStatusState; cleanup(); }); - describe('controller initialization', () => { + describe('component initialization', () => { it('should build the tab and search status controllers with the proper parameters', async () => { createTestComponent(); await flushPromises(); @@ -166,8 +160,18 @@ describe('c-quantic-tab', () => { createTestComponent(); await flushPromises(); - expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1); - expect(mockSearchStatus.subscribe).toHaveBeenCalledTimes(1); + expect(functionsMocks.tabStateSubscriber).toHaveBeenCalledTimes(1); + expect(functionsMocks.searchStatusStateSubscriber).toHaveBeenCalledTimes( + 1 + ); + }); + + it('should dispatch the quantic__tabrendered event', async () => { + const element = createTestComponent(); + setupEventListeners(element); + await flushPromises(); + + expect(functionsMocks.exampleTabRendered).toHaveBeenCalledTimes(1); }); }); @@ -192,48 +196,53 @@ describe('c-quantic-tab', () => { }); }); - describe('when the component renders', () => { - it('should not show the tab before the initial search completes', async () => { - const element = createTestComponent(); - simulateSearchStatusUpdate(true, false); - await flushPromises(); + describe('component behavior during the initial search', () => { + describe('when the initial search is not yet executed', () => { + beforeAll(() => { + searchStatusState = {...searchStatusState, firstSearchExecuted: false}; + }); - const tab = element.shadowRoot.querySelector(selectors.tabButton); + it('should not show the tab before the initial search completes', async () => { + const element = createTestComponent(); + await flushPromises(); - expect(tab).toBeNull(); - }); + const tab = element.shadowRoot.querySelector(selectors.tabButton); - it('should show the tab after the initial search completes', async () => { - const element = createTestComponent(); - simulateSearchStatusUpdate(); - await flushPromises(); + expect(tab).toBeNull(); + }); + }); - const tab = element.shadowRoot.querySelector(selectors.tabButton); + describe('when the initial search is executed', () => { + beforeAll(() => { + searchStatusState = {...searchStatusState, firstSearchExecuted: true}; + }); - expect(tab).not.toBeNull(); - expect(tab.textContent).toBe(defaultOptions.label); - expect(tab.title).toEqual(defaultOptions.label); - expect(tab.getAttribute('aria-pressed')).toBe('false'); - expect(tab.getAttribute('aria-label')).toBe(defaultOptions.label); - }); + it('should show the tab after the initial search completes', async () => { + const element = createTestComponent(); + await flushPromises(); - it('should dispatch the quantic__tabrendered event', async () => { - const element = createTestComponent(); - setupEventListeners(element); - await flushPromises(); + const tab = element.shadowRoot.querySelector(selectors.tabButton); - expect(functionsMocks.exampleTabRendered).toHaveBeenCalledTimes(1); + expect(tab).not.toBeNull(); + expect(tab.textContent).toBe(defaultOptions.label); + expect(tab.title).toEqual(defaultOptions.label); + expect(tab.getAttribute('aria-pressed')).toBe('false'); + expect(tab.getAttribute('aria-label')).toBe(defaultOptions.label); + }); }); }); describe('when the tab is not active', () => { - it('should render the tab without the active class', async () => { + beforeAll(() => { + tabState = {...tabState, isActive: false}; + }); + + it('should not display the tab as an active tab', async () => { const element = createTestComponent(); await flushPromises(); const tab = element.shadowRoot.querySelector(selectors.tabButton); - tab.click(); - await flushPromises(); + expect(tab).not.toBeNull(); expect(tab.classList).not.toContain(expectedActiveTabClass); expect(element.isActive).toBe(false); @@ -241,20 +250,15 @@ describe('c-quantic-tab', () => { }); describe('when the tab is active', () => { - it('should render the tab with the active class', async () => { - functionsMocks.buildTab.mockImplementation(() => ({ - state: { - isActive: true, - }, - subscribe: functionsMocks.subscribe, - select: functionsMocks.select, - })); + beforeAll(() => { + tabState = {...tabState, isActive: true}; + }); + + it('should display the tab as an active tab', async () => { const element = createTestComponent(); await flushPromises(); const tab = element.shadowRoot.querySelector(selectors.tabButton); - tab.click(); - await flushPromises(); expect(tab.classList).toContain(expectedActiveTabClass); expect(element.isActive).toBe(true); @@ -262,20 +266,7 @@ describe('c-quantic-tab', () => { }); describe('when the tab is clicked', () => { - it('should trigger the select method', async () => { - const element = createTestComponent(); - await flushPromises(); - - const tab = element.shadowRoot.querySelector(selectors.tabButton); - expect(tab).not.toBeNull(); - - await tab.click(); - await flushPromises(); - - expect(functionsMocks.select).toHaveBeenCalled(); - }); - - it('should select the tab and make it active', async () => { + it('should call the select method of the tab controller', async () => { const element = createTestComponent(); await flushPromises(); @@ -285,21 +276,19 @@ describe('c-quantic-tab', () => { await tab.click(); await flushPromises(); - expect(element.isActive).toBe(true); - expect(tab.getAttribute('aria-pressed')).toBe('true'); - expect(tab.classList).toContain('slds-is-active'); + expect(functionsMocks.select).toHaveBeenCalledTimes(1); }); }); - describe('when calling the select method', () => { - it('should select the tab', async () => { + describe('when calling the public select method of the component', () => { + it('should call the select method of the tab controller', async () => { const element = createTestComponent(); await flushPromises(); await element.select(); await flushPromises(); - expect(functionsMocks.select).toHaveBeenCalled(); + expect(functionsMocks.select).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/fixture.ts b/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/fixture.ts index 4cf69bf444..2f2d213f66 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/fixture.ts +++ b/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/fixture.ts @@ -10,19 +10,19 @@ import { const pageUrl = 's/quantic-tab'; -interface TabBarOptions {} +interface TabOptions {} type QuanticTabE2EFixtures = { tab: TabObject; search: SearchObject; - options: Partial; + options: Partial; }; type QuanticTabE2ESearchFixtures = QuanticTabE2EFixtures & { urlHash: string; }; -type QuanticTabE2EInsightFixtures = QuanticTabE2ESearchFixtures & { +type QuanticTabE2EInsightFixtures = QuanticTabE2EFixtures & { insightSetup: InsightSetupObject; }; diff --git a/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/pageObject.ts b/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/pageObject.ts index 54ba340a9c..d7ed010202 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/pageObject.ts +++ b/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/pageObject.ts @@ -1,4 +1,4 @@ -import {Locator, Page, Request} from '@playwright/test'; +import type {Locator, Page, Request, Response} from '@playwright/test'; import {isUaSearchEvent} from '../../../../../../playwright/utils/requests'; export class TabObject { @@ -10,25 +10,16 @@ export class TabObject { return this.page.locator('c-quantic-tab'); } - get tabButton(): Locator { - return this.tab.locator('button.slds-tabs_default__item.tab_button'); + tabButton(tabLabel): Locator { + return this.tab.getByRole('button', {name: new RegExp(`^${tabLabel}$`)}); } - get activeTabLabel(): Locator { - return this.tab - .locator('button.slds-tabs_default__item.slds-is-active') - .locator('span.slds-tabs_default__link'); + get activeTabLabel(): Promise { + return this.tab.locator('button.slds-is-active').textContent(); } - tabLabel(tabIndex: number): Promise { - return this.tab - .locator('span.slds-tabs_default__link') - .nth(tabIndex) - .textContent(); - } - - async clickTabButton(tabIndex: number): Promise { - await this.tabButton.nth(tabIndex).click(); + async clickTabButton(tabLabel: string): Promise { + await this.tabButton(tabLabel).click(); } async pressTabThenEnter(): Promise { @@ -43,24 +34,46 @@ export class TabObject { await this.page.keyboard.press('Space'); } - async waitForTabUaAnalytics(actionCause): Promise { + async waitForTabSearchUaAnalytics( + actionCause, + customChecker?: Function + ): Promise { const uaRequest = this.page.waitForRequest((request) => { if (isUaSearchEvent(request)) { const requestBody = request.postDataJSON?.(); + const {customData} = requestBody; + const expectedFields = { actionCause: actionCause, originContext: 'Search', }; - return Object.keys(expectedFields).every( + const matchesExpectedFields = Object.keys(expectedFields).every( (key) => requestBody?.[key] === expectedFields[key] ); + + return ( + matchesExpectedFields && + (customChecker ? customChecker(customData) : true) + ); } return false; }); return uaRequest; } - async waitForTabSelectUaAnalytics(): Promise { - return this.waitForTabUaAnalytics('interfaceChange'); + async waitForTabSelectUaAnalytics(expectedFields: object): Promise { + return this.waitForTabSearchUaAnalytics( + 'interfaceChange', + (customData: object) => { + return Object.keys(expectedFields).every( + (key) => customData?.[key] === expectedFields[key] + ); + } + ); + } + + extractActionCauseFromSearchResponse(response: Response) { + const {analytics} = response.request().postDataJSON(); + return analytics.actionCause; } } diff --git a/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/quanticTab.e2e.ts b/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/quanticTab.e2e.ts index a5bfe34bc0..388ba650f8 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/quanticTab.e2e.ts +++ b/packages/quantic/force-app/main/default/lwc/quanticTab/e2e/quanticTab.e2e.ts @@ -5,16 +5,20 @@ import { } from '../../../../../../playwright/utils/useCase'; const fixtures = { - search: testSearch, - insight: testInsight, + search: testSearch as typeof testSearch, + insight: testInsight as typeof testInsight, }; const expectedActionCause = 'interfaceChange'; -const expectedOriginContext = 'Search'; -const expectedTabsLabels = ['All', 'Case', 'Knowledge']; +const exampleTabs = ['All', 'Case', 'Knowledge']; useCaseTestCases.forEach((useCase) => { - let test = fixtures[useCase.value]; + let test; + if (useCase.value === useCaseEnum.search) { + test = fixtures[useCase.value] as typeof testSearch; + } else { + test = fixtures[useCase.value] as typeof testInsight; + } test.describe(`quantic tab ${useCase.label}`, () => { test.describe('when clicking on a tab', () => { @@ -24,48 +28,47 @@ useCaseTestCases.forEach((useCase) => { }) => { const selectedTabIndex = 0; const searchResponsePromise = search.waitForSearchResponse(); - const uaRequestPromise = tab.waitForTabSelectUaAnalytics(); - await tab.clickTabButton(selectedTabIndex); + const uaRequestPromise = tab.waitForTabSelectUaAnalytics({ + interfaceChangeTo: exampleTabs[selectedTabIndex], + }); + await tab.clickTabButton(exampleTabs[selectedTabIndex]); const searchResponse = await searchResponsePromise; - const {analytics} = searchResponse.request().postDataJSON(); - expect(analytics.actionCause).toEqual(expectedActionCause); - expect(analytics.originContext).toEqual(expectedOriginContext); + await uaRequestPromise; - const analyticsResponse = await uaRequestPromise; - const {customData} = analyticsResponse.postDataJSON(); - expect(customData.interfaceChangeTo).toEqual( - expectedTabsLabels[selectedTabIndex] - ); + const actionCause = + tab.extractActionCauseFromSearchResponse(searchResponse); + expect(actionCause).toEqual(expectedActionCause); }); }); if (useCase.value === useCaseEnum.search) { test.describe('when loading selected tab from URL', () => { + const expectedTab = exampleTabs[2]; test.use({ - urlHash: 'tab=Knowledge', + urlHash: `tab=${expectedTab}`, }); + test('should make the right tab active', async ({tab}) => { - const desiredTabLabel = expectedTabsLabels[2]; - expect(await tab.activeTabLabel.textContent()).toBe(desiredTabLabel); + expect(await tab.activeTabLabel).toBe(expectedTab); }); }); } test.describe('when testing accessibility', () => { test('should be accessible to keyboard', async ({tab}) => { - await tab.clickTabButton(0); + await tab.clickTabButton(exampleTabs[0]); - let activeTabLabel = await tab.activeTabLabel.textContent(); - expect(activeTabLabel).toEqual(expectedTabsLabels[0]); + let activeTabLabel = await tab.activeTabLabel; + expect(activeTabLabel).toEqual(exampleTabs[0]); await tab.pressTabThenEnter(); - activeTabLabel = await tab.activeTabLabel.textContent(); - expect(activeTabLabel).toEqual(expectedTabsLabels[1]); + activeTabLabel = await tab.activeTabLabel; + expect(activeTabLabel).toEqual(exampleTabs[1]); await tab.pressShiftTabThenSpace(); - activeTabLabel = await tab.activeTabLabel.textContent(); - expect(activeTabLabel).toEqual(expectedTabsLabels[0]); + activeTabLabel = await tab.activeTabLabel; + expect(activeTabLabel).toEqual(exampleTabs[0]); }); }); });