Skip to content

Commit 6f21e25

Browse files
mmitichefbeaudoincoveoSimonMilorderocheleau
authored
test(quantic): first setup of playwright added in Quantic (#4597)
## [SFINT-5768](https://coveord.atlassian.net/browse/SFINT-5768) ### Intro In this PR, we introduce the Playwright setup for the Quantic project, allowing for automated E2E tests on Lightning Web Components (LWC) in Salesforce. The Playwright structure here includes: ### Page Objects: Page objects encapsulate component selectors and interactions, making tests more modular and maintainable, in the proposed structure we have common page objects and unique page objects. The common page objects will be shared across all the Quantic E2E tests, for example: - `ConfigurationObject`: responsible of configuring the Quantic Example page to initializes the test environment for a given test. - `SearchObject`: responsible of performing a search and intercepting the search requests in both the search use case and the insight use case. - `InsightObject`: responsible of performing the logic that is specific to the insight use case, like for example waiting the initialization of the Quantic Insight Interface. A unique page object is an object that is specific to a given component, like in this example the `PagerObject` ### Fixtures: A Playwright fixture is a reusable setup that provides a consistent environment for tests. I created a base fixture called `quanticBase` that will be the base of all the fixture we will create for each Quantic component, this base fixture by default uses `ConfigurationObject`, `SearchObject` and `InsightObject` page objects as these are always needed in any test. Additionally each component will have his own fixtures that will extend `quanticBase`, like for example in this example, the QuanticPager has two fixtures `testSearch` and `testInsight`, two fixtures that provides the setup to test the component in the search use case and the insight use case respectively. ### Tests: Following this structure, each Quantic component will now contain a new folder called `e2e` that will contain the page object of the component, the fixtures needed to test and the test spec. Each component will have the following structure: ``` QuanticPager/ │ ├── e2e/ │ ├── page-object.ts │ ├── fixture.ts │ └── quantic-pager.e2e.ts ├── __test__/ │ └── quantic-pager.unit.test.ts │ ├── QuanticPager.js ├── QuanticPager.html ├── QuanticPager.css ``` Each component will have the component logic, the unit tests and the E2E in the same folder. Jest Unit tests should focus on the following points: - Test component behaviour in isolation. - Mock External Dependencies like the capabilities used from the Headless library. - Ensure the component renders the correct HTML based on its state and input properties. - Verify that component properties update correctly based on user interactions or lifecycle events. - Cover edge cases. Playwright E2E tests should focus on the following points: - Test the essential paths that users take to achieve their goals using a given component. - Mimic actual user actions, such as clicking buttons, entering data and navigation. - Test interactions with external systems like our Coveo APIs and analytics. [SFINT-5768]: https://coveord.atlassian.net/browse/SFINT-5768?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Frederic Beaudoin <[email protected]> Co-authored-by: Simon Milord <[email protected]> Co-authored-by: Etienne Rocheleau <[email protected]>
1 parent b053628 commit 6f21e25

20 files changed

+662
-2
lines changed

package-lock.json

+76
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/quantic/.forceignore

+3
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ package.xml
1313
**/__tests__/**
1414
**/testUtils/**/*
1515

16+
# E2E tests
17+
**/e2e/**
18+
1619
**/README.md

packages/quantic/.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ force-app/main/default/staticresources/dompurify
4444
# Test Coverage & Report
4545
coverage
4646
reports
47+
/test-results/
48+
/playwright-report/
49+
/playwright/.cache/
4750

4851
# Cypress artifacts
4952
.cache/
@@ -58,4 +61,4 @@ docs/out/*
5861
# Environment Variables
5962
.env
6063

61-
.tmp
64+
.tmp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Decision Record: Testing Strategy for Quantic
2+
3+
- **Date**: 2024-11-06
4+
- **Status**: accepted
5+
6+
## Context
7+
8+
Our project involves testing Lightning Web Components (LWCs) to ensure quality and reliability. We have decided to use Jest for unit testing to verify component logic and isolated behavior and Playwright for end-to-end (E2E) testing to validate user interactions and integration within the full UI. This document outlines the criteria for deciding what should be tested in each environment.
9+
10+
## Decision
11+
12+
We have established the following guidelines for Jest and Playwright testing in LWCs:
13+
14+
### Jest Unit Tests
15+
16+
Jest will focus on isolated component behavior, covering specific scenarios without involving the browser environment or real user workflows.
17+
18+
Key points for Jest unit tests:
19+
20+
- **Isolated Component Behavior**: Tests should verify the component's behavior in isolation, without dependency on external systems or other components.
21+
- **Mock External Dependencies**: Use mocks to simulate external dependencies, such as capabilities used from the Headless library, ensuring unit tests are fast and isolated.
22+
- **HTML Rendering**: Validate that the component renders the correct HTML based on its state (including but not limited to: internal state, mocked headless state) and input properties.
23+
- **Property Updates**: Ensure component properties update as expected in response to user interactions and lifecycle events.
24+
- **Edge Cases**: Cover edge cases to ensure the component handles unexpected or extreme inputs gracefully.
25+
26+
**Example Jest Test Scenarios**:
27+
28+
- Mock a Headless controller call to test a specific component in isolation.
29+
- Mock a Headless state and ensure the component renders correctly by reflecting the state in the UI.
30+
- Verify that clicking a button updates a specific property, that the rendered HTML reflects this change and the corresponding Headless component controller was called correctly and with the correct parameters.
31+
- Test a lifecycle event, such as `connectedCallback`, to confirm it initializes the component correctly.
32+
33+
### Playwright E2E Tests
34+
35+
Playwright E2E tests will simulate actual user actions to validate the functionality of LWCs within the full user interface and ensure interaction with external systems.
36+
37+
Key points for Playwright E2E tests:
38+
39+
- **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.
40+
- **Simulate User Actions**: Test realistic user actions, such as clicking buttons, entering data, and navigating through the UI.
41+
- **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+
43+
**Example Playwright Test Scenarios**:
44+
45+
- 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.
46+
- 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.
47+
- 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.
48+
49+
## Alternatives Considered
50+
51+
1. **Relying Solely on E2E with Cypress for All Testing**:
52+
- This was used before and is now rejected because using only E2E tests would slow down feedback cycles and increase test maintenance due to the complexity of mocking and setting up end-to-end environments for every component interaction.
53+
54+
## Consequences
55+
56+
- This approach balances efficient, reliable test coverage with manageable test maintenance. Jest allows us to cover specific logic and UI rendering in a lightweight manner, while Playwright provides confidence that the application works as expected for end-users.
57+
- By clearly separating Jest and Playwright responsibilities, we prevent redundancy and maintain test suite performance.
58+
- We are also taking the approach of relying more on the tests already done upstream in Headless for example where each component controller is already tested too, we choose not to re-test that once you trigger a Headless action correctly, we trust that it will have the correct result on the state so we don't need to re-test all of that with e2e tests.

packages/quantic/decisions/index.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Architecture Decision Records
2+
3+
This folder contains records of key architecture and technical decisions.

packages/quantic/force-app/main/default/.eslintrc.json

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
"rules": {
2121
"@lwc/lwc/no-async-operation": "off"
2222
}
23+
},
24+
{
25+
"files": ["**/e2e/*.ts"],
26+
"parser": "@typescript-eslint/parser",
27+
"rules": {
28+
"no-redeclare": ["error", {"builtinGlobals": false}]
29+
}
2330
}
2431
]
2532
}

packages/quantic/force-app/main/default/lwc/jsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"../../../../typings/lwc/**/*.d.ts",
9696
"../../../../coveo.d.ts"
9797
],
98+
"exclude": ["**/e2e/**"],
9899
"typeAcquisition": {
99100
"include": ["jest"]
100101
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {PagerObject} from './pagerObject';
2+
import {quanticBase} from '../../../../../../playwright/fixtures/baseFixture';
3+
import {SearchObject} from '../../../../../../playwright/page-object/searchObject';
4+
import {
5+
searchRequestRegex,
6+
insightSearchRequestRegex,
7+
} from '../../../../../../playwright/utils/requests';
8+
import {InsightSetupObject} from '../../../../../../playwright/page-object/insightSetupObject';
9+
import {useCaseEnum} from '../../../../../../playwright/utils/useCase';
10+
11+
const pagerUrl = 's/quantic-pager';
12+
13+
interface PagerOptions {
14+
numberOfPages: number;
15+
}
16+
type QuanticPagerE2EFixtures = {
17+
pager: PagerObject;
18+
search: SearchObject;
19+
options: Partial<PagerOptions>;
20+
};
21+
22+
type QuanticPagerE2ESearchFixtures = QuanticPagerE2EFixtures & {
23+
urlHash: string;
24+
};
25+
26+
type QuanticPagerE2EInsightFixtures = QuanticPagerE2ESearchFixtures & {
27+
insightSetup: InsightSetupObject;
28+
};
29+
30+
export const testSearch = quanticBase.extend<QuanticPagerE2ESearchFixtures>({
31+
options: {},
32+
urlHash: '',
33+
search: async ({page}, use) => {
34+
await use(new SearchObject(page, searchRequestRegex));
35+
},
36+
pager: async ({page, options, configuration, search, urlHash}, use) => {
37+
await page.goto(urlHash ? `${pagerUrl}#${urlHash}` : pagerUrl);
38+
configuration.configure(options);
39+
await search.waitForSearchResponse();
40+
41+
await use(new PagerObject(page));
42+
},
43+
});
44+
45+
export const testInsight = quanticBase.extend<QuanticPagerE2EInsightFixtures>({
46+
options: {},
47+
search: async ({page}, use) => {
48+
await use(new SearchObject(page, insightSearchRequestRegex));
49+
},
50+
insightSetup: async ({page}, use) => {
51+
await use(new InsightSetupObject(page));
52+
},
53+
pager: async ({page, options, search, configuration, insightSetup}, use) => {
54+
await page.goto(pagerUrl);
55+
configuration.configure({...options, useCase: useCaseEnum.insight});
56+
await insightSetup.waitForInsightInterfaceInitialization();
57+
await search.performSearch();
58+
await search.waitForSearchResponse();
59+
await use(new PagerObject(page));
60+
},
61+
});
62+
63+
export {expect} from '@playwright/test';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {Locator, Page, Request} from '@playwright/test';
2+
import {isUaCustomEvent} from '../../../../../../playwright/utils/requests';
3+
4+
export class PagerObject {
5+
constructor(public page: Page) {
6+
this.page = page;
7+
}
8+
9+
get nextPageButton(): Locator {
10+
return this.page.getByRole('button', {name: /Next Page/i});
11+
}
12+
13+
get previousPageButton(): Locator {
14+
return this.page.getByRole('button', {name: /Previous Page/i});
15+
}
16+
17+
get pageButtons(): Locator {
18+
return this.page.locator('c-quantic-number-button button');
19+
}
20+
21+
pageButton(pageNumber: number): Locator {
22+
return this.pageButtons.nth(pageNumber - 1);
23+
}
24+
25+
async goToLastPage(): Promise<void> {
26+
await this.pageButtons.nth(-1).click();
27+
}
28+
29+
async clickNextPageButton(): Promise<void> {
30+
await this.nextPageButton.click();
31+
}
32+
33+
async clickPreviousPageButton(): Promise<void> {
34+
await this.previousPageButton.click();
35+
}
36+
37+
async clickPageNumberButton(pageNumber: number): Promise<void> {
38+
await this.pageButtons.nth(pageNumber - 1).click();
39+
}
40+
41+
async waitForPagerUaAnalytics(eventValue): Promise<Request> {
42+
const uaRequest = this.page.waitForRequest((request) => {
43+
if (isUaCustomEvent(request)) {
44+
const requestBody = request.postDataJSON?.();
45+
const expectedFields = {
46+
eventType: 'getMoreResults',
47+
eventValue: eventValue,
48+
};
49+
return Object.keys(expectedFields).every(
50+
(key) => requestBody?.[key] === expectedFields[key]
51+
);
52+
}
53+
return false;
54+
});
55+
return uaRequest;
56+
}
57+
58+
async waitForPagerNextUaAnalytics(): Promise<Request> {
59+
return this.waitForPagerUaAnalytics('pagerNext');
60+
}
61+
62+
async waitForPagerPreviousUaAnalytics(): Promise<Request> {
63+
return this.waitForPagerUaAnalytics('pagerPrevious');
64+
}
65+
66+
async waitForPagerNumberUaAnalytics(): Promise<Request> {
67+
return this.waitForPagerUaAnalytics('pagerNumber');
68+
}
69+
}

0 commit comments

Comments
 (0)