Skip to content

Commit

Permalink
test(quantic): SFINT-5832 Sort E2E tests migrate from Cypress to Play…
Browse files Browse the repository at this point in the history
…wright (#4777)

https://coveord.atlassian.net/browse/SFINT-5832

**IN THIS PR:**
- Added Playwright E2E tests for the quantic-sort component

**UNIT TESTS:**
- No need, as UTs already there

**E2E PLAYWRIGHT TESTS:**
- Playwright for Sort component

---------

Co-authored-by: mmitiche <[email protected]>
Co-authored-by: Etienne Rocheleau <[email protected]>
Co-authored-by: Simon Milord <[email protected]>
  • Loading branch information
4 people authored Feb 11, 2025
1 parent fa6a88d commit 9c846ba
Show file tree
Hide file tree
Showing 5 changed files with 466 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import QuanticSort from 'c/quanticSort';
import {createElement} from 'lwc';
import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';

const selectors = {
lightningCombobox: 'lightning-combobox',
componentError: 'c-quantic-component-error',
sortDropdown: '[data-testid="sort-dropdown"]',
initializationError: 'c-quantic-component-error',
};

const sortVariants = {
default: {
name: 'default',
Expand All @@ -20,25 +27,40 @@ const sortVariants = {
},
};

const sortByLabel = 'Sort By';

jest.mock('c/quanticHeadlessLoader');
jest.mock('@salesforce/label/c.quantic_SortBy', () => ({default: 'Sort By'}), {
virtual: true,
});
jest.mock(
'@salesforce/label/c.quantic_SortBy',
() => ({default: sortByLabel}),
{
virtual: true,
}
);

function mockBueno() {
jest.spyOn(mockHeadlessLoader, 'getBueno').mockReturnValue(
new Promise(() => {
// @ts-ignore
global.Bueno = {
isString: jest
.fn()
.mockImplementation(
(value) =>
Object.prototype.toString.call(value) === '[object String]'
),
};
})
);
function mockBueno(shouldError = false) {
// @ts-ignore
mockHeadlessLoader.getBueno = () => {
// @ts-ignore
global.Bueno = {
isString: jest
.fn()
.mockImplementation(
(value) => Object.prototype.toString.call(value) === '[object String]'
),
StringValue: jest.fn(),
RecordValue: jest.fn(),
Schema: jest.fn(() => ({
validate: () => {
if (shouldError) {
throw new Error();
}
jest.fn();
},
})),
};
return new Promise((resolve) => resolve());
};
}

let isInitialized = false;
Expand All @@ -47,43 +69,55 @@ const exampleEngine = {
id: 'exampleEngineId',
};

const mockSearchStatusState = {
const defaultSearchStatusState = {
hasResults: true,
};

const mockSearchStatus = {
state: mockSearchStatusState,
subscribe: jest.fn((callback) => {
callback();
return jest.fn();
}),
};
let searchStatusState = defaultSearchStatusState;

const functionsMocks = {
buildSort: jest.fn(() => ({
state: {},
subscribe: functionsMocks.subscribe,
subscribe: functionsMocks.sortStateSubscriber,
sortBy: functionsMocks.sortBy,
})),
buildSearchStatus: jest.fn(() => ({
state: searchStatusState,
subscribe: functionsMocks.searchStatusStateSubscriber,
})),
buildCriterionExpression: jest.fn((criterion) => criterion),
buildRelevanceSortCriterion: jest.fn(() => 'relevance'),
buildDateSortCriterion: jest.fn(() => 'date'),
buildSearchStatus: jest.fn(() => mockSearchStatus),
subscribe: jest.fn((cb) => {
sortStateSubscriber: jest.fn((cb) => {
cb();
return functionsMocks.unsubscribe;
return functionsMocks.sortStateUnsubscriber;
}),
unsubscribe: jest.fn(() => {}),
searchStatusStateSubscriber: jest.fn((cb) => {
cb();
return functionsMocks.searchStatusStateUnsubscriber;
}),
sortStateUnsubscriber: jest.fn(),
searchStatusStateUnsubscriber: jest.fn(),
sortBy: jest.fn(),
};

const defaultOptions = {
engineId: exampleEngine.id,
variant: 'default',
};

const expectedSortByLabel = 'Sort By';
/**
* Mocks the return value of the assignedNodes method.
* @param {Array<Element>} assignedElements
*/
function mockSlotAssignedNodes(assignedElements) {
HTMLSlotElement.prototype.assignedNodes = function () {
return assignedElements;
};
}

function createTestComponent(options = defaultOptions) {
function createTestComponent(options = defaultOptions, assignedElements = []) {
prepareHeadlessState();
mockSlotAssignedNodes(assignedElements);

const element = createElement('c-quantic-sort', {
is: QuanticSort,
Expand Down Expand Up @@ -128,6 +162,15 @@ function mockSuccessfulHeadlessInitialization() {
};
}

function mockErroneousHeadlessInitialization() {
// @ts-ignore
mockHeadlessLoader.initializeWithHeadless = (element) => {
if (element instanceof QuanticSort) {
element.setInitializationError();
}
};
}

function cleanup() {
// The jsdom instance is shared across test cases in a single file so reset the DOM
while (document.body.firstChild) {
Expand All @@ -145,14 +188,152 @@ describe('c-quantic-sort', () => {

afterEach(() => {
cleanup();
searchStatusState = defaultSearchStatusState;
});

describe('when an initialization error occurs', () => {
beforeEach(() => {
mockErroneousHeadlessInitialization();
});

it('should display the initialization error component', async () => {
const element = createTestComponent();
await flushPromises();

const initializationError = element.shadowRoot.querySelector(
selectors.initializationError
);

expect(initializationError).not.toBeNull();
});
});

describe('controller initialization', () => {
it('should subscribe to the headless state changes', async () => {
it('should subscribe to the headless sort and search status state changes', async () => {
createTestComponent();
await flushPromises();

expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1);
expect(functionsMocks.sortStateSubscriber).toHaveBeenCalledTimes(1);
expect(functionsMocks.searchStatusStateSubscriber).toHaveBeenCalledTimes(
1
);
});
});

describe('when no results are found', () => {
beforeAll(() => {
searchStatusState = {...defaultSearchStatusState, hasResults: false};
});

it('should not display the sort dropdown', async () => {
const element = createTestComponent();
await flushPromises();

const sortDropdown = element.shadowRoot.querySelector(
selectors.sortDropdown
);

expect(sortDropdown).toBeNull();
});
});

describe('when a sort option is selected', () => {
it('should call the sortBy method of the sort controller', async () => {
const element = createTestComponent();
await flushPromises();

const lightningCombobox = element.shadowRoot.querySelector(
selectors.lightningCombobox
);
const exampleDefaultSortOptionValue = 'relevance';
expect(lightningCombobox).not.toBeNull();

lightningCombobox.dispatchEvent(
new CustomEvent('change', {
detail: {value: exampleDefaultSortOptionValue},
})
);

expect(functionsMocks.sortBy).toHaveBeenCalledTimes(1);
expect(functionsMocks.sortBy).toHaveBeenCalledWith(
exampleDefaultSortOptionValue
);
});
});

describe('when custom sort options are passed', () => {
const exampleSlot = {
value: 'example value',
label: 'example label',
criterion: {
by: 'example field',
order: 'example order',
},
};
const exampleAssignedElements = [exampleSlot];

it('should build the controller with the correct sort option and display the custom sort options', async () => {
const element = createTestComponent(
defaultOptions,
exampleAssignedElements
);
await flushPromises();

expect(functionsMocks.buildSort).toHaveBeenCalledTimes(1);
expect(functionsMocks.buildSort).toHaveBeenCalledWith(exampleEngine, {
initialState: {
criterion: {
by: 'example field',
order: 'example order',
},
},
});
const lightningCombobox = element.shadowRoot.querySelector(
selectors.lightningCombobox
);

expect(lightningCombobox.options).toEqual([exampleSlot]);
});
});

describe('when invalid sort options are passed', () => {
beforeEach(() => {
mockBueno(true);
jest.spyOn(console, 'error').mockImplementation(() => {});
});

const invalidExampleSlot = {
value: 'example value',
label: '',
criterion: {
by: 'example field',
order: 'example order',
},
};
const exampleAssignedElements = [invalidExampleSlot];

it('should display the component error', async () => {
const element = createTestComponent(
defaultOptions,
exampleAssignedElements
);
await flushPromises();

expect(functionsMocks.buildSort).toHaveBeenCalledTimes(1);
expect(functionsMocks.buildSort).toHaveBeenCalledWith(exampleEngine, {
initialState: {
criterion: {
by: 'example field',
order: 'example order',
},
},
});
const componentError = element.shadowRoot.querySelector(
selectors.componentError
);

expect(componentError).not.toBeNull();
expect(console.error).toHaveBeenCalledTimes(1);
});
});

Expand Down Expand Up @@ -187,7 +368,7 @@ describe('c-quantic-sort', () => {

const sortLabel = element.shadowRoot.querySelector(variant.labelSelector);

expect(sortLabel.value).toBe(expectedSortByLabel);
expect(sortLabel.value).toBe(sortByLabel);
});
});

Expand Down Expand Up @@ -228,7 +409,7 @@ describe('c-quantic-sort', () => {

const sortLabel = element.shadowRoot.querySelector(variant.labelSelector);

expect(sortLabel.textContent).toBe(expectedSortByLabel);
expect(sortLabel.textContent).toBe(sortByLabel);
expect(sortLabel.classList).toContain('slds-text-heading_small');
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {SortObject} from './pageObject';
import {quanticBase} from '../../../../../../playwright/fixtures/baseFixture';
import {SearchObject} from '../../../../../../playwright/page-object/searchObject';
import {
searchRequestRegex,
insightSearchRequestRegex,
} from '../../../../../../playwright/utils/requests';
import {InsightSetupObject} from '../../../../../../playwright/page-object/insightSetupObject';
import {useCaseEnum} from '../../../../../../playwright/utils/useCase';

const sortUrl = 's/quantic-sort';

interface SortOptions {}

type QuanticSortE2EFixtures = {
sort: SortObject;
search: SearchObject;
options: Partial<SortOptions>;
};

type QuanticSortE2ESearchFixtures = QuanticSortE2EFixtures & {
urlHash: string;
};

type QuanticSortE2EInsightFixtures = QuanticSortE2EFixtures & {
insightSetup: InsightSetupObject;
};

export const testSearch = quanticBase.extend<QuanticSortE2ESearchFixtures>({
options: {},
urlHash: '',
search: async ({page}, use) => {
await use(new SearchObject(page, searchRequestRegex));
},
sort: async ({page, options, configuration, search, urlHash}, use) => {
await page.goto(urlHash ? `${sortUrl}#${urlHash}` : sortUrl);
configuration.configure(options);
await search.waitForSearchResponse();
await use(new SortObject(page));
},
});

export const testInsight = quanticBase.extend<QuanticSortE2EInsightFixtures>({
options: {},
search: async ({page}, use) => {
await use(new SearchObject(page, insightSearchRequestRegex));
},
insightSetup: async ({page}, use) => {
await use(new InsightSetupObject(page));
},
sort: async ({page, options, search, configuration, insightSetup}, use) => {
await page.goto(sortUrl);
configuration.configure({...options, useCase: useCaseEnum.insight});
await insightSetup.waitForInsightInterfaceInitialization();
await search.performSearch();
await search.waitForSearchResponse();
await use(new SortObject(page));
},
});

export {expect} from '@playwright/test';
Loading

0 comments on commit 9c846ba

Please sign in to comment.