Skip to content

Commit 99ce0d0

Browse files
mmiticheSimonMilorderocheleau
authored
test(quantic): Tabs E2E Cypress to Playwright + unit tests Migration (#4802)
[SFINT-5794](https://coveord.atlassian.net/browse/SFINT-5794) ## IN THIS PR: ### Quantic Tab Bar component: - Added a `tabBarItem` slot name for the quantic Tab slot - Added unit tests for the `quanticTabBar` component - Added Playwright E2E tests for the `quanticTabBar` component ### Quantic Tab component: - Added unit tests for the `quanticTab` component - Added Playwright E2E tests for the `quanticTab` component ### UNIT TESTS: QUANTIC TAB: <img width="720" alt="image" src="https://github.com/user-attachments/assets/be94f71b-1910-495d-a340-83532adc702a" /> QUANTIC TAB BAR: <img width="720" alt="image" src="https://github.com/user-attachments/assets/44d9dff0-215b-40c0-b1d9-3fe4142a2b60" /> ### E2E PLAYWRIGHT TESTS: QUANTIC TAB: <img width="1020" alt="image" src="https://github.com/user-attachments/assets/2dbce88c-d25d-458f-acab-1bf968977010" /> QUANTIC TAB BAR: <img width="1020" alt="image" src="https://github.com/user-attachments/assets/dc004d5b-74e9-48d7-b876-e8025a0aeb6e" /> [SFINT-5794]: https://coveord.atlassian.net/browse/SFINT-5794?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: simonmilord <[email protected]> Co-authored-by: erocheleau <[email protected]>
1 parent 4af4238 commit 99ce0d0

File tree

16 files changed

+1187
-18
lines changed

16 files changed

+1187
-18
lines changed

packages/quantic/cypress/e2e/default-1/tab-bar/tab-bar-selectors.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export const TabBarSelectors: TabBarSelector = {
2121
allTabs: () => TabBarSelectors.get().find(tabComponent),
2222
activeTab: () =>
2323
TabBarSelectors.get().find('button.slds-tabs_default__item.slds-is-active'),
24-
moreButton: () => TabBarSelectors.get().find('.tab-bar_more-button'),
24+
moreButton: () =>
25+
TabBarSelectors.get().find('[data-testid="tab-bar_more-section"]'),
2526
moreButtonLabel: () =>
2627
TabBarSelectors.moreButton().find('button').first().invoke('text'),
2728
moreButtonIcon: () => TabBarSelectors.moreButton().find('lightning-icon'),

packages/quantic/decisions/0001-testing-strategy.md

+3
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ Key points for Playwright E2E tests:
3939
- **User Workflow Testing**: Focus on the essential paths users take to accomplish their goals with a given component, this for all the different Quantic use cases: Search, Insight, Case assist and Recomendations.
4040
- **Simulate User Actions**: Test realistic user actions, such as clicking buttons, entering data, and navigating through the UI.
4141
- **External System Interactions**: Verify interactions with external systems, like Coveo APIs and analytics, to ensure components behave as expected in a real-world environment. This also acts as a double test because it will also notify us by failing the test if the external API contract is broken too because we test for the expected request body but also the expected response as much as possible.
42+
- **Browser-specific behaviors**: Test browser-specific behaviors that would interact with our components.
4243

4344
**Example Playwright Test Scenarios**:
4445

4546
- A user uses a Quantic component by interacting with its multiple buttons and triggering Coveo analytics. We test that the expected analytics are sent, that the events are valid, and the response from the analytics API are correct.
4647
- A user navigates through a whole search workflow, interacting with multiple components. We test the expected Search API calls and responses, the analytics calls and response, and also the reactivity of the full search experience.
4748
- Verify the integration between the Quantic components and Salesforce. We test that modifying data through the Salesforce components has an impact and triggers the correct components reactions in a Quantic experience.
49+
- A user modifies the URL parameters. We assess the impact on our components.
50+
- The browser viewport size changes. We verify that our components adapt their display accordingly.
4851

4952
## Alternatives Considered
5053

packages/quantic/force-app/main/default/lwc/quanticPager/e2e/fixture.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {PagerObject} from './pagerObject';
1+
import {PagerObject} from './pageObject';
22
import {quanticBase} from '../../../../../../playwright/fixtures/baseFixture';
33
import {SearchObject} from '../../../../../../playwright/page-object/searchObject';
44
import {
@@ -8,7 +8,7 @@ import {
88
import {InsightSetupObject} from '../../../../../../playwright/page-object/insightSetupObject';
99
import {useCaseEnum} from '../../../../../../playwright/utils/useCase';
1010

11-
const pagerUrl = 's/quantic-pager';
11+
const pageUrl = 's/quantic-pager';
1212

1313
interface PagerOptions {
1414
numberOfPages: number;
@@ -34,7 +34,7 @@ export const testSearch = quanticBase.extend<QuanticPagerE2ESearchFixtures>({
3434
await use(new SearchObject(page, searchRequestRegex));
3535
},
3636
pager: async ({page, options, configuration, search, urlHash}, use) => {
37-
await page.goto(urlHash ? `${pagerUrl}#${urlHash}` : pagerUrl);
37+
await page.goto(urlHash ? `${pageUrl}#${urlHash}` : pageUrl);
3838
configuration.configure(options);
3939
await search.waitForSearchResponse();
4040

@@ -51,7 +51,7 @@ export const testInsight = quanticBase.extend<QuanticPagerE2EInsightFixtures>({
5151
await use(new InsightSetupObject(page));
5252
},
5353
pager: async ({page, options, search, configuration, insightSetup}, use) => {
54-
await page.goto(pagerUrl);
54+
await page.goto(pageUrl);
5555
configuration.configure({...options, useCase: useCaseEnum.insight});
5656
await insightSetup.waitForInsightInterfaceInitialization();
5757
await search.performSearch();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/* eslint-disable jest/no-conditional-expect */
2+
/* eslint-disable no-import-assign */
3+
import QuanticTab from '../quanticTab';
4+
// @ts-ignore
5+
import {createElement} from 'lwc';
6+
import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
7+
8+
jest.mock('c/quanticHeadlessLoader');
9+
10+
let isInitialized = false;
11+
12+
const exampleEngine = {
13+
id: 'exampleEngineId',
14+
};
15+
16+
const defaultOptions = {
17+
engineId: exampleEngine.id,
18+
label: 'Example Tab',
19+
expression: 'exampleExpression',
20+
isActive: false,
21+
};
22+
23+
const selectors = {
24+
initializationError: 'c-quantic-component-error',
25+
tabButton: 'button',
26+
};
27+
28+
const defaultSearchStatusState = {
29+
hasResults: true,
30+
firstSearchExecuted: true,
31+
};
32+
let searchStatusState = defaultSearchStatusState;
33+
34+
const defaultTabState = {
35+
isActive: false,
36+
};
37+
let tabState = defaultTabState;
38+
39+
const functionsMocks = {
40+
buildTab: jest.fn(() => ({
41+
state: tabState,
42+
subscribe: functionsMocks.tabStateSubscriber,
43+
select: functionsMocks.select,
44+
})),
45+
buildSearchStatus: jest.fn(() => ({
46+
state: searchStatusState,
47+
subscribe: functionsMocks.searchStatusStateSubscriber,
48+
})),
49+
tabStateSubscriber: jest.fn((cb) => {
50+
cb();
51+
return functionsMocks.tabStateUnsubscriber;
52+
}),
53+
searchStatusStateSubscriber: jest.fn((cb) => {
54+
cb();
55+
return functionsMocks.searchStatusStateUnsubscriber;
56+
}),
57+
tabStateUnsubscriber: jest.fn(),
58+
searchStatusStateUnsubscriber: jest.fn(),
59+
exampleTabRendered: jest.fn(),
60+
select: jest.fn(),
61+
};
62+
63+
const expectedActiveTabClass = 'slds-is-active';
64+
65+
function createTestComponent(options = defaultOptions) {
66+
const element = createElement('c-quantic-tab', {
67+
is: QuanticTab,
68+
});
69+
for (const [key, value] of Object.entries(options)) {
70+
element[key] = value;
71+
}
72+
document.body.appendChild(element);
73+
return element;
74+
}
75+
76+
function prepareHeadlessState() {
77+
// @ts-ignore
78+
mockHeadlessLoader.getHeadlessBundle = () => {
79+
return {
80+
buildTab: functionsMocks.buildTab,
81+
buildSearchStatus: functionsMocks.buildSearchStatus,
82+
};
83+
};
84+
}
85+
86+
// Helper function to wait until the microtask queue is empty.
87+
function flushPromises() {
88+
return new Promise((resolve) => setTimeout(resolve, 0));
89+
}
90+
91+
function mockSuccessfulHeadlessInitialization() {
92+
// @ts-ignore
93+
mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
94+
if (element instanceof QuanticTab && !isInitialized) {
95+
isInitialized = true;
96+
initialize(exampleEngine);
97+
}
98+
};
99+
}
100+
101+
function mockErroneousHeadlessInitialization() {
102+
// @ts-ignore
103+
mockHeadlessLoader.initializeWithHeadless = (element) => {
104+
if (element instanceof QuanticTab) {
105+
element.setInitializationError();
106+
}
107+
};
108+
}
109+
110+
function setupEventListeners(element) {
111+
element.addEventListener(
112+
'quantic__tabrendered',
113+
functionsMocks.exampleTabRendered
114+
);
115+
}
116+
117+
function cleanup() {
118+
// The jsdom instance is shared across test cases in a single file so reset the DOM
119+
while (document.body.firstChild) {
120+
document.body.removeChild(document.body.firstChild);
121+
}
122+
jest.clearAllMocks();
123+
isInitialized = false;
124+
}
125+
126+
describe('c-quantic-tab', () => {
127+
beforeEach(() => {
128+
mockSuccessfulHeadlessInitialization();
129+
prepareHeadlessState();
130+
});
131+
132+
afterEach(() => {
133+
tabState = defaultTabState;
134+
searchStatusState = defaultSearchStatusState;
135+
cleanup();
136+
});
137+
138+
describe('component initialization', () => {
139+
it('should build the tab and search status controllers with the proper parameters', async () => {
140+
createTestComponent();
141+
await flushPromises();
142+
143+
expect(functionsMocks.buildTab).toHaveBeenCalledTimes(1);
144+
expect(functionsMocks.buildTab).toHaveBeenCalledWith(exampleEngine, {
145+
options: {
146+
expression: defaultOptions.expression,
147+
id: defaultOptions.label,
148+
},
149+
initialState: {
150+
isActive: defaultOptions.isActive,
151+
},
152+
});
153+
expect(functionsMocks.buildSearchStatus).toHaveBeenCalledTimes(1);
154+
expect(functionsMocks.buildSearchStatus).toHaveBeenCalledWith(
155+
exampleEngine
156+
);
157+
});
158+
159+
it('should subscribe to the headless tab and search status state changes', async () => {
160+
createTestComponent();
161+
await flushPromises();
162+
163+
expect(functionsMocks.tabStateSubscriber).toHaveBeenCalledTimes(1);
164+
expect(functionsMocks.searchStatusStateSubscriber).toHaveBeenCalledTimes(
165+
1
166+
);
167+
});
168+
169+
it('should dispatch the quantic__tabrendered event', async () => {
170+
const element = createTestComponent();
171+
setupEventListeners(element);
172+
await flushPromises();
173+
174+
expect(functionsMocks.exampleTabRendered).toHaveBeenCalledTimes(1);
175+
});
176+
});
177+
178+
describe('when an initialization error occurs', () => {
179+
beforeEach(() => {
180+
mockErroneousHeadlessInitialization();
181+
});
182+
183+
afterAll(() => {
184+
mockSuccessfulHeadlessInitialization();
185+
});
186+
187+
it('should display the initialization error component', async () => {
188+
const element = createTestComponent();
189+
await flushPromises();
190+
191+
const initializationError = element.shadowRoot.querySelector(
192+
selectors.initializationError
193+
);
194+
195+
expect(initializationError).not.toBeNull();
196+
});
197+
});
198+
199+
describe('component behavior during the initial search', () => {
200+
describe('when the initial search is not yet executed', () => {
201+
beforeAll(() => {
202+
searchStatusState = {...searchStatusState, firstSearchExecuted: false};
203+
});
204+
205+
it('should not show the tab before the initial search completes', async () => {
206+
const element = createTestComponent();
207+
await flushPromises();
208+
209+
const tab = element.shadowRoot.querySelector(selectors.tabButton);
210+
211+
expect(tab).toBeNull();
212+
});
213+
});
214+
215+
describe('when the initial search is executed', () => {
216+
beforeAll(() => {
217+
searchStatusState = {...searchStatusState, firstSearchExecuted: true};
218+
});
219+
220+
it('should show the tab after the initial search completes', async () => {
221+
const element = createTestComponent();
222+
await flushPromises();
223+
224+
const tab = element.shadowRoot.querySelector(selectors.tabButton);
225+
226+
expect(tab).not.toBeNull();
227+
expect(tab.textContent).toBe(defaultOptions.label);
228+
expect(tab.title).toEqual(defaultOptions.label);
229+
expect(tab.getAttribute('aria-pressed')).toBe('false');
230+
expect(tab.getAttribute('aria-label')).toBe(defaultOptions.label);
231+
});
232+
});
233+
});
234+
235+
describe('when the tab is not active', () => {
236+
beforeAll(() => {
237+
tabState = {...tabState, isActive: false};
238+
});
239+
240+
it('should not display the tab as an active tab', async () => {
241+
const element = createTestComponent();
242+
await flushPromises();
243+
244+
const tab = element.shadowRoot.querySelector(selectors.tabButton);
245+
expect(tab).not.toBeNull();
246+
247+
expect(tab.classList).not.toContain(expectedActiveTabClass);
248+
expect(element.isActive).toBe(false);
249+
});
250+
});
251+
252+
describe('when the tab is active', () => {
253+
beforeAll(() => {
254+
tabState = {...tabState, isActive: true};
255+
});
256+
257+
it('should display the tab as an active tab', async () => {
258+
const element = createTestComponent();
259+
await flushPromises();
260+
261+
const tab = element.shadowRoot.querySelector(selectors.tabButton);
262+
263+
expect(tab.classList).toContain(expectedActiveTabClass);
264+
expect(element.isActive).toBe(true);
265+
});
266+
});
267+
268+
describe('when the tab is clicked or the select method is called', () => {
269+
it('should call the select method of the tab controller', async () => {
270+
const element = createTestComponent();
271+
await flushPromises();
272+
273+
const tab = element.shadowRoot.querySelector(selectors.tabButton);
274+
expect(tab).not.toBeNull();
275+
276+
await tab.click();
277+
await flushPromises();
278+
279+
expect(functionsMocks.select).toHaveBeenCalledTimes(1);
280+
281+
await element.select();
282+
await flushPromises();
283+
284+
expect(functionsMocks.select).toHaveBeenCalledTimes(2);
285+
});
286+
});
287+
});

0 commit comments

Comments
 (0)