diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue index 8869bde2..8a4c5fd1 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue @@ -1,29 +1,27 @@ - - - - revert - revert - - + - - diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/ToggleSwitch.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/ToggleSwitch.vue new file mode 100644 index 00000000..2c4fd606 --- /dev/null +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/ToggleSwitch.vue @@ -0,0 +1,188 @@ + + + + + {{ label }} + {{ label }} + + + + + + + diff --git a/tests/e2e/specs/revert-toggle.cy.js b/tests/e2e/specs/revert-toggle.cy.js new file mode 100644 index 00000000..d928b108 --- /dev/null +++ b/tests/e2e/specs/revert-toggle.cy.js @@ -0,0 +1,50 @@ +describe('revert toggle', () => { + context('toggle switch', () => { + beforeEach(() => { + cy.visit('/'); + cy.get('.card') + .eq(1) // to get 2nd element, first is often cleanup that may lack revert button + .click(); // open the card card + cy.get('.toggle-switch') + .first() + .as('toggleSwitch'); + }); + + it('should be visible', () => { + cy.get('@toggleSwitch') + .should('be.visible'); + }); + + it('should have revert label', () => { + cy.get('@toggleSwitch') + .find('span') + .contains('revert'); + }); + + it('should render label completely without clipping', () => { + cy + .get('@toggleSwitch') + .find('span') + .should(($label) => { + const text = $label.text(); + const font = getFont($label[0]); + const expectedMinimumTextWidth = getTextWidth(text, font); + const containerWidth = $label.parent().width(); + expect(expectedMinimumTextWidth).to.be.lessThan(containerWidth); + }); + }); + }); +}); + +function getFont(element) { + const computedStyle = window.getComputedStyle(element); + return `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`; +} + +function getTextWidth(text, font) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = font; + const measuredWidth = ctx.measureText(text).width; + return measuredWidth; +} diff --git a/tests/unit/presentation/components/Scripts/View/Tree/SelectableTree/Node/ToggleSwitch.spec.ts b/tests/unit/presentation/components/Scripts/View/Tree/SelectableTree/Node/ToggleSwitch.spec.ts new file mode 100644 index 00000000..eec4b1ec --- /dev/null +++ b/tests/unit/presentation/components/Scripts/View/Tree/SelectableTree/Node/ToggleSwitch.spec.ts @@ -0,0 +1,238 @@ +import 'mocha'; +import { + Wrapper, shallowMount, + mount, +} from '@vue/test-utils'; +import { expect } from 'chai'; +import { nextTick, defineComponent } from 'vue'; +import ToggleSwitch from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/ToggleSwitch.vue'; + +const DOM_INPUT_TOGGLE_CHECKBOX_SELECTOR = 'input.toggle-input'; +const DOM_INPUT_TOGGLE_LABEL_OFF_SELECTOR = 'span.label-off'; +const DOM_INPUT_TOGGLE_LABEL_ON_SELECTOR = 'span.label-on'; + +describe('ToggleSwitch.vue', () => { + describe('initial state', () => { + const testCases = [ + { + initialValue: false, + description: 'unchecked for false', + }, + { + initialValue: true, + description: 'checked for true', + }, + ]; + testCases.forEach(({ initialValue, description }) => { + it(`renders as ${description}`, () => { + // arrange + const expectedState = initialValue; + + // act + const wrapper = mountComponent({ + properties: { + modelValue: initialValue, + }, + }); + const { checkboxElement } = getCheckboxElement(wrapper); + + // assert + expect(checkboxElement.checked).to.equal(expectedState); + }); + }); + }); + describe('label rendering', () => { + const testCases = [ + { + description: 'off label', + selector: DOM_INPUT_TOGGLE_LABEL_OFF_SELECTOR, + }, + { + description: 'on label', + selector: DOM_INPUT_TOGGLE_LABEL_ON_SELECTOR, + }, + ]; + testCases.forEach(({ selector, description }) => { + it(description, () => { + // arrange + const expectedLabel = 'expected-test-label'; + + // act + const wrapper = mountComponent({ + properties: { + label: expectedLabel, + }, + }); + + // assert + const element = wrapper.find(selector); + expect(element.text()).to.equal(expectedLabel); + }); + }); + }); + describe('model updates', () => { + describe('emission on change', () => { + const testCases = [ + { + initialValue: true, + newCheckValue: false, + }, + { + initialValue: false, + newCheckValue: true, + }, + ]; + testCases.forEach(({ initialValue, newCheckValue }) => { + it(`emits ${newCheckValue} when initial value is ${initialValue} and checkbox value changes`, async () => { + // arrange + const wrapper = mountComponent({ + properties: { + modelValue: initialValue, + }, + }); + const { checkboxWrapper } = getCheckboxElement(wrapper); + + // act + await checkboxWrapper.setChecked(newCheckValue); + await nextTick(); + + // assert + expect(wrapper.emitted().input).to.deep.equal([[newCheckValue]]); + }); + }); + }); + describe('no emission on identical value', () => { + const testCases = [ + { + value: true, + description: 'true', + }, + { + value: false, + description: 'false', + }, + ]; + testCases.forEach(({ value, description }) => { + it(`does not emit for an unchanged value of ${description}`, async () => { + // arrange + const wrapper = mountComponent({ + properties: { + modelValue: value, + }, + }); + const { checkboxWrapper } = getCheckboxElement(wrapper); + + // act + await checkboxWrapper.setChecked(value); + await nextTick(); + + // assert + expect(wrapper.emitted().input).to.equal(undefined); + }); + }); + }); + }); + + describe('click propagation', () => { + it('stops propagation `stopClickPropagation` is true', async () => { + // arrange + const { wrapper: parentWrapper, parentClickEventName } = mountToggleSwitchParent( + { stopClickPropagation: true }, + ); + const switchWrapper = parentWrapper.getComponent(ToggleSwitch); + + // act + switchWrapper.trigger('click'); + await nextTick(); + + // assert + expect(switchWrapper.exists()); + const receivedEvents = parentWrapper.emitted(parentClickEventName); + expect(receivedEvents).to.equal(undefined); + }); + it('allows propagation `stopClickPropagation` is false', async () => { + // arrange + const { wrapper: parentWrapper, parentClickEventName } = mountToggleSwitchParent( + { stopClickPropagation: false }, + ); + const switchWrapper = parentWrapper.getComponent(ToggleSwitch); + + // act + switchWrapper.trigger('click'); + await nextTick(); + + // assert + expect(switchWrapper.exists()); + const receivedEvents = parentWrapper.emitted(parentClickEventName); + expect(receivedEvents).to.have.lengthOf(1); + }); + }); +}); + +function getCheckboxElement(wrapper: Wrapper) { + const checkboxWrapper = wrapper.find(DOM_INPUT_TOGGLE_CHECKBOX_SELECTOR); + const checkboxElement = checkboxWrapper.element as HTMLInputElement; + return { + checkboxWrapper, + checkboxElement, + }; +} + +function mountComponent(options?: { + readonly properties?: { + readonly modelValue?: boolean, + readonly label?: string, + readonly stopClickPropagation?: boolean, + } +}) { + const wrapper = shallowMount(ToggleSwitch as unknown, { + propsData: { + value: options?.properties?.modelValue, + label: options?.properties?.label ?? 'test-label', + stopClickPropagation: options?.properties?.stopClickPropagation, + }, + }); + return wrapper; +} + +function mountToggleSwitchParent(options?: { + readonly stopClickPropagation?: boolean, +}) { + const parentClickEventName = 'parent-clicked'; + const parentComponent = defineComponent({ + components: { + ToggleSwitch, + }, + emits: [parentClickEventName], + template: ` + + + + `, + setup(_, { emit }) { + const stopClickPropagation = options?.stopClickPropagation; + + function handleParentClick() { + emit(parentClickEventName); + } + + return { + handleParentClick, + stopClickPropagation, + }; + }, + }); + const wrapper = mount( + parentComponent as unknown, + { + stubs: { ToggleSwitch: false }, + }, + ); + return { + wrapper, + parentClickEventName, + }; +}