Skip to content

Commit

Permalink
Merge 40af12e into 1b49530
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremierobert-coveo committed Feb 25, 2020
2 parents 1b49530 + 40af12e commit 849cf32
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/Index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { UserProfilingEndpoint } from './rest/UserProfilingEndpoint';
export { UserProfileModel } from './models/UserProfileModel';
export { ResultsFilter } from './components/ResultsFilter/ResultsFilter';
export { ViewedByCustomer } from './components/ViewedByCustomer/ViewedByCustomer';
export { ResultAction } from './components/ResultAction/ResultAction';
119 changes: 119 additions & 0 deletions src/components/ResultAction/ResultAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Component, ComponentOptions, IQueryResult, IResultsComponentBindings, $$ } from 'coveo-search-ui';

/**
* The possible options for _ResultAction_.
*/
export interface IResultActionOptions {
/**
* The icon that the ResultAction will display.
* If text is provided, the button will contain that text.
* If the HTML of an SVG image is provided, that image will be displayed in the button.
*/
icon?: string;

/**
* The tooltip that displays on hovering the ResultAction.
*/
tooltip?: string;
}

/**
* The base class for all ResultAction components.
* Its main responsibility is handling the visual elements of the Result Action.
*/
export abstract class ResultAction extends Component {
static ID = 'ResultAction';

private isInitialized = false;

/**
* The possible options for _ResultAction_.
* @componentOptions
*/
static options: IResultActionOptions = {
/**
* See {@link IResultActionOptions.icon}
* Optional. You may instead provide the icon by appending it as a child element.
*/
icon: ComponentOptions.buildStringOption(),

/**
* See {@link IResultActionOptions.tooltip}
* Optional. If no tooltip is provided, the tooltip popup will not appear.
*/
tooltip: ComponentOptions.buildStringOption()
};

/**
* Construct a ResultAction component.
* @param element The HTML element bound to this component.
* @param options The options that can be provided to this component.
* @param bindings The bindings, or environment within which this component exists.
* @param queryResult The result of the query in which this resultAction exists.
*/
constructor(
public element: HTMLElement,
public options: IResultActionOptions,
public bindings?: IResultsComponentBindings,
public queryResult?: IQueryResult
) {
super(element, ResultAction.ID, bindings);

this.options = ComponentOptions.initComponentOptions(element, ResultAction, options);
this.queryResult = this.queryResult || this.resolveResult();

// Hide until initialized.
$$(this.element).addClass('coveo-hidden');

this.bind.on(this.element, 'click', () => this.doAction());
}

/**
* The action that will be performed when the ResultAction is clicked.
* @abstract
*/
protected abstract doAction(): void;

/**
* Initializes the component if it is not already initialized.
*/
protected init() {
if (!this.isInitialized) {
this.show();
this.isInitialized = true;
} else {
this.logger.debug('Attempted to initialize ResultAction that was already initialized.');
}
}

/**
* Deactivate the component if it is initialized.
* @param e The reason for the deactivation.
*/
protected deactivate(e: string) {
$$(this.element).remove();
this.logger.warn(e);
this.isInitialized = false;
}

/**
* Make the result action button visible.
*/
private show() {
$$(this.element).removeClass('coveo-hidden');

if (this.options.icon) {
const icon = document.createElement('span');
icon.innerHTML = this.options.icon;
icon.className = 'coveo-icon';
this.element.appendChild(icon);
}

if (this.options.tooltip) {
const tooltip = document.createElement('span');
tooltip.innerText = this.options.tooltip;
tooltip.className = 'coveo-caption-for-icon';
this.element.appendChild(tooltip);
}
}
}
117 changes: 117 additions & 0 deletions tests/components/ResultAction/ResultAction.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { IQueryResult, $$ } from 'coveo-search-ui';
import { createSandbox, SinonStub, SinonSandbox } from 'sinon';
import { Mock, Fake } from 'coveo-search-ui-tests';
import { ResultAction } from '../../../src/components/ResultAction/ResultAction';

describe('ResultAction', () => {
let sandbox: SinonSandbox;

let componentSetup: Mock.IBasicComponentSetup<ResultActionMockImpl>;
let result: IQueryResult;
let element: HTMLElement;
let testComponent: ResultActionMockImpl;
let testOptions: Mock.AdvancedComponentSetupOptions;

// Since ResultAction is abstract, create a mock implementation.
class ResultActionMockImpl extends ResultAction {
// Make functions public so they can be tested.
public doAction: SinonStub;
public init: () => void;
public deactivate: (e?: string) => void;
}

beforeAll(() => {
sandbox = createSandbox();
});

beforeEach(() => {
result = Fake.createFakeResult();
element = document.createElement('div');
document.body.append(element);
testOptions = new Mock.AdvancedComponentSetupOptions(element, { icon: 'someIcon', tooltip: 'someTooltip' });

componentSetup = Mock.advancedResultComponentSetup(ResultActionMockImpl, result, testOptions);
testComponent = componentSetup.cmp;
testComponent.doAction = sandbox.stub();
});

afterEach(() => {
sandbox.reset();
sandbox.restore();
$$(document.body)
.children()
.forEach(el => el.remove());
});

describe('after construction', () => {
it('should call resolveResult if no results are given', () => {
const resolveResultStub = sandbox.stub(ResultActionMockImpl.prototype, 'resolveResult');

componentSetup = Mock.advancedResultComponentSetup(ResultActionMockImpl, null, testOptions);
testComponent = componentSetup.cmp;
testComponent.doAction = sandbox.stub();

expect(resolveResultStub.called).toBeTrue();
});

it('should be hidden by default', () => {
expect(element.classList.contains('coveo-hidden')).toBeTrue();
expect(element.hasChildNodes()).toBeFalse();
});
});

describe('after initialization', () => {
beforeEach(() => {
testComponent.init();
});

it('should log a debug message if initialized twice', () => {
const debugStub = sandbox.stub(testComponent.logger, 'debug');
expect(debugStub.called).toBeFalse();
testComponent.init();
expect(debugStub.called).toBeTrue();
});

it('should be visible', () => {
expect(element.classList.contains('coveo-hidden')).toBeFalse();
expect(element.hasChildNodes()).toBeTrue();
});

it('should become invisible after deactivation', () => {
testComponent.deactivate();
expect(element.parentElement).toBeNull();
});

it('should be able to reinitialize after being deactivated', () => {
testComponent.deactivate();
testComponent.init();
expect(element.classList.contains('coveo-hidden')).toBeFalse();
expect(element.hasChildNodes()).toBeTrue();
});

it('should invoke the action when clicked', () => {
element.click();
expect(testComponent.doAction.called).toBeTrue();
});
});

describe('options', () => {
it('should not display an icon after initialization if *icon* is not set', () => {
testComponent.options.icon = null;

testComponent.init();

expect(element.classList.contains('coveo-hidden')).toBeFalse();
expect(element.querySelector('.coveo-icon')).toBeNull();
});

it('should not display a tooltip after initialization if *tooltip* is not set', () => {
testComponent.options.tooltip = null;

testComponent.init();

expect(element.classList.contains('coveo-hidden')).toBeFalse();
expect(element.querySelector('.coveo-tooltip')).toBeNull();
});
});
});

0 comments on commit 849cf32

Please sign in to comment.