Skip to content

Commit

Permalink
docs(headless): added defineSearchBox SSR controller (#3108)
Browse files Browse the repository at this point in the history
  • Loading branch information
btaillon-coveo authored Aug 23, 2023
1 parent 18a622f commit 0f131c9
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/actions/e2e-headless-ssr/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ runs:
name: Run Cypress
with:
browser: chrome
command-prefix: 'npx cypress-repeat run -n 3 --until-passes --rerun-failed-only --'
command-prefix: 'npm run e2e'
working-directory: ./packages/samples/headless-ssr
start: npm run prod
wait-on: 'http://localhost:3000'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {SearchEngine} from '../../../app/search-engine/search-engine';
import {ControllerDefinitionWithoutProps} from '../../../app/ssr-engine/types/common';
import {
SearchBox,
SearchBoxProps,
buildSearchBox,
} from '../../search-box/headless-search-box';

export type {
SearchBoxOptions,
SearchBoxProps,
SearchBoxState,
SearchBox,
} from '../../search-box/headless-search-box';

/**
* @internal
*/
export const defineSearchBox = (
props?: SearchBoxProps
): ControllerDefinitionWithoutProps<SearchEngine, SearchBox> => ({
build: (engine) => buildSearchBox(engine, props),
});
7 changes: 7 additions & 0 deletions packages/headless/src/ssr.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export type {

export {defineSearchEngine} from './app/ssr-engine/ssr-engine';

export type {
SearchBox,
SearchBoxOptions,
SearchBoxProps,
SearchBoxState,
} from './controllers/ssr/search-box/headless-ssr-search-box';
export {defineSearchBox} from './controllers/ssr/search-box/headless-ssr-search-box';
export type {
ResultList,
ResultListOptions,
Expand Down
68 changes: 54 additions & 14 deletions packages/samples/headless-ssr/cypress/e2e/ssr.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import 'cypress-web-vitals';
import {ConsoleAliases, spyOnConsole, waitForHydration} from './ssr-e2e-utils';

const getResultTitles = () =>
(
cy.get('.result-list li').invoke('map', function (this: HTMLElement) {
return this.innerText;
}) as Cypress.Chainable<JQuery<string>>
).invoke('toArray');

describe('headless ssr example', () => {
const route = '/generic';
const numResults = 10;
Expand All @@ -14,7 +21,9 @@ describe('headless ssr example', () => {
expect(dom.querySelector(msgSelector)?.textContent).to.equal(
numResultsMsg
);
expect(dom.querySelectorAll('li').length).to.equal(numResults);
expect(dom.querySelectorAll('.result-list li').length).to.equal(
numResults
);
});
});
cy.visit(route);
Expand All @@ -23,7 +32,7 @@ describe('headless ssr example', () => {
it('renders page in CSR as expected', () => {
cy.visit(route);
cy.get(msgSelector).should('have.text', numResultsMsg);
cy.get('li').should('have.length', numResults);
cy.get('.result-list li').should('have.length', numResults);
});

it('renders result list in SSR and then in CSR', () => {
Expand All @@ -48,21 +57,52 @@ describe('headless ssr example', () => {
});

it('should pass the web-vitals audits', () => {
// TODO: Add input based vitals after interactive elements are added to test page (e.g. search box)
// Note: Thresholds might need to be adjusted as the page tested changes (e.g. more components are added etc)
const VITALS_THRESHOLD = {
thresholds: {fcp: 100, lcp: 100, cls: 0, ttfb: 20},
const VITALS_THRESHOLD: Cypress.ReportWebVitalsConfig = {
thresholds: {
fcp: 200,
lcp: 200,
cls: 0,
ttfb: 40,
fid: 200, // TODO: Make sure that the time for re-rendering after the interactions below is properly counted.
inp: 200,
},
};
cy.visit(route);
cy.vitals(VITALS_THRESHOLD);
cy.startVitalsCapture({url: route});
getResultTitles()
.should('have.length.greaterThan', 0)
.as('initial-results');
waitForHydration();
cy.get('.search-box input').focus().type('abc{enter}');
cy.get<string>('@initial-results').then((initialResults) =>
getResultTitles().should('not.deep.equal', initialResults)
);
cy.reportVitals(VITALS_THRESHOLD);
});

it('should not log any error nor warning', () => {
spyOnConsole();
cy.visit(route);
waitForHydration();
cy.wait(1000);
cy.get(ConsoleAliases.error).should('not.be.called');
cy.get(ConsoleAliases.warn).should('not.be.called');
describe('after hydration', () => {
beforeEach(() => {
spyOnConsole();
cy.visit(route);
waitForHydration();
});

it('should not log any error nor warning', () => {
cy.wait(1000);
cy.get(ConsoleAliases.error).should('not.be.called');
cy.get(ConsoleAliases.warn).should('not.be.called');
});

it('after submitting a query, should change search results', () => {
getResultTitles()
.should('have.length.greaterThan', 0)
.as('initial-results');
cy.get('.search-box input').focus().type('abc{enter}');
cy.get<string>('@initial-results').then((initialResults) => {
getResultTitles()
.should('not.deep.equal', initialResults)
.and('have.length', 10);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import {
defineResultList,
InferSSRState,
InferCSRState,
defineSearchBox,
} from '@coveo/headless/ssr';

const engineDefinition = defineSearchEngine({
configuration: {
...getSampleSearchEngineConfiguration(),
analytics: {enabled: false},
},
controllers: {resultList: defineResultList()},
controllers: {
searchBox: defineSearchBox(),
resultList: defineResultList(),
},
});

export type SearchSSRState = InferSSRState<typeof engineDefinition>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const HydrationMetadata: FunctionComponent<HydrationMetadataProps> = ({
<input
id="hydrated-indicator"
type="checkbox"
readOnly
disabled
checked={!!csrResult}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ import {
import {useEffect, useState, FunctionComponent} from 'react';

interface ResultListProps {
initialState: ResultListState;
ssrState: ResultListState;
controller?: ResultListController;
}

export const ResultList: FunctionComponent<ResultListProps> = (props) => {
const {initialState, controller} = props;
const [state, setState] = useState(initialState);
export const ResultList: FunctionComponent<ResultListProps> = ({
ssrState,
controller,
}) => {
const [state, setState] = useState(ssrState);

useEffect(
() => controller?.subscribe(() => setState({...controller.state})),
[controller]
);

return (
<ul>
<ul className="result-list">
{state.results.map((result) => (
<li key={result.uniqueId}>
<h3>{result.title}</h3>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
SearchBoxState,
SearchBox as SearchBoxController,
} from '@coveo/headless';
import {useEffect, useState, FunctionComponent} from 'react';

interface SearchBoxProps {
ssrState: SearchBoxState;
controller?: SearchBoxController;
}

export const SearchBox: FunctionComponent<SearchBoxProps> = ({
ssrState,
controller,
}) => {
const [state, setState] = useState(ssrState);

useEffect(
() => controller?.subscribe?.(() => setState({...controller.state})),
[controller]
);

return (
<form
className="search-box"
onSubmit={(e) => {
e.preventDefault();
controller?.submit();
}}
>
<input
value={state.value}
onChange={(e) => controller?.updateText(e.target.value)}
/>
</form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {useEffect, useState} from 'react';
import {HydrationMetadata} from './hydration-metadata';
import {ResultList} from './result-list';
import {SearchBox} from './search-box';

export default function SearchPage({ssrState}: {ssrState: SearchSSRState}) {
const [csrResult, setCSRResult] = useState<SearchCSRState | undefined>(
Expand All @@ -22,8 +23,12 @@ export default function SearchPage({ssrState}: {ssrState: SearchSSRState}) {
return (
<>
<HydrationMetadata ssrState={ssrState} csrResult={csrResult} />
<SearchBox
ssrState={ssrState.controllers.searchBox.state}
controller={csrResult?.controllers.searchBox}
/>
<ResultList
initialState={ssrState.controllers.resultList.state}
ssrState={ssrState.controllers.resultList.state}
controller={csrResult?.controllers.resultList}
/>
</>
Expand Down
2 changes: 1 addition & 1 deletion ui-kit.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,4 @@
"editor.formatOnSave": true
},
}
}
}

0 comments on commit 0f131c9

Please sign in to comment.