diff --git a/tests/integration/components/o-s-s/context-menu-test.ts b/tests/integration/components/o-s-s/context-menu-test.ts
new file mode 100644
index 000000000..1f2032f9c
--- /dev/null
+++ b/tests/integration/components/o-s-s/context-menu-test.ts
@@ -0,0 +1,221 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { click, render, triggerEvent } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import sinon from 'sinon';
+import { setupIntl } from 'ember-intl/test-support';
+
+module('Integration | Component | o-s-s/context-menu', function (hooks) {
+ setupRenderingTest(hooks);
+ setupIntl(hooks);
+
+ hooks.beforeEach(function () {
+ this.label = 'Open menu';
+ this.onOpenStub = sinon.stub();
+ this.onCloseStub = sinon.stub();
+ });
+
+ test('it renders', async function (assert) {
+ await render(hbs`
`);
+
+ assert.dom('[data-control-name="context-menu"]').exists();
+ assert.dom('[data-control-name="context-menu"]').hasText(this.label);
+ });
+
+ module('It accepts same arguments as OSSButton component', function () {
+ test('it accepts label argument', async function (assert) {
+ this.label = 'first';
+ await render(hbs`
`);
+ assert.dom('button').hasText('first');
+ this.set('label', 'secondary');
+ assert.dom('button').hasText('secondary');
+ });
+
+ test('it accepts skin argument', async function (assert) {
+ this.skin = 'primary';
+ await render(hbs`
`);
+ assert.dom('button').hasClass('upf-btn--primary');
+ this.set('skin', 'secondary');
+ assert.dom('button').hasClass('upf-btn--secondary');
+ });
+
+ test('it accepts size argument', async function (assert) {
+ this.size = 'sm';
+ await render(hbs`
`);
+ assert.dom('button').hasClass('upf-btn--sm');
+ this.set('size', 'md');
+ assert.dom('button').hasClass('upf-btn--md');
+ });
+
+ test('it accepts loading argument', async function (assert) {
+ this.loading = true;
+ await render(
+ hbs`
`
+ );
+ assert.dom('button i').hasClass('fa-circle-notch').hasClass('fa-spin');
+ this.set('loading', false);
+ assert.dom('button i').doesNotExist();
+ });
+
+ test('it accepts loadingOptions argument', async function (assert) {
+ this.loadingOptions = { showLabel: true };
+ await render(
+ hbs`
`
+ );
+ assert.dom('button i').hasClass('fa-circle-notch').hasClass('fa-spin');
+ assert.dom('button').hasText(this.label);
+ this.set('loadingOptions', { showLabel: false });
+ assert.dom('button').hasNoText();
+ });
+
+ test('it accepts icon argument', async function (assert) {
+ this.icon = 'fa-plane';
+ await render(hbs`
`);
+ assert.dom('button i').hasClass('fa-plane');
+ this.set('icon', 'fa-plus');
+ assert.dom('button i').hasClass('fa-plus');
+ });
+
+ test('it accepts iconUrl argument', async function (assert) {
+ this.iconUrl = '/@upfluence/oss-components/assets/heart.svg';
+ await render(
+ hbs`
`
+ );
+ assert.dom('button img').hasAttribute('src', this.iconUrl);
+ });
+
+ test('it accepts theme argument', async function (assert) {
+ this.theme = 'light';
+ await render(
+ hbs`
`
+ );
+ assert.dom('button').hasNoClass('upf-btn--dark-bg');
+ this.set('theme', 'dark');
+ assert.dom('button').hasClass('upf-btn--dark-bg');
+ });
+
+ test('it accepts square argument', async function (assert) {
+ this.square = true;
+ await render(
+ hbs`
`
+ );
+ assert.dom('button').hasClass('upf-square-btn');
+ this.set('square', false);
+ assert.dom('button').doesNotHaveClass('upf-square-btn');
+ });
+
+ test('it accepts countDown argument', async function (assert) {
+ const clock = sinon.useFakeTimers({
+ shouldAdvanceTime: true
+ });
+
+ this.countDown = { callback: sinon.stub(), time: 50, step: 10 };
+ await render(
+ hbs`
`
+ );
+ await click('button');
+ assert.true(this.countDown.callback.notCalled);
+ clock.tick(100);
+ assert.true(this.countDown.callback.calledOnce);
+ clock.restore();
+ });
+ });
+
+ module('When clicking on the button', function () {
+ test('it opens the panel', async function (assert) {
+ await render(hbs`
`);
+
+ assert.dom('.context-menu-panel__scrollable-container').doesNotExist();
+ await click('button');
+ assert.dom('.context-menu-panel__scrollable-container').exists();
+ });
+
+ test('it trigger the onMenuOpened callback', async function (assert) {
+ await render(hbs`
`);
+
+ assert.ok(this.onOpenStub.notCalled);
+ await click('button');
+ assert.ok(this.onOpenStub.calledOnce);
+ });
+
+ module('When clicking a second time on the button', function () {
+ test('it closes the panel', async function (assert) {
+ await render(hbs`
`);
+ await click('button');
+ assert.dom('.context-menu-panel__scrollable-container').exists();
+ await click('button');
+ assert.dom('.context-menu-panel__scrollable-container').doesNotExist();
+ });
+
+ test('it trigger the onMenuOpened callback', async function (assert) {
+ await render(hbs`
`);
+ await click('button');
+ assert.ok(this.onCloseStub.notCalled);
+ await click('button');
+ assert.ok(this.onCloseStub.calledOnce);
+ });
+ });
+ });
+
+ module('When the panel is opened', function () {
+ test('On click outside of the panel, it closes', async function (assert) {
+ await render(hbs`
+
+
`);
+ await click('button');
+ assert.ok(this.onCloseStub.notCalled);
+ await click('[data-control-name="outside-container"]');
+ assert.ok(this.onCloseStub.calledOnce);
+ });
+
+ module('CloseOnMouseLeave', () => {
+ test('When closeOnMouseLeave is true, on mouse leave it closes the panel', async function (assert) {
+ await render(hbs`
`);
+ await click('button');
+ assert.dom('.context-menu-panel__scrollable-container').exists();
+ await triggerEvent('.context-menu-panel__scrollable-container', 'mouseleave');
+ assert.dom('.context-menu-panel__scrollable-container').doesNotExist();
+ assert.ok(this.onCloseStub.calledOnce);
+ });
+
+ test('When closeOnMouseLeave is false, on mouse leave it does not close the panel', async function (assert) {
+ await render(hbs`
`);
+ await click('button');
+ assert.dom('.context-menu-panel__scrollable-container').exists();
+ await triggerEvent('.context-menu-panel__scrollable-container', 'mouseleave');
+ assert.dom('.context-menu-panel__scrollable-container').exists();
+ assert.ok(this.onCloseStub.notCalled);
+ });
+ });
+ });
+
+ test('When button is loading, clicking does nothing', async function (assert) {
+ await render(hbs`
`);
+
+ assert.ok(this.onOpenStub.notCalled);
+ await click('button');
+ assert.ok(this.onOpenStub.notCalled);
+ });
+});
diff --git a/tests/integration/components/o-s-s/context-menu/panel-test.ts b/tests/integration/components/o-s-s/context-menu/panel-test.ts
new file mode 100644
index 000000000..d470600c2
--- /dev/null
+++ b/tests/integration/components/o-s-s/context-menu/panel-test.ts
@@ -0,0 +1,266 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { click, render, settled, triggerEvent, waitUntil } from '@ember/test-helpers';
+import { hbs, type TemplateFactory } from 'ember-cli-htmlbars';
+import { setupIntl } from 'ember-intl/test-support';
+import sinon from 'sinon';
+import { setComponentTemplate } from '@ember/component';
+import Component from '@glimmer/component';
+import { SUBPANEL_OFFSET } from '@upfluence/oss-components/components/o-s-s/context-menu/panel';
+
+module('Integration | Component | o-s-s/context-menu/panel', function (hooks) {
+ setupRenderingTest(hooks);
+ setupIntl(hooks);
+
+ hooks.beforeEach(function () {
+ this.subItems = [
+ { title: 'Sub Item 1.1', action: sinon.stub() },
+ { title: 'Sub Item 1.2', action: sinon.stub() },
+ { title: 'Sub Item 1.3', action: sinon.stub() }
+ ];
+ this.items = [
+ { title: 'Item 1', action: sinon.stub(), items: this.subItems },
+ {
+ title: 'Item 2',
+ action: sinon.stub()
+ }
+ ];
+
+ this.onCloseStub = sinon.stub();
+ });
+
+ test('it renders properly', async function (assert) {
+ await render(hbs`
`);
+
+ assert.dom('.context-menu-panel__dropdown').exists();
+ assert.dom('.context-menu-panel__dropdown li .oss-infinite-select-option').exists({ count: 2 });
+ });
+
+ test('When referenceTarget is passed, it attaches and moves with the target', async function (assert) {
+ await render(hbs`
+ {{#if this.isInitialized}}
+
+ {{/if}}
+
+
+ `);
+
+ const buttonRef = document.querySelector('#second_button') as HTMLElement;
+
+ this.set('referenceTarget', buttonRef);
+ this.set('isInitialized', true);
+ await settled();
+ const panelRef = document.querySelector('.context-menu-panel__scrollable-container') as HTMLElement;
+ assert.equal(panelRef.style.top, '36px');
+ await waitUntil(() => panelRef.style.left === '63.5px', { timeout: 300 });
+ assert.equal(panelRef.style.left, '63.5px');
+ buttonRef.style.marginLeft = '50px';
+ await settled();
+ assert.equal(panelRef.style.top, '36px');
+ await waitUntil(() => panelRef.style.left === '113.5px', { timeout: 300 });
+ assert.equal(panelRef.style.left, '113.5px');
+ });
+
+ module('placement', function (hooks) {
+ test('When placement is set to right-start, it positions the panel accordingly', async function (assert) {
+ await render(hbs`
+ {{#if this.isInitialized}}
+
+ {{/if}}
+
+ `);
+
+ const buttonRef = document.querySelector('#trigger') as HTMLElement;
+
+ this.set('referenceTarget', buttonRef);
+ this.set('isInitialized', true);
+ const panelRef = document.querySelector('.context-menu-panel__scrollable-container') as HTMLElement;
+ const qunitTestContainer = buttonRef.offsetParent as HTMLElement;
+ const leftPosition = buttonRef.getBoundingClientRect().left - qunitTestContainer.getBoundingClientRect().left;
+ await settled();
+ const expectedLeftPosition =
+ Number(((leftPosition + buttonRef.getBoundingClientRect().width) * 2).toFixed(4)) + 'px';
+ const expectedTopPosition = 0 + 'px';
+ assert.equal(expectedLeftPosition, panelRef.style.left);
+ assert.equal(expectedTopPosition, panelRef.style.top);
+ });
+
+ test('When placement is set to bottom-start, it positions the panel accordingly', async function (assert) {
+ await render(hbs`
+ {{#if this.isInitialized}}
+
+ {{/if}}
+
+ `);
+
+ const buttonRef = document.querySelector('#trigger') as HTMLElement;
+
+ this.set('referenceTarget', buttonRef);
+ this.set('isInitialized', true);
+ const panelRef = document.querySelector('.context-menu-panel__scrollable-container') as HTMLElement;
+ const qunitTestContainer = buttonRef.offsetParent as HTMLElement;
+ const topPosition = buttonRef.getBoundingClientRect().top - qunitTestContainer.getBoundingClientRect().top;
+ await settled();
+ const expectedLeftPosition = 0 + 'px';
+ const expectedTopPosition =
+ Number(((topPosition + buttonRef.getBoundingClientRect().height) * 2).toFixed(4)) + 'px';
+ assert.equal(expectedLeftPosition, panelRef.style.left);
+ assert.equal(expectedTopPosition, panelRef.style.top);
+ });
+ });
+
+ module('Offset', function () {
+ test('When offset is 0, panel is stuck to his reference target', async function (assert) {
+ this.offset = 0;
+ await render(hbs`
+ {{#if this.isInitialized}}
+
+ {{/if}}
+
+ `);
+
+ const buttonRef = document.querySelector('#trigger') as HTMLElement;
+
+ this.set('referenceTarget', buttonRef);
+ this.set('isInitialized', true);
+ const panelRef = document.querySelector('.context-menu-panel__scrollable-container') as HTMLElement;
+ await settled();
+ assert.equal(panelRef.style.top, '36px');
+ });
+
+ test('When offset is defined, panel is is moved by that many pixels than defined to his reference target', async function (assert) {
+ this.offset = 20;
+ await render(hbs`
+ {{#if this.isInitialized}}
+
+ {{/if}}
+
+ `);
+
+ const buttonRef = document.querySelector('#trigger') as HTMLElement;
+
+ this.set('referenceTarget', buttonRef);
+ this.set('isInitialized', true);
+ const panelRef = document.querySelector('.context-menu-panel__scrollable-container') as HTMLElement;
+ await settled();
+ assert.equal(panelRef.style.top, '56px');
+ });
+ });
+
+ module('When items are passed', function () {
+ module('if items has subitems', function () {
+ test('Clicking on item opens a submenu on the right of the trigger element', async function (assert) {
+ await render(hbs`
+ {{#if this.isInitialized}}
+
+ {{/if}}
+
+ `);
+
+ const buttonRef = document.querySelector('#trigger') as HTMLElement;
+
+ this.set('referenceTarget', buttonRef);
+ this.set('isInitialized', true);
+
+ assert.dom('div.context-menu-panel__scrollable-container').exists({ count: 1 });
+ await click('div.context-menu-panel__scrollable-container li:nth-of-type(1) .oss-infinite-select-option');
+ assert.dom('div.context-menu-panel__scrollable-container').exists({ count: 2 });
+ const panels = document.querySelectorAll('div.context-menu-panel__scrollable-container');
+ const triggerPosition = (
+ panels[0]?.querySelector('div.context-menu-panel__dropdown li:nth-of-type(1)') as HTMLElement
+ ).getBoundingClientRect();
+ assert.equal(triggerPosition.x + triggerPosition.width, panels[1]?.getBoundingClientRect().x);
+ assert.equal(triggerPosition.y + SUBPANEL_OFFSET / 2, panels[1]?.getBoundingClientRect().y);
+ });
+
+ test('Submenu items are properly displayed', async function (assert) {
+ await render(hbs`
`);
+
+ assert.dom('div.context-menu-panel__dropdown').exists({ count: 1 });
+ await click('div.context-menu-panel__dropdown li:nth-of-type(1) .oss-infinite-select-option');
+ assert.dom('div.context-menu-panel__dropdown').exists({ count: 2 });
+ const panels = document.querySelectorAll('div.context-menu-panel__dropdown');
+ assert.equal(panels[1]?.querySelectorAll('li').length, 3);
+ assert.equal(
+ (panels[1]?.querySelector('li:nth-of-type(1)') as HTMLElement).textContent?.trim(),
+ 'Sub Item 1.1'
+ );
+ assert.equal(
+ (panels[1]?.querySelector('li:nth-of-type(2)') as HTMLElement).textContent?.trim(),
+ 'Sub Item 1.2'
+ );
+ assert.equal(
+ (panels[1]?.querySelector('li:nth-of-type(3)') as HTMLElement).textContent?.trim(),
+ 'Sub Item 1.3'
+ );
+ });
+ });
+
+ test('If item has custom component it render the component instead of default one', async function (assert) {
+ class TestComponent extends Component {}
+ setComponentTemplate(hbs`
{{@item.title}}
` as any, TestComponent);
+ const component = this.owner.register('component:test-component', TestComponent);
+
+ this.items = [{ title: 'custom', action: () => console.log('Item 1 clicked'), rowRenderer: TestComponent }];
+ await render(hbs`
`);
+
+ assert.dom('.context-menu-panel__dropdown li [data-control-name="custom-row"]').exists();
+ });
+ });
+
+ test('When no items are passed, it displays the empty state', async function (assert) {
+ await render(hbs`
`);
+
+ assert.dom('.upf-infinite-select__items-container--empty').exists();
+ });
+
+ test('When mouse leave the panel, it triggers onMouseLeave action', async function (assert) {
+ this.onMouseLeaveStub = sinon.stub();
+ await render(hbs`
`);
+ assert.ok(this.onMouseLeaveStub.notCalled);
+ await triggerEvent('.context-menu-panel__dropdown', 'mouseleave');
+ assert.ok(this.onMouseLeaveStub.calledOnce);
+ });
+
+ module('When clicking on an item', function () {
+ test('It triggers the item action', async function (assert) {
+ await render(hbs`
`);
+
+ assert.ok(this.items[1].action.notCalled);
+ await click('div.context-menu-panel__dropdown li:nth-of-type(2) .oss-infinite-select-option');
+ assert.ok(this.items[1].action.calledOnce);
+ });
+
+ test('When action returns false, the menu should stay open', async function (assert) {
+ this.items[1].action.returns(false);
+ await render(hbs`
`);
+
+ assert.ok(this.onCloseStub.notCalled);
+ await click('div.context-menu-panel__dropdown li:nth-of-type(2) .oss-infinite-select-option');
+ assert.ok(this.onCloseStub.notCalled);
+ });
+
+ test('When action returns true or undefined, the menu should close', async function (assert) {
+ await render(hbs`
`);
+
+ assert.ok(this.onCloseStub.notCalled);
+ await click('div.context-menu-panel__dropdown li:nth-of-type(2) .oss-infinite-select-option');
+ assert.ok(this.onCloseStub.calledOnce);
+ });
+ });
+});
diff --git a/tests/integration/components/o-s-s/infinite-select-test.js b/tests/integration/components/o-s-s/infinite-select-test.js
index 30edb3b82..1dd910487 100644
--- a/tests/integration/components/o-s-s/infinite-select-test.js
+++ b/tests/integration/components/o-s-s/infinite-select-test.js
@@ -23,6 +23,16 @@ const FAKE_DATA = [
{ name: 'Wolverine', characters: 'James Howlett' }
];
+const FAKE_DATA_GROUPED = [
+ { name: 'banana', label: 'banana', groupKey: 'fruit' },
+ { name: 'lettuce', label: 'lettuce', groupKey: 'vegetable' },
+ { name: 'orange', label: 'orange', groupKey: 'fruit' },
+ { name: 'carrot', label: 'carrot', groupKey: 'vegetable' },
+ { name: 'apple', label: 'apple', groupKey: 'fruit' },
+ { name: 'spinach', label: 'spinach', groupKey: 'vegetable' },
+ { name: 'other', label: 'other' }
+];
+
module('Integration | Component | o-s-s/infinite-select', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
@@ -508,4 +518,72 @@ module('Integration | Component | o-s-s/infinite-select', function (hooks) {
});
});
});
+
+ module('When data has groupKey', function (hooks) {
+ hooks.beforeEach(function () {
+ this.items = FAKE_DATA_GROUPED;
+ this.onSelect = () => {};
+ });
+
+ test('For each different groupKey, a group is created', async function (assert) {
+ await renderGrouped();
+
+ assert.dom('.upf-infinite-select__items-container ul').exists({ count: 3 });
+ assert
+ .dom('.upf-infinite-select__items-container ul:nth-of-type(1)')
+ .hasAttribute('data-control-name', 'infinite-select-group-fruit');
+ assert
+ .dom('.upf-infinite-select__items-container ul:nth-of-type(2)')
+ .hasAttribute('data-control-name', 'infinite-select-group-vegetable');
+ assert
+ .dom('.upf-infinite-select__items-container ul:nth-of-type(3)')
+ .hasAttribute('data-control-name', 'infinite-select-group-_ungrouped_');
+ });
+
+ test('Items are placed in their respective group', async function (assert) {
+ await renderGrouped();
+
+ const groupFruitItems = Array.from(
+ document.querySelectorAll('.upf-infinite-select__items-container ul:nth-of-type(1) .upf-infinite-select__item')
+ ).map((el) => el.textContent.trim());
+ const groupVegetableItems = Array.from(
+ document.querySelectorAll('.upf-infinite-select__items-container ul:nth-of-type(2) .upf-infinite-select__item')
+ ).map((el) => el.textContent.trim());
+ const groupUngroupedItems = Array.from(
+ document.querySelectorAll('.upf-infinite-select__items-container ul:nth-of-type(3) .upf-infinite-select__item')
+ ).map((el) => el.textContent.trim());
+
+ assert.deepEqual(groupFruitItems, ['banana', 'orange', 'apple']);
+ assert.deepEqual(groupVegetableItems, ['lettuce', 'carrot', 'spinach']);
+ assert.deepEqual(groupUngroupedItems, ['other']);
+ });
+
+ module('Separators', function () {
+ test('A separator is rendered between each group', async function (assert) {
+ await renderGrouped();
+
+ assert.dom('.upf-infinite-select__items-container hr.group-separator').exists({ count: 2 });
+ });
+
+ test('There is no separator after the last group', async function (assert) {
+ await renderGrouped();
+
+ assert.dom('.upf-infinite-select__items-container ul:last-of-type + hr.group-separator').doesNotExist();
+ });
+ });
+ });
+
+ async function renderGrouped(): Promise
{
+ await render(
+ hbs`
+ <:option as |item index|>
+ {{item.label}}
+
+ `
+ );
+ }
});
diff --git a/tests/integration/components/o-s-s/text-area-test.ts b/tests/integration/components/o-s-s/text-area-test.ts
index 03c976a70..6c75bf464 100644
--- a/tests/integration/components/o-s-s/text-area-test.ts
+++ b/tests/integration/components/o-s-s/text-area-test.ts
@@ -107,5 +107,45 @@ module('Integration | Component | o-s-s/text-area', function (hooks) {
this.resize = 'NotACorrectValue';
await render(hbs``);
});
+
+ test('when @hasError is true, textarea has errored class', async function (assert) {
+ await render(hbs``);
+ assert.dom(this.textareaSelector).hasClass('oss-textarea--errored');
+ assert.dom('.oss-textarea-container').hasClass('oss-textarea-container--errored');
+ });
+
+ test('when @hasError is false, textarea does not have errored class', async function (assert) {
+ await render(hbs``);
+ assert.dom(this.textareaSelector).hasNoClass('oss-textarea--errored');
+ assert.dom('.oss-textarea-container').hasNoClass('oss-textarea-container--errored');
+ });
+
+ test('when @errorMessage is provided, textarea has errored class', async function (assert) {
+ await render(hbs``);
+ assert.dom(this.textareaSelector).hasClass('oss-textarea--errored');
+ assert.dom('.oss-textarea-container').hasClass('oss-textarea-container--errored');
+ assert.dom('.font-color-error-500').exists();
+ assert.dom('.font-color-error-500').hasText('This is an error');
+ });
+
+ test('when @errorMessage is empty string, textarea does not have errored class', async function (assert) {
+ await render(hbs``);
+ assert.dom(this.textareaSelector).hasNoClass('oss-textarea--errored');
+ assert.dom('.oss-textarea-container').hasNoClass('oss-textarea-container--errored');
+ });
+
+ test('when both @hasError and @errorMessage are provided, textarea has errored class', async function (assert) {
+ await render(hbs``);
+ assert.dom(this.textareaSelector).hasClass('oss-textarea--errored');
+ assert.dom('.oss-textarea-container').hasClass('oss-textarea-container--errored');
+ assert.dom('.font-color-error-500').exists();
+ assert.dom('.font-color-error-500').hasText('Error message');
+ });
+
+ test('when neither @hasError nor @errorMessage are provided, textarea does not have errored class', async function (assert) {
+ await render(hbs``);
+ assert.dom(this.textareaSelector).hasNoClass('oss-textarea--errored');
+ assert.dom('.oss-textarea-container').hasNoClass('oss-textarea-container--errored');
+ });
});
});