diff --git a/package-lock.json b/package-lock.json index 63ae54dd3f8..579f22cf3fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25856,9 +25856,10 @@ } }, "node_modules/coveo.analytics": { - "version": "2.30.38", - "resolved": "https://registry.npmjs.org/coveo.analytics/-/coveo.analytics-2.30.38.tgz", - "integrity": "sha512-CxiBWV7XxDNAyCWS7gwikHjJYz8NigYVHSkGU23JkcQf2oK0XJEsxcU/eRN3VRBfLLmhBTRZbpWJ1SW2imDovQ==", + "version": "2.30.39", + "resolved": "https://registry.npmjs.org/coveo.analytics/-/coveo.analytics-2.30.39.tgz", + "integrity": "sha512-Vce17Mq9lwoBY587ZHqLOcKCu0ufymrWpiZ1X0K6NeoDFygcBFz/7vkpn+mnihG/67AnylhGVcloDWkfRLqnEQ==", + "license": "MIT", "dependencies": { "@types/uuid": "^9.0.0", "cross-fetch": "^3.1.5", @@ -53536,11 +53537,11 @@ }, "packages/atomic": { "name": "@coveo/atomic", - "version": "3.2.4", + "version": "3.3.0", "license": "Apache-2.0", "dependencies": { "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@popperjs/core": "^2.11.6", "@salesforce-ux/design-system": "^2.16.1", "@stencil/store": "2.0.16", @@ -53639,7 +53640,7 @@ }, "peerDependencies": { "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0" + "@coveo/headless": "3.3.0" } }, "packages/atomic-angular": { @@ -53654,14 +53655,14 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "3.2.4", + "@coveo/atomic": "3.3.0", "rxjs": "7.8.1" }, "devDependencies": { "@angular-devkit/build-angular": "17.3.9", "@angular/cli": "17.3.9", "@angular/compiler-cli": "17.3.12", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@types/node": "20.14.12", "jasmine-core": "5.2.0", "karma": "6.4.3", @@ -53677,7 +53678,7 @@ "node": "^20.9.0" }, "peerDependencies": { - "@coveo/headless": "3.2.0" + "@coveo/headless": "3.3.0" } }, "packages/atomic-angular/node_modules/jasmine-core": { @@ -53706,10 +53707,10 @@ }, "packages/atomic-angular/projects/atomic-angular": { "name": "@coveo/atomic-angular", - "version": "3.1.4", + "version": "3.1.5", "license": "Apache-2.0", "dependencies": { - "@coveo/atomic": "3.2.4", + "@coveo/atomic": "3.3.0", "tslib": "2.6.3" }, "engines": { @@ -53718,16 +53719,16 @@ "peerDependencies": { "@angular/common": "14 - 17", "@angular/core": "14 - 17", - "@coveo/headless": "3.2.0" + "@coveo/headless": "3.3.0" } }, "packages/atomic-hosted-page": { "name": "@coveo/atomic-hosted-page", - "version": "1.0.5", + "version": "1.0.6", "license": "Apache-2.0", "dependencies": { "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@stencil/core": "4.20.0" }, "devDependencies": { @@ -53809,9 +53810,9 @@ }, "packages/atomic-react": { "name": "@coveo/atomic-react", - "version": "3.1.4", + "version": "3.1.5", "dependencies": { - "@coveo/atomic": "3.2.4" + "@coveo/atomic": "3.3.0" }, "devDependencies": { "@coveo/release": "1.0.0", @@ -53836,7 +53837,7 @@ "node": "^20.9.0" }, "peerDependencies": { - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } @@ -56974,7 +56975,7 @@ }, "packages/headless": { "name": "@coveo/headless", - "version": "3.2.0", + "version": "3.3.0", "license": "Apache-2.0", "dependencies": { "@coveo/bueno": "1.0.1", @@ -56983,7 +56984,7 @@ "@microsoft/fetch-event-source": "2.0.1", "@reduxjs/toolkit": "2.2.7", "abortcontroller-polyfill": "1.7.5", - "coveo.analytics": "2.30.38", + "coveo.analytics": "2.30.39", "dayjs": "1.11.12", "exponential-backoff": "3.1.0", "fast-equals": "5.0.1", @@ -57015,10 +57016,10 @@ }, "packages/headless-react": { "name": "@coveo/headless-react", - "version": "2.0.5", + "version": "2.0.6", "license": "Apache-2.0", "dependencies": { - "@coveo/headless": "3.2.0" + "@coveo/headless": "3.3.0" }, "devDependencies": { "@coveo/release": "1.0.0", @@ -57965,12 +57966,12 @@ }, "packages/quantic": { "name": "@coveo/quantic", - "version": "3.1.0", + "version": "3.2.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "dompurify": "3.1.6", "marked": "12.0.2" }, @@ -59910,7 +59911,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic-angular": "3.1.4", + "@coveo/atomic-angular": "3.1.5", "rxjs": "7.8.1", "tslib": "2.6.3", "zone.js": "0.14.8" @@ -60210,9 +60211,9 @@ "name": "@coveo/atomic-next-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.2.4", - "@coveo/atomic-react": "3.1.4", - "@coveo/headless": "3.2.0", + "@coveo/atomic": "3.3.0", + "@coveo/atomic-react": "3.1.5", + "@coveo/headless": "3.3.0", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" @@ -60275,9 +60276,9 @@ "name": "@coveo/atomic-react-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.2.4", - "@coveo/atomic-react": "3.1.4", - "@coveo/headless": "3.2.0", + "@coveo/atomic": "3.3.0", + "@coveo/atomic-react": "3.1.5", + "@coveo/headless": "3.3.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -60765,7 +60766,7 @@ "name": "@coveo/headless-commerce-react-samples", "version": "0.1.0", "dependencies": { - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -62058,7 +62059,7 @@ "version": "0.0.0", "dependencies": { "@coveo/auth": "2.0.1", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "14.3.1", "@testing-library/user-event": "14.5.2", @@ -64144,8 +64145,8 @@ "name": "@coveo/headless-ssr-samples-common", "version": "0.0.0", "dependencies": { - "@coveo/headless": "3.2.0", - "@coveo/headless-react": "2.0.5", + "@coveo/headless": "3.3.0", + "@coveo/headless-react": "2.0.6", "next": "14.2.5", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -64166,7 +64167,7 @@ "name": "@coveo/headless-ssr-commerce-samples", "version": "0.0.0", "dependencies": { - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "next": "14.2.5", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -64285,10 +64286,10 @@ "version": "0.1.0", "dependencies": { "@babel/standalone": "7.25.0", - "@coveo/atomic": "3.2.4", - "@coveo/atomic-hosted-page": "1.0.5", - "@coveo/atomic-react": "3.1.4", - "@coveo/headless": "3.2.0", + "@coveo/atomic": "3.3.0", + "@coveo/atomic-hosted-page": "1.0.6", + "@coveo/atomic-react": "3.1.5", + "@coveo/headless": "3.3.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -64357,9 +64358,9 @@ "name": "@coveo/atomic-stencil-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.2.4", + "@coveo/atomic": "3.3.0", "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@stencil/core": "4.20.0", "stencil-router-v2": "0.6.0" }, @@ -64642,7 +64643,7 @@ "name": "@coveo/atomic-vuejs-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.2.4", + "@coveo/atomic": "3.3.0", "vue": "^3.4.15" }, "devDependencies": { diff --git a/packages/atomic-angular/package.json b/packages/atomic-angular/package.json index 49d48cac400..702268d9d07 100644 --- a/packages/atomic-angular/package.json +++ b/packages/atomic-angular/package.json @@ -20,17 +20,17 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "3.2.4", + "@coveo/atomic": "3.3.0", "rxjs": "7.8.1" }, "peerDependencies": { - "@coveo/headless": "3.2.0" + "@coveo/headless": "3.3.0" }, "devDependencies": { "@angular-devkit/build-angular": "17.3.9", "@angular/cli": "17.3.9", "@angular/compiler-cli": "17.3.12", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@types/node": "20.14.12", "jasmine-core": "5.2.0", "karma": "6.4.3", diff --git a/packages/atomic-angular/projects/atomic-angular/package.json b/packages/atomic-angular/projects/atomic-angular/package.json index b21ad8d62d6..b38c14e902f 100644 --- a/packages/atomic-angular/projects/atomic-angular/package.json +++ b/packages/atomic-angular/projects/atomic-angular/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/atomic-angular", - "version": "3.1.4", + "version": "3.1.5", "license": "Apache-2.0", "repository": { "url": "https://github.com/coveo/ui-kit" @@ -8,10 +8,10 @@ "peerDependencies": { "@angular/common": "14 - 17", "@angular/core": "14 - 17", - "@coveo/headless": "3.2.0" + "@coveo/headless": "3.3.0" }, "dependencies": { - "@coveo/atomic": "3.2.4", + "@coveo/atomic": "3.3.0", "tslib": "2.6.3" }, "engines": { diff --git a/packages/atomic-hosted-page/package.json b/packages/atomic-hosted-page/package.json index 5a6ff0b76d8..6202f75ca89 100644 --- a/packages/atomic-hosted-page/package.json +++ b/packages/atomic-hosted-page/package.json @@ -1,7 +1,7 @@ { "name": "@coveo/atomic-hosted-page", "description": "Web Component used to inject a Coveo Hosted Search Page in the DOM.", - "version": "1.0.5", + "version": "1.0.6", "repository": { "type": "git", "url": "git+https://github.com/coveo/ui-kit.git", @@ -31,7 +31,7 @@ }, "dependencies": { "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@stencil/core": "4.20.0" }, "devDependencies": { diff --git a/packages/atomic-react/package.json b/packages/atomic-react/package.json index ffb5ed52cbd..6dd5dda9f69 100644 --- a/packages/atomic-react/package.json +++ b/packages/atomic-react/package.json @@ -2,7 +2,7 @@ "name": "@coveo/atomic-react", "sideEffects": false, "type": "module", - "version": "3.1.4", + "version": "3.1.5", "description": "React specific wrapper for the Atomic component library", "repository": { "type": "git", @@ -30,7 +30,7 @@ "commerce/" ], "dependencies": { - "@coveo/atomic": "3.2.4" + "@coveo/atomic": "3.3.0" }, "devDependencies": { "@coveo/release": "1.0.0", @@ -52,7 +52,7 @@ "rollup-plugin-polyfill-node": "^0.13.0" }, "peerDependencies": { - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" }, diff --git a/packages/atomic/CHANGELOG.md b/packages/atomic/CHANGELOG.md index 83aef5d366b..f76a4f5ea5e 100644 --- a/packages/atomic/CHANGELOG.md +++ b/packages/atomic/CHANGELOG.md @@ -1,3 +1,22 @@ +## 3.3.0 (2024-10-10) + +- test(atomic): add tests & stories for atomic-product-image (#4469) ([bc09766](https://github.com/coveo/ui-kit/commits/bc09766)), closes [#4469](https://github.com/coveo/ui-kit/issues/4469) +- test(atomic): add tests for atomic-product-price (#4441) ([09c56b4](https://github.com/coveo/ui-kit/commits/09c56b4)), closes [#4441](https://github.com/coveo/ui-kit/issues/4441) +- test(atomic): fix flaky atomic-external tests (#4515) ([d207ff5](https://github.com/coveo/ui-kit/commits/d207ff5)), closes [#4515](https://github.com/coveo/ui-kit/issues/4515) +- fix(atomic, headless): use `next` as default value for queryCorrectionMode (#4495) ([ddce004](https://github.com/coveo/ui-kit/commits/ddce004)), closes [#4495](https://github.com/coveo/ui-kit/issues/4495) +- fix(quantic,atomic): fixed timezone issue with user actions timeline (#4514) ([1142855](https://github.com/coveo/ui-kit/commits/1142855)), closes [#4514](https://github.com/coveo/ui-kit/issues/4514) +- feat(headless): change citation from custom to click event (#4492) ([1cc5aee](https://github.com/coveo/ui-kit/commits/1cc5aee)), closes [#4492](https://github.com/coveo/ui-kit/issues/4492) +- fix(atomic): atomic-product-image crashes when giving an invalid image-alt-field value (#4482) ([1fb3864](https://github.com/coveo/ui-kit/commits/1fb3864)), closes [#4482](https://github.com/coveo/ui-kit/issues/4482) +- fix(atomic): atomic-product-image fallback image is not using 1:1 ratio (#4483) ([816f529](https://github.com/coveo/ui-kit/commits/816f529)), closes [#4483](https://github.com/coveo/ui-kit/issues/4483) +- fix(atomic): dim unselected facet value (#4410) ([5ba9101](https://github.com/coveo/ui-kit/commits/5ba9101)), closes [#4410](https://github.com/coveo/ui-kit/issues/4410) +- fix(atomic): fix styling issues with atomic tabs (#4498) ([6d17ed7](https://github.com/coveo/ui-kit/commits/6d17ed7)), closes [#4498](https://github.com/coveo/ui-kit/issues/4498) +- fix(atomic): improve error message for atomic-tabs without name (#4497) ([a25bc35](https://github.com/coveo/ui-kit/commits/a25bc35)), closes [#4497](https://github.com/coveo/ui-kit/issues/4497) +- fix(atomic): prevent quickview from reopening when changing tab (#4508) ([35ab282](https://github.com/coveo/ui-kit/commits/35ab282)), closes [#4508](https://github.com/coveo/ui-kit/issues/4508) +- fix(atomic): show atomic-tab-manager dropdown based on available space instead of fixed breakpoint ( ([78a429c](https://github.com/coveo/ui-kit/commits/78a429c)), closes [#4502](https://github.com/coveo/ui-kit/issues/4502) +- fix(genqa): fix rga feedback position with chrome (#4480) ([e2585e4](https://github.com/coveo/ui-kit/commits/e2585e4)), closes [#4480](https://github.com/coveo/ui-kit/issues/4480) +- chore: Add CI healthcheck for CDN builds (#4458) ([904df53](https://github.com/coveo/ui-kit/commits/904df53)), closes [#4458](https://github.com/coveo/ui-kit/issues/4458) +- chore(atomic): fix atomic-tab-manager flaky test (#4507) ([42645c8](https://github.com/coveo/ui-kit/commits/42645c8)), closes [#4507](https://github.com/coveo/ui-kit/issues/4507) + ## 3.2.4 (2024-10-02) - test(atomic): add tests for atomic-commerce-recommendation-list (#4437) ([74dff0d](https://github.com/coveo/ui-kit/commits/74dff0d)), closes [#4437](https://github.com/coveo/ui-kit/issues/4437) diff --git a/packages/atomic/cypress/e2e/did-you-mean-actions.ts b/packages/atomic/cypress/e2e/did-you-mean-actions.ts deleted file mode 100644 index b7bff05e12d..00000000000 --- a/packages/atomic/cypress/e2e/did-you-mean-actions.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {interceptSearchResponse} from '../fixtures/fixture-common'; -import {addTag, TagProps, TestFixture} from '../fixtures/test-fixture'; -import {didYouMeanComponent} from './did-you-mean-selectors'; - -export const addDidYouMean = - (props: TagProps = {}) => - (env: TestFixture) => { - addTag(env, didYouMeanComponent, props); - }; - -export const addDidYouMeanCorrectionToNextQuery = ( - correction: string, - automatic: boolean -) => - interceptSearchResponse((response) => { - response.queryCorrections = [ - {correctedQuery: correction, wordCorrections: []}, - ]; - if (automatic) { - response.results = []; - } - return response; - }, 1); - -export const addQueryTriggerCorrectionToNextQuery = (correction: string) => - interceptSearchResponse((response) => { - response.triggers = [{type: 'query', content: correction}]; - return response; - }, 1); diff --git a/packages/atomic/cypress/e2e/did-you-mean-assertions.ts b/packages/atomic/cypress/e2e/did-you-mean-assertions.ts deleted file mode 100644 index f016fd3ac85..00000000000 --- a/packages/atomic/cypress/e2e/did-you-mean-assertions.ts +++ /dev/null @@ -1,99 +0,0 @@ -import {should} from './common-assertions'; -import {DidYouMeanSelectors} from './did-you-mean-selectors'; - -export function assertDisplayAutoCorrected(display: boolean) { - it(`${should(display)} display auto corrected text`, () => { - DidYouMeanSelectors.noResults().should( - display ? 'be.visible' : 'not.exist' - ); - DidYouMeanSelectors.autoCorrected().should( - display ? 'be.visible' : 'not.exist' - ); - }); -} - -export function assertDisplayQueryTriggered(display: boolean) { - it(`${should(display)} display query triggered text`, () => { - DidYouMeanSelectors.showingResultsFor().should( - display ? 'be.visible' : 'not.exist' - ); - DidYouMeanSelectors.searchInsteadFor().should( - display ? 'be.visible' : 'not.exist' - ); - }); -} - -export function assertDisplayDidYouMeanWithButton(display: boolean) { - it(`${should( - display - )} display "did you mean" text with a manual correction button`, () => { - DidYouMeanSelectors.didYouMean().should( - display ? 'be.visible' : 'not.exist' - ); - DidYouMeanSelectors.correctionButton().should( - display ? 'be.visible' : 'not.exist' - ); - }); -} - -export function assertDisplayUndoButton(display: boolean) { - it(`${should(display)} display a undo button`, () => { - DidYouMeanSelectors.undoButton().should( - display ? 'be.visible' : 'not.exist' - ); - }); -} - -export function assertLogDidYouMeanClick() { - it('should log didyoumeanClick to UA', () => { - cy.expectSearchEvent('didyoumeanClick'); - }); -} - -export function assertLogQueryTriggerUndo(undoneQuery: string) { - it('should log the undoQuery to UA', () => { - cy.expectSearchEvent('undoQuery').then((payload) => - expect(payload.customData).to.have.property('undoneQuery', undoneQuery) - ); - }); -} - -export function assertHasAutoCorrectOriginalQuery(originalQuery: string) { - it(`should display the original query ("${originalQuery}")`, () => { - DidYouMeanSelectors.noResultsOriginalQuery().should(($el) => - expect($el.text()).to.equal(originalQuery) - ); - }); -} - -export function assertHasAutoCorrectNewQuery(newQuery: string) { - it(`should display the correction of an automatically corrected query ("${newQuery}")`, () => { - DidYouMeanSelectors.autoCorrectedNewQuery().should(($el) => - expect($el.text()).to.equal(newQuery) - ); - }); -} - -export function assertHasManualCorrectNewQuery(newQuery: string) { - it(`should display a manual correction button with the new query ("${newQuery}")`, () => { - DidYouMeanSelectors.correctionButton().should(($el) => - expect($el.text()).to.equal(newQuery) - ); - }); -} - -export function assertHasTriggerOriginalQuery(originalQuery: string) { - it(`should display an undo button with the original query ("${originalQuery}")`, () => { - DidYouMeanSelectors.undoButton().should(($el) => - expect($el.text()).to.equal(originalQuery) - ); - }); -} - -export function assertHasTriggerNewQuery(newQuery: string) { - it(`should display the correction of a query trigger ("${newQuery}")`, () => { - DidYouMeanSelectors.showingResultsForNewQuery().should(($el) => - expect($el.text()).to.equal(newQuery) - ); - }); -} diff --git a/packages/atomic/cypress/e2e/did-you-mean-selectors.ts b/packages/atomic/cypress/e2e/did-you-mean-selectors.ts deleted file mode 100644 index 4fd93aeabf1..00000000000 --- a/packages/atomic/cypress/e2e/did-you-mean-selectors.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const didYouMeanComponent = 'atomic-did-you-mean'; -export const DidYouMeanSelectors = { - shadow: () => cy.get(didYouMeanComponent).shadow(), - noResults: () => DidYouMeanSelectors.shadow().find('[part="no-results"]'), - noResultsOriginalQuery: () => - DidYouMeanSelectors.noResults().find('[part~="highlight"]'), - autoCorrected: () => - DidYouMeanSelectors.shadow().find('[part="auto-corrected"]'), - autoCorrectedNewQuery: () => - DidYouMeanSelectors.autoCorrected().find('[part~="highlight"]'), - showingResultsFor: () => - DidYouMeanSelectors.shadow().find('[part="showing-results-for"]'), - showingResultsForNewQuery: () => - DidYouMeanSelectors.showingResultsFor().find('[part="highlight"]'), - searchInsteadFor: () => - DidYouMeanSelectors.shadow().find('[part="search-instead-for"]'), - didYouMean: () => DidYouMeanSelectors.shadow().find('[part="did-you-mean"]'), - correctionButton: () => - DidYouMeanSelectors.shadow().find('[part="correction-btn"]'), - undoButton: () => DidYouMeanSelectors.shadow().find('[part="undo-btn"]'), -}; diff --git a/packages/atomic/cypress/e2e/did-you-mean.cypress.ts b/packages/atomic/cypress/e2e/did-you-mean.cypress.ts deleted file mode 100644 index 9020a340465..00000000000 --- a/packages/atomic/cypress/e2e/did-you-mean.cypress.ts +++ /dev/null @@ -1,119 +0,0 @@ -import {TestFixture} from '../fixtures/test-fixture'; -import { - addDidYouMean, - addDidYouMeanCorrectionToNextQuery, - addQueryTriggerCorrectionToNextQuery, -} from './did-you-mean-actions'; -import * as DidYouMeanAssertions from './did-you-mean-assertions'; -import {DidYouMeanSelectors} from './did-you-mean-selectors'; -import {addQuerySummary} from './query-summary-actions'; -import * as QuerySummaryAssertions from './query-summary-assertions'; -import {addSearchBox} from './search-box/search-box-actions'; -import {SearchBoxSelectors} from './search-box/search-box-selectors'; - -describe('Did You Mean Test Suites', () => { - const originalQuery = 'test'; - const newQuery = 'shrimp'; - - function commonSetup(env: TestFixture) { - env.with(addSearchBox()).with(addQuerySummary()).with(addDidYouMean()); - } - - function search() { - SearchBoxSelectors.textArea().type(`${originalQuery}{enter}`, { - force: true, - delay: 200, - }); - } - - describe('with an automatic query correction', () => { - beforeEach(() => { - new TestFixture().with(commonSetup).init(); - addDidYouMeanCorrectionToNextQuery(newQuery, true); - search(); - }); - - QuerySummaryAssertions.assertHasQuery(newQuery); - DidYouMeanAssertions.assertHasAutoCorrectOriginalQuery(originalQuery); - DidYouMeanAssertions.assertHasAutoCorrectNewQuery(newQuery); - DidYouMeanAssertions.assertDisplayAutoCorrected(true); - DidYouMeanAssertions.assertDisplayDidYouMeanWithButton(false); - DidYouMeanAssertions.assertDisplayQueryTriggered(false); - DidYouMeanAssertions.assertDisplayUndoButton(false); - }); - - describe('with a manual query correction', () => { - beforeEach(() => { - new TestFixture().with(commonSetup).init(); - addDidYouMeanCorrectionToNextQuery(newQuery, false); - search(); - }); - - QuerySummaryAssertions.assertHasQuery(originalQuery); - DidYouMeanAssertions.assertHasManualCorrectNewQuery(newQuery); - DidYouMeanAssertions.assertDisplayAutoCorrected(false); - DidYouMeanAssertions.assertDisplayDidYouMeanWithButton(true); - DidYouMeanAssertions.assertDisplayQueryTriggered(false); - DidYouMeanAssertions.assertDisplayUndoButton(false); - - describe('after pressing on the correction button', () => { - beforeEach(() => { - DidYouMeanSelectors.correctionButton().click(); - }); - QuerySummaryAssertions.assertHasQuery(newQuery); - DidYouMeanAssertions.assertDisplayAutoCorrected(false); - DidYouMeanAssertions.assertDisplayDidYouMeanWithButton(false); - DidYouMeanAssertions.assertDisplayQueryTriggered(false); - DidYouMeanAssertions.assertDisplayUndoButton(false); - }); - }); - - describe('after correcting a query', () => { - beforeEach(() => { - new TestFixture().with(commonSetup).init(); - addDidYouMeanCorrectionToNextQuery(newQuery, false); - search(); - DidYouMeanSelectors.correctionButton().click(); - }); - - DidYouMeanAssertions.assertLogDidYouMeanClick(); - }); - - describe('with a query trigger', () => { - beforeEach(() => { - new TestFixture().with(commonSetup).init(); - addQueryTriggerCorrectionToNextQuery(newQuery); - search(); - }); - - QuerySummaryAssertions.assertHasQuery(newQuery); - DidYouMeanAssertions.assertHasTriggerOriginalQuery(originalQuery); - DidYouMeanAssertions.assertHasTriggerNewQuery(newQuery); - DidYouMeanAssertions.assertDisplayDidYouMeanWithButton(false); - DidYouMeanAssertions.assertDisplayQueryTriggered(true); - DidYouMeanAssertions.assertDisplayUndoButton(true); - - describe('after pressing on the undo button', () => { - beforeEach(() => { - DidYouMeanSelectors.undoButton().click(); - }); - - QuerySummaryAssertions.assertHasQuery(originalQuery); - DidYouMeanAssertions.assertDisplayAutoCorrected(false); - DidYouMeanAssertions.assertDisplayDidYouMeanWithButton(false); - DidYouMeanAssertions.assertDisplayQueryTriggered(false); - DidYouMeanAssertions.assertDisplayUndoButton(false); - }); - }); - - describe('after undoing a query', () => { - beforeEach(() => { - new TestFixture().with(commonSetup).init(); - addQueryTriggerCorrectionToNextQuery(newQuery); - search(); - DidYouMeanSelectors.undoButton().click(); - }); - - DidYouMeanAssertions.assertLogQueryTriggerUndo(newQuery); - }); -}); diff --git a/packages/atomic/cypress/e2e/generated-answer-assertions.ts b/packages/atomic/cypress/e2e/generated-answer-assertions.ts index 138ebca1fe0..f003b684b35 100644 --- a/packages/atomic/cypress/e2e/generated-answer-assertions.ts +++ b/packages/atomic/cypress/e2e/generated-answer-assertions.ts @@ -2,7 +2,7 @@ import {should} from './common-assertions'; import {GeneratedAnswerSelectors} from './generated-answer-selectors'; export function assertLogOpenGeneratedAnswerSource() { - cy.expectCustomEvent('generatedAnswer', 'openGeneratedAnswerSource'); + cy.expectClickEvent('generatedAnswerCitationClick'); } export function assertLogGeneratedAnswerSourceHover() { diff --git a/packages/atomic/package.json b/packages/atomic/package.json index e58e9247795..8a447ac36b5 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -1,7 +1,7 @@ { "name": "@coveo/atomic", "type": "module", - "version": "3.2.4", + "version": "3.3.0", "description": "A web-component library for building modern UIs interfacing with the Coveo platform", "homepage": "https://docs.coveo.com/en/atomic/latest/", "repository": { @@ -67,7 +67,7 @@ }, "dependencies": { "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0", + "@coveo/headless": "3.3.0", "@popperjs/core": "^2.11.6", "@salesforce-ux/design-system": "^2.16.1", "@stencil/store": "2.0.16", @@ -163,7 +163,7 @@ }, "peerDependencies": { "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.2.0" + "@coveo/headless": "3.3.0" }, "license": "Apache-2.0", "engines": { diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index 1ed53e7e6ed..bd0673dc8f4 100644 --- a/packages/atomic/src/components.d.ts +++ b/packages/atomic/src/components.d.ts @@ -805,7 +805,7 @@ export namespace Components { */ "automaticallyCorrectQuery": boolean; /** - * Define which query correction system to use `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. Default value is `legacy`. In the next major version of Atomic, the default value will be `next`. + * Define which query correction system to use `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. Default value is `next`. */ "queryCorrectionMode": 'legacy' | 'next'; } @@ -2051,6 +2051,7 @@ export namespace Components { "field": string; /** * The product field that contains the alt text for the images. This will look for the field in the product object first, then in the product.additionalFields object. The field can be a string or an array of strings. If the value of the field is a string, it will be used as the alt text for all the images. If the value of the field is an array of strings, the alt text will be used in the order of the images. If the field is not specified, or does not contain a valid value, the alt text will be set to "Image {index} out of {totalImages} for {productName}". + * @type {string} */ "imageAltField"?: string; /** @@ -6731,7 +6732,7 @@ declare namespace LocalJSX { */ "automaticallyCorrectQuery"?: boolean; /** - * Define which query correction system to use `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. Default value is `legacy`. In the next major version of Atomic, the default value will be `next`. + * Define which query correction system to use `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. Default value is `next`. */ "queryCorrectionMode"?: 'legacy' | 'next'; } @@ -7935,6 +7936,7 @@ declare namespace LocalJSX { "field"?: string; /** * The product field that contains the alt text for the images. This will look for the field in the product object first, then in the product.additionalFields object. The field can be a string or an array of strings. If the value of the field is a string, it will be used as the alt text for all the images. If the value of the field is an array of strings, the alt text will be used in the order of the images. If the field is not specified, or does not contain a valid value, the alt text will be set to "Image {index} out of {totalImages} for {productName}". + * @type {string} */ "imageAltField"?: string; } diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx new file mode 100644 index 00000000000..260294c6f75 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx @@ -0,0 +1,70 @@ +import {wrapInCommerceInterface} from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; +import {wrapInCommerceProductList} from '@coveo/atomic-storybook-utils/commerce/commerce-product-list-wrapper'; +import {wrapInProductTemplate} from '@coveo/atomic-storybook-utils/commerce/commerce-product-template-wrapper'; +import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; +import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; +import type {Meta, StoryObj as Story} from '@storybook/web-components'; +import type {Decorator} from '@storybook/web-components'; +import {html} from 'lit-html'; + +const styledDivDecorator: Decorator = (story) => { + return html`
${story()}
`; +}; + +const { + decorator: commerceInterfaceDecorator, + play: initializeCommerceInterface, +} = wrapInCommerceInterface({ + skipFirstSearch: false, + type: 'product-listing', + engineConfig: { + context: { + view: { + url: 'https://ui-kit.coveo/atomic/storybook/atomic-product-image', + }, + language: 'en', + country: 'US', + currency: 'USD', + }, + }, +}); +const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(); +const {decorator: productTemplateDecorator} = wrapInProductTemplate(); + +const meta: Meta = { + component: 'atomic-product-image', + title: 'Atomic-Commerce/Product Template Components/ProductImage', + id: 'atomic-product-image', + render: renderComponent, + decorators: [ + productTemplateDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + styledDivDecorator, + ], + parameters, + play: initializeCommerceInterface, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-product-image', +}; + +export const withAFallbackImage: Story = { + name: 'With a fallback image', + args: { + 'attributes-field': 'invalid', + 'attributes-fallback': 'https://sports.barca.group/logos/barca.svg', + }, +}; + +export const withAnAltTextField: Story = { + name: 'With an alt text field', + args: { + 'attributes-field': 'invalid', + 'attributes-fallback': 'invalid', + 'attributes-image-alt-field': 'ec_name', + }, +}; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx index 94be915b12f..f099ac61e61 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx @@ -1,3 +1,4 @@ +import {isNullOrUndefined} from '@coveo/bueno'; import {Product, ProductTemplatesHelpers} from '@coveo/headless/commerce'; import {Component, h, Prop, Element, State, Method} from '@stencil/core'; import { @@ -47,6 +48,7 @@ export class AtomicProductImage implements InitializableComponent { * If the value of the field is an array of strings, the alt text will be used in the order of the images. * * If the field is not specified, or does not contain a valid value, the alt text will be set to "Image {index} out of {totalImages} for {productName}". + * @type {string} */ @Prop({reflect: true}) imageAltField?: string; @@ -171,6 +173,10 @@ export class AtomicProductImage implements InitializableComponent { this.imageAltField ); + if (isNullOrUndefined(value)) { + return null; + } + if (Array.isArray(value)) { return value.map((v) => `${v}`.trim()); } @@ -208,9 +214,10 @@ export class AtomicProductImage implements InitializableComponent { }); if (this.images.length === 0) { this.validateUrl(this.fallback); - return ( {this.bindings.i18n.t('image-not-found-alt')} { } return ( - // TODO: handle small/icon image sizes better on mobile + // TODO - KIT-3612 : handle small/icon image sizes better on mobile { -// test.describe('when clicking on the next button', async ({productImage}) => { -// test.fixme('should navigate to the next image', () => {}); -// test.fixme('should not open the product', () => {}); -// }); -// test.describe('when clicking on the previous button', async ({productImage}) => { -// test.fixme('should navigate to the previous image', () => {}); -// test.fixme('should not open the product', () => {}); -// }); -// }); +/* eslint-disable @cspell/spellchecker */ +import {test, expect} from './fixture'; + +test.describe('default', async () => { + test.beforeEach(async ({productImage}) => { + await productImage.load(); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should render the image', async ({productImage}) => { + expect(productImage.noCarouselImage).toBeVisible(); + }); + + test('should have a default alt text', async ({productImage}) => { + const altText = await productImage.noCarouselImage.getAttribute('alt'); + expect(altText).toEqual('Image 1 out of 1 for Nublu Water Bottle'); + }); + + test('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); +}); + +test.describe('with a custom fallback image', async () => { + const FALLBACK = 'https://sports.barca.group/logos/barca.svg'; + + test.describe('when the product image is missing', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomThumbnails([]); + await productImage.load({story: 'with-a-fallback-image'}); + await productImage.noCarouselImage.waitFor(); + }); + + test('should render the fallback image', async ({productImage}) => { + const src = await productImage.noCarouselImage.getAttribute('src'); + expect(src).toContain(FALLBACK); + }); + + test('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); + }); + + test.describe('when the product image is invalid', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomThumbnails(['invalid-image']); + await productImage.load({story: 'with-a-fallback-image'}); + }); + + test('should render the fallback image', async ({productImage}) => { + const src = await productImage.noCarouselImage.getAttribute('src'); + expect(src).toContain(FALLBACK); + }); + + test('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); + }); + + test.describe('when the product image is not a string', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomThumbnails([1]); + await productImage.load({story: 'with-a-fallback-image'}); + }); + + test('should render the fallback image', async ({productImage}) => { + const src = await productImage.noCarouselImage.getAttribute('src'); + expect(src).toContain(FALLBACK); + }); + + test('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); + }); +}); + +test.describe('with an alt text field', async () => { + test.describe('when imageAltField is a valid string', () => { + const NO_CAROUSEL_CUSTOM_FIELD = 'Nublu Water Bottle'; + const CAROUSEL_CUSTOM_FIELD = 'Blue Lagoon Mat'; + + test.beforeEach(async ({productImage}) => { + await productImage.withCustomField( + 'Nublu Water Bottle', + 'Blue Lagoon Mat' + ); + await productImage.load({ + story: 'with-an-alt-text-field', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_field', + }, + }); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should use the same alt text for all images', async ({ + productImage, + }) => { + const altNoCarousel = + await productImage.noCarouselImage.getAttribute('alt'); + expect(altNoCarousel).toEqual(NO_CAROUSEL_CUSTOM_FIELD); + + const altCarousel = await productImage.carouselImage.getAttribute('alt'); + expect(altCarousel).toEqual(CAROUSEL_CUSTOM_FIELD); + }); + }); + + test.describe('when imageAltField is an array of valid strings', () => { + const NO_CAROUSEL_CUSTOM_FIELDS = [ + 'FIRST Nublu Water Bottle', + 'SECOND Nublu Water Bottle 2', + ]; + const CAROUSEL_CUSTOM_FIELDS = [ + 'FIRST Blue Lagoon Mat', + 'SECOND Blue Lagoon Mat', + ]; + + test.beforeEach(async ({productImage}) => { + await productImage.withCustomField( + NO_CAROUSEL_CUSTOM_FIELDS, + CAROUSEL_CUSTOM_FIELDS + ); + await productImage.load({ + story: 'with-an-alt-text-field', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_field', + }, + }); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should correctly assign alt text for the first image', async ({ + productImage, + }) => { + const noCarouselAlt = + await productImage.noCarouselImage.getAttribute('alt'); + expect(noCarouselAlt).toContain(NO_CAROUSEL_CUSTOM_FIELDS[0]); + + const carouselAlt = await productImage.carouselImage.getAttribute('alt'); + expect(carouselAlt).toContain(CAROUSEL_CUSTOM_FIELDS[0]); + }); + + test('should correctly assign alt text for the last image', async ({ + productImage, + }) => { + await productImage.nextButton.click(); + + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain(CAROUSEL_CUSTOM_FIELDS[1]); + }); + }); + + test.describe('when imageAltField is not specified', () => { + test.beforeEach(async ({productImage}) => { + await productImage.load(); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should generate default alt text for all images', async ({ + productImage, + }) => { + expect(await productImage.noCarouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 1 for Nublu Water Bottle' + ); + expect(await productImage.carouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 2 for Blue Lagoon Mat' + ); + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain('Image 2 out of 2 for Blue Lagoon Mat'); + }); + }); + + test.describe('when imageAltField is invalid', () => { + test.beforeEach(async ({productImage}) => { + await productImage.load({ + story: 'with-an-alt-text-field', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_field', + }, + }); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should use the default alt text for all images', async ({ + productImage, + page, + }) => { + await page.waitForTimeout(10000); + expect(await productImage.noCarouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 1 for Nublu Water Bottle' + ); + expect(await productImage.carouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 2 for Blue Lagoon Mat' + ); + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain('Image 2 out of 2 for Blue Lagoon Mat'); + }); + }); + + test.describe('when imageAltField is an empty array', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomField([], []); + await productImage.load({ + story: 'with-an-alt-text-field', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_field', + }, + }); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should use the default alt text for all images', async ({ + productImage, + }) => { + expect(await productImage.noCarouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 1 for Nublu Water Bottle' + ); + expect(await productImage.carouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 2 for Blue Lagoon Mat' + ); + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain('Image 2 out of 2 for Blue Lagoon Mat'); + }); + }); +}); + +test.describe('as a carousel', async () => { + const URL = + 'http://localhost:4400/iframe.html?id=atomic-product-image--default&viewMode=story#sortCriteria=relevance'; + const FIRST_IMAGE = + 'https://images.barca.group/Sports/mj/Trampolines%20%26%20Floats/Huge%20inflatable%20mats/3_Blue/df1a99488425_bottom_right.webp'; + const SECOND_IMAGE = + 'https://images.barca.group/Sports/mj/Trampolines%20%26%20Floats/Huge%20inflatable%20mats/3_Blue/df1a99488425_bottom_left.webp'; + + test.beforeEach(async ({productImage}) => { + await productImage.load(); + await productImage.carouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should render the first image by default', async ({productImage}) => { + await expect(productImage.carouselImage).toBeVisible(); + const src = await productImage.carouselImage.getAttribute('src'); + expect(src).toContain(FIRST_IMAGE); + }); + + test.describe('when clicking the next button', () => { + test.beforeEach(async ({productImage}) => { + await productImage.nextButton.click(); + }); + + test('should navigate to the next image', async ({productImage}) => { + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(SECOND_IMAGE); + }); + + test('should navigate to the first image if the last image is reached', async ({ + productImage, + }) => { + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('src'); + }) + .toContain(FIRST_IMAGE); + }); + + test('should not open the product', async ({page}) => { + expect(page.url()).toEqual(URL); + }); + }); + + test.describe('when clicking the previous button', () => { + test.beforeEach(async ({productImage}) => { + await productImage.previousButton.click(); + }); + + test('should navigate to the last image if the first image is reached', async ({ + productImage, + }) => { + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(SECOND_IMAGE); + }); + + test('should navigate to the previous image', async ({productImage}) => { + await productImage.previousButton.click(); + + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(FIRST_IMAGE); + }); + + test('should not open the product', async ({page}) => { + expect(page.url()).toEqual(URL); + }); + }); + + test.describe('when clicking the indicator dot', () => { + test('should navigate to the corresponding image', async ({ + productImage, + }) => { + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(FIRST_IMAGE); + + await productImage.indicatorDot.click(); + + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(SECOND_IMAGE); + }); + }); +}); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts index ca73e2a7976..7d1d031d2e3 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts @@ -15,4 +15,5 @@ export const test = base.extend({ await use(new ProductImageObject(page)); }, }); + export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts index 30a89aebb3a..063c75d6c6b 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts @@ -5,5 +5,57 @@ export class ProductImageObject extends BasePageObject<'atomic-product-image'> { constructor(page: Page) { super(page, 'atomic-product-image'); } - // TODO tests + + get noCarouselImage() { + return this.page.getByRole('img').nth(0); + } + + get carouselImage() { + return this.page.getByRole('img').nth(1); + } + + get nextButton() { + return this.page.getByRole('button', {name: 'Next'}); + } + + get previousButton() { + return this.page.getByRole('button', {name: 'Previous'}); + } + + get indicatorDot() { + return this.page.getByRole('listitem').nth(1); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async withCustomThumbnails(thumbnails: any[]) { + await this.page.route('**/commerce/v2/listing', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.products[0].ec_thumbnails = thumbnails; + + await route.fulfill({ + response, + json: body, + }); + }); + return this; + } + + async withCustomField( + fieldNoCarousel: string | string[], + fieldCarousel: string | string[] + ) { + await this.page.route('**/commerce/v2/listing', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.products[0].custom_alt_field = fieldNoCarousel; + body.products[1].custom_alt_field = fieldCarousel; + + await route.fulfill({ + response, + json: body, + }); + }); + return this; + } } diff --git a/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx b/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx index 2387dbd9f44..12e6b428704 100644 --- a/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx +++ b/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx @@ -74,7 +74,6 @@ export class AtomicModal implements InitializableComponent { if (isOpen) { this.wasEverOpened = true; - //TODO: remove the addition of a class to the body in atomicV3 document.body.classList.add(modalOpenedClass); this.bindings.interfaceElement.classList.add(modalOpenedClass); await this.waitForAnimationEnded(); @@ -83,7 +82,6 @@ export class AtomicModal implements InitializableComponent { } this.focusTrap!.active = true; } else { - //TODO: remove the removal of a class to the body in atomicV3 document.body.classList.remove(modalOpenedClass); this.bindings.interfaceElement.classList.remove(modalOpenedClass); if (isIOS()) { diff --git a/packages/atomic/src/components/common/tab-manager/tab-dropdown-option.tsx b/packages/atomic/src/components/common/tab-manager/tab-dropdown-option.tsx index ff82ed4fccd..c42f8d6b151 100644 --- a/packages/atomic/src/components/common/tab-manager/tab-dropdown-option.tsx +++ b/packages/atomic/src/components/common/tab-manager/tab-dropdown-option.tsx @@ -10,7 +10,12 @@ export const TabDropdownOption: FunctionalComponent = ( props ) => { return ( - ); diff --git a/packages/atomic/src/components/common/tab-manager/tab-dropdown.tsx b/packages/atomic/src/components/common/tab-manager/tab-dropdown.tsx index 1ff2e970e38..a87837d0fcd 100644 --- a/packages/atomic/src/components/common/tab-manager/tab-dropdown.tsx +++ b/packages/atomic/src/components/common/tab-manager/tab-dropdown.tsx @@ -1,9 +1,11 @@ import {FunctionalComponent, h} from '@stencil/core'; +import TabsIcon from '../../../images/arrow-bottom-rounded.svg'; export interface TabDropdownProps { tabs: Array<{name: string; label: string}>; activeTab: string; onTabChange: (tabName: string) => void; + class?: string; } export const TabDropdown: FunctionalComponent = ( @@ -12,18 +14,29 @@ export const TabDropdown: FunctionalComponent = ( ) => { return ( ); }; diff --git a/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx b/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx index 8728de6a37d..a5f31504b8b 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx @@ -49,12 +49,10 @@ export class AtomicIPXModal implements InitializableComponent { const modalOpenedClass = 'atomic-ipx-modal-opened'; if (isOpen) { - //TODO: remove the addition of a class to the body in atomicV3 document.body.classList.add(modalOpenedClass); this.bindings.interfaceElement.classList.add(modalOpenedClass); return; } - //TODO: remove the removal of a class to the body in atomicV3 document.body.classList.remove(modalOpenedClass); this.bindings.interfaceElement.classList.remove(modalOpenedClass); } diff --git a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx index 1f7e15b98be..25366bc908c 100644 --- a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx +++ b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx @@ -1,30 +1,18 @@ +/* eslint-disable @cspell/spellchecker */ import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; import {wrapInSearchInterface} from '@coveo/atomic-storybook-utils/search/search-interface-wrapper'; -import type {Meta, StoryObj as Story} from '@storybook/web-components'; - -const {decorator, play} = wrapInSearchInterface({ - search: { - preprocessSearchResponseMiddleware: (res) => { - res.body.results = []; - res.body.queryCorrections = [ - { - correctedQuery: 'test', - wordCorrections: [ - { - offset: 0, - length: 4, - // eslint-disable-next-line @cspell/spellchecker - originalWord: 'tset', - correctedWord: 'test', - }, - ], - }, - ]; - return res; - }, - }, -}); +import {userEvent} from '@storybook/test'; +import type { + Decorator, + Meta, + StoryObj as Story, + StoryContext, +} from '@storybook/web-components'; +import {html} from 'lit/static-html.js'; +import {within} from 'shadow-dom-testing-library'; + +const {decorator, play} = wrapInSearchInterface(); const meta: Meta = { title: 'Atomic/DidYouMean', @@ -38,6 +26,55 @@ const meta: Meta = { export default meta; +const searchBoxDecorator: Decorator = (story) => html` +
+ +
+ ${story()} +`; + +const searchPlay: ( + context: StoryContext, + query: string +) => Promise = async (context, query) => { + await play(context); + const {canvasElement, step} = context; + const canvas = within(canvasElement); + + const searchBox = ( + await canvas.findAllByShadowTitle('Search field with suggestions.', { + exact: false, + }) + )?.find( + (el) => el.getAttribute('part') === 'textarea' + ) as HTMLTextAreaElement; + + const submitButton = ( + await canvas.findAllByShadowTitle('Search field with suggestions.', { + exact: false, + }) + )?.find((el) => el.getAttribute('part') === 'submit-button'); + + await step(`Search "${query}"`, async () => { + searchBox!.value = ''; + + await userEvent.type(searchBox!, query); + await userEvent.click(submitButton!); + }); +}; + export const Default: Story = { name: 'atomic-did-you-mean', + decorators: [searchBoxDecorator], + play: (context) => searchPlay(context, 'coveoo'), +}; + +export const ManualCorrection: Story = { + decorators: [searchBoxDecorator], + play: (context) => searchPlay(context, 'ceveo'), +}; + +export const QueryTrigger: Story = { + decorators: [searchBoxDecorator], + play: (context) => searchPlay(context, 'Japan'), }; diff --git a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx index fb7ec813e38..565a7cb04be 100644 --- a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx +++ b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx @@ -57,17 +57,16 @@ export class AtomicDidYouMean implements InitializableComponent { */ @Prop({reflect: true}) public automaticallyCorrectQuery = true; - // TODO: V3: Default to `next` /** * Define which query correction system to use * * `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. * `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. * - * Default value is `legacy`. In the next major version of Atomic, the default value will be `next`. + * Default value is `next`. */ @Prop({reflect: true}) - public queryCorrectionMode: 'legacy' | 'next' = 'legacy'; + public queryCorrectionMode: 'legacy' | 'next' = 'next'; @Watch('queryCorrectionMode') public updateQueryCorrectionMode() { diff --git a/packages/atomic/src/components/search/atomic-did-you-mean/e2e/atomic-did-you-mean.e2e.ts b/packages/atomic/src/components/search/atomic-did-you-mean/e2e/atomic-did-you-mean.e2e.ts new file mode 100644 index 00000000000..4f5ba1b91d1 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-did-you-mean/e2e/atomic-did-you-mean.e2e.ts @@ -0,0 +1,99 @@ +/* eslint-disable @cspell/spellchecker */ +import {test, expect} from './fixture'; + +test.describe('with an automatic query correction', () => { + const ORIGINAL_QUERY = 'coveoo'; + const CORRECTED_QUERY = 'coveo'; + test.beforeEach(async ({didYouMean}) => { + await didYouMean.load(); + await didYouMean.hydrated.waitFor(); + }); + + test('search box should contain the corrected query', async ({searchBox}) => { + await expect(searchBox.searchInput).toHaveValue(CORRECTED_QUERY); + }); + + test('should display the original query', async ({page}) => { + await expect(page.getByText("We couldn't find anything for")).toBeVisible(); + await expect(page.getByText(ORIGINAL_QUERY)).toBeVisible(); + }); + + test('should display the auto corrected', async ({page}) => { + await expect( + page.getByText('Query was automatically corrected to') + ).toBeVisible(); + await expect(page.getByText(CORRECTED_QUERY, {exact: true})).toBeVisible(); + }); +}); + +test.describe('with a manual query correction', () => { + const ORIGINAL_QUERY = 'ceveo'; + const CORRECTED_QUERY = 'coveo'; + test.beforeEach(async ({didYouMean}) => { + await didYouMean.load({story: 'manual-correction'}); + await didYouMean.hydrated.waitFor(); + }); + + test('search box should contain the original query', async ({searchBox}) => { + await expect(searchBox.searchInput).toHaveValue(ORIGINAL_QUERY); + }); + + test('should show did you mean text', async ({page}) => { + await expect( + page.getByText(`Did you mean: ${CORRECTED_QUERY}`) + ).toBeVisible(); + }); + + test('should show manual correction button', async ({page}) => { + await expect( + page.getByRole('button', {name: CORRECTED_QUERY}) + ).toBeVisible(); + }); + + test('when clicking on the manual correction button, the search box should contain the corrected query', async ({ + searchBox, + page, + }) => { + await page.getByRole('button', {name: CORRECTED_QUERY}).click(); + await expect(searchBox.searchInput).toHaveValue(CORRECTED_QUERY); + }); +}); + +test.describe('with a query trigger', () => { + const ORIGINAL_QUERY = 'Japan'; + const TRIGGER_QUERY = 'China'; + test.beforeEach(async ({didYouMean}) => { + await didYouMean.load({story: 'query-trigger'}); + await didYouMean.hydrated.waitFor(); + }); + + test('search box should contain the corrected query', async ({searchBox}) => { + await expect(searchBox.searchInput).toHaveValue(TRIGGER_QUERY); + }); + + test('should show trigger correction text', async ({page}) => { + await expect( + page.getByText(`Showing results for ${TRIGGER_QUERY}`) + ).toBeVisible(); + }); + + test('should show manual trigger correction text', async ({page}) => { + await expect( + page.getByText(`Search instead for ${ORIGINAL_QUERY}`) + ).toBeVisible(); + }); + + test('should show undo button', async ({page}) => { + await expect( + page.getByRole('button', {name: ORIGINAL_QUERY}) + ).toBeVisible(); + }); + + test('when clicking on the undo button, the search box should contain the original query', async ({ + searchBox, + page, + }) => { + await page.getByRole('button', {name: ORIGINAL_QUERY}).click(); + await expect(searchBox.searchInput).toHaveValue(ORIGINAL_QUERY); + }); +}); diff --git a/packages/atomic/src/components/search/atomic-did-you-mean/e2e/fixture.ts b/packages/atomic/src/components/search/atomic-did-you-mean/e2e/fixture.ts new file mode 100644 index 00000000000..c464c43f551 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-did-you-mean/e2e/fixture.ts @@ -0,0 +1,22 @@ +import {test as base} from '@playwright/test'; +import { + AxeFixture, + makeAxeBuilder, +} from '../../../../../playwright-utils/base-fixture'; +import {SearchBoxPageObject} from '../../atomic-search-box/e2e/page-object'; +import {DidYouMeanPageObject} from './page-object'; + +interface TestFixture { + didYouMean: DidYouMeanPageObject; + searchBox: SearchBoxPageObject; +} +export const test = base.extend({ + makeAxeBuilder, + didYouMean: async ({page}, use) => { + await use(new DidYouMeanPageObject(page)); + }, + searchBox: async ({page}, use) => { + await use(new SearchBoxPageObject(page)); + }, +}); +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/search/atomic-did-you-mean/e2e/page-object.ts b/packages/atomic/src/components/search/atomic-did-you-mean/e2e/page-object.ts new file mode 100644 index 00000000000..dd8df2c0b21 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-did-you-mean/e2e/page-object.ts @@ -0,0 +1,8 @@ +import {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../playwright-utils/base-page-object'; + +export class DidYouMeanPageObject extends BasePageObject<'atomic-did-you-mean'> { + constructor(page: Page) { + super(page, 'atomic-did-you-mean'); + } +} diff --git a/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts b/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts index 526428e14ec..a7776c4665f 100644 --- a/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts +++ b/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts @@ -1,39 +1,44 @@ import {test, expect} from './fixture'; -test.describe('External Test Suite', () => { - test.describe('when modifying state of a component (search box) that is a child of an atomic-external component', () => { - test.beforeEach(async ({external}) => { - await external.load(); +test.describe('when modifying state of a component (search box) that is a child of an atomic-external component', () => { + test.beforeEach(async ({external, page}) => { + await external.load(); + await external.hydrated.waitFor(); - await external.searchBox - .locator('[part="textarea"]') - .fill('hello', {timeout: 1000}); + await page + .locator('atomic-external') + .getByLabel('Search field with suggestions') + .waitFor({state: 'visible', timeout: 5000}); - await external.searchBox.press('Enter'); - }); + await page + .locator('atomic-external') + .getByLabel('Search field with suggestions') + .fill('hello'); - test("other components' state under the same atomic-external should be affected", async ({ - external, - }) => { - await expect(external.querySummary).toHaveText(/hello/); - }); + await external.searchBox.press('Enter'); + }); + + test("other components' state under the same atomic-external should be affected", async ({ + external, + }) => { + await expect(external.querySummary).toHaveText(/hello/); + }); - test("other components' state under the linked atomic-search-interface should be affected", async ({ - page, - }) => { - const querySummary = page.locator( - 'atomic-search-interface#interface-2 > atomic-query-summary' - ); - await expect(querySummary).toHaveText(/hello/); - }); + test("other components' state under the linked atomic-search-interface should be affected", async ({ + page, + }) => { + const querySummary = page.locator( + 'atomic-search-interface#interface-2 > atomic-query-summary' + ); + await expect(querySummary).toHaveText(/hello/); + }); - test("other components' state under a different atomic-search-interface should not be affected", async ({ - page, - }) => { - const querySummary = page.locator( - 'atomic-search-interface#interface-1 > atomic-query-summary' - ); - await expect(querySummary).not.toHaveText(/hello/); - }); + test("other components' state under a different atomic-search-interface should not be affected", async ({ + page, + }) => { + const querySummary = page.locator( + 'atomic-search-interface#interface-1 > atomic-query-summary' + ); + await expect(querySummary).not.toHaveText(/hello/); }); }); diff --git a/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx b/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx index cb91b5aa9f6..145ca3fd6ea 100644 --- a/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx +++ b/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx @@ -139,9 +139,12 @@ export class AtomicQuickview implements InitializableComponent { this.quickview.fetchResultContent(); } - public render() { + componentWillUpdate(): void { this.addQuickviewModalIfNeeded(); this.updateModalContent(); + } + + public render() { if (this.quickviewState.resultHasPreview) { return (