diff options
Diffstat (limited to 'spec/frontend')
9 files changed, 448 insertions, 108 deletions
diff --git a/spec/frontend/__helpers__/assert_props.js b/spec/frontend/__helpers__/assert_props.js index 3e372454bf5..9935719580a 100644 --- a/spec/frontend/__helpers__/assert_props.js +++ b/spec/frontend/__helpers__/assert_props.js @@ -1,14 +1,30 @@ import { mount } from '@vue/test-utils'; import { ErrorWithStack } from 'jest-util'; -export function assertProps(Component, props, extraMountArgs = {}) { - const originalConsoleError = global.console.error; - global.console.error = function error(...args) { - throw new ErrorWithStack( - `Unexpected call of console.error() with:\n\n${args.join(', ')}`, - this.error, - ); +function installConsoleHandler(method) { + const originalHandler = global.console[method]; + + global.console[method] = function throwableHandler(...args) { + if (args[0]?.includes('Invalid prop') || args[0]?.includes('Missing required prop')) { + throw new ErrorWithStack( + `Unexpected call of console.${method}() with:\n\n${args.join(', ')}`, + this[method], + ); + } + + originalHandler.apply(this, args); + }; + + return function restore() { + global.console[method] = originalHandler; }; +} + +export function assertProps(Component, props, extraMountArgs = {}) { + const [restoreError, restoreWarn] = [ + installConsoleHandler('error'), + installConsoleHandler('warn'), + ]; const ComponentWithoutRenderFn = { ...Component, render() { @@ -19,6 +35,7 @@ export function assertProps(Component, props, extraMountArgs = {}) { try { mount(ComponentWithoutRenderFn, { propsData: props, ...extraMountArgs }); } finally { - global.console.error = originalConsoleError; + restoreError(); + restoreWarn(); } } diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js index 30f674f5ba7..0e59e9ab5b6 100644 --- a/spec/frontend/jobs/components/table/job_table_app_spec.js +++ b/spec/frontend/jobs/components/table/job_table_app_spec.js @@ -127,6 +127,25 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); }); + it('should refetch jobs count query when the amount jobs and count do not match', async () => { + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); + + // after applying filter a new count is fetched + findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1); + + // tab is switched to `finished`, no count + await findTabs().vm.$emit('fetchJobsByStatus', ['FAILED', 'SUCCESS', 'CANCELED']); + + // tab is switched back to `all`, the old filter count has to be overwritten with new count + await findTabs().vm.$emit('fetchJobsByStatus', null); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(2); + }); + describe('when infinite scrolling is triggered', () => { it('does not display a skeleton loader', () => { triggerInfiniteScroll(); @@ -251,6 +270,18 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1); }); + it('refetches jobs count query when filtering', async () => { + createComponent(); + + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1); + }); + it('shows raw text warning when user inputs raw text', async () => { const expectedWarning = { message: s__( @@ -262,11 +293,13 @@ describe('Job table app', () => { createComponent(); jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn()); + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']); expect(createAlert).toHaveBeenCalledWith(expectedWarning); expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); }); it('updates URL query string when filtering jobs by status', async () => { diff --git a/spec/frontend/lib/utils/css_utils_spec.js b/spec/frontend/lib/utils/css_utils_spec.js new file mode 100644 index 00000000000..dcaeb075c93 --- /dev/null +++ b/spec/frontend/lib/utils/css_utils_spec.js @@ -0,0 +1,22 @@ +import { getCssClassDimensions } from '~/lib/utils/css_utils'; + +describe('getCssClassDimensions', () => { + const mockDimensions = { width: 1, height: 2 }; + let actual; + + beforeEach(() => { + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(mockDimensions); + actual = getCssClassDimensions('foo bar'); + }); + + it('returns the measured width and height', () => { + expect(actual).toEqual(mockDimensions); + }); + + it('measures an element with the given classes', () => { + expect(Element.prototype.getBoundingClientRect).toHaveBeenCalledTimes(1); + + const [tempElement] = Element.prototype.getBoundingClientRect.mock.contexts; + expect([...tempElement.classList]).toEqual(['foo', 'bar']); + }); +}); diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js index cc6f1c27142..dad7308ac0a 100644 --- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js +++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js @@ -139,6 +139,25 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); }); + it('should refetch jobs count query when the amount jobs and count do not match', async () => { + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); + + // after applying filter a new count is fetched + findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1); + + // tab is switched to `finished`, no count + await findTabs().vm.$emit('fetchJobsByStatus', ['FAILED', 'SUCCESS', 'CANCELED']); + + // tab is switched back to `all`, the old filter count has to be overwritten with new count + await findTabs().vm.$emit('fetchJobsByStatus', null); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(2); + }); + describe('when infinite scrolling is triggered', () => { it('does not display a skeleton loader', () => { triggerInfiniteScroll(); @@ -324,11 +343,13 @@ describe('Job table app', () => { createComponent(); jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn()); + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']); expect(createAlert).toHaveBeenCalledWith(expectedWarning); expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); }); it('updates URL query string when filtering jobs by status', async () => { diff --git a/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js b/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js new file mode 100644 index 00000000000..2f76ad66dd5 --- /dev/null +++ b/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js @@ -0,0 +1,64 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerCell from '~/pages/admin/jobs/components/table/cells/runner_cell.vue'; +import { RUNNER_EMPTY_TEXT } from '~/pages/admin/jobs/components/constants'; +import { allRunnersData } from '../../../../../../ci/runner/mock_data'; + +const mockRunner = allRunnersData.data.runners.nodes[0]; + +const mockJobWithRunner = { + id: 'gid://gitlab/Ci::Build/2264', + runner: mockRunner, +}; + +const mockJobWithoutRunner = { + id: 'gid://gitlab/Ci::Build/2265', +}; + +describe('Runner Cell', () => { + let wrapper; + + const findRunnerLink = () => wrapper.findComponent(GlLink); + const findEmptyRunner = () => wrapper.find('[data-testid="empty-runner-text"]'); + + const createComponent = (props = {}) => { + wrapper = shallowMount(RunnerCell, { + propsData: { + ...props, + }, + }); + }; + + describe('Runner Link', () => { + describe('Job with runner', () => { + beforeEach(() => { + createComponent({ job: mockJobWithRunner }); + }); + + it('shows and links to the runner', () => { + expect(findRunnerLink().exists()).toBe(true); + expect(findRunnerLink().text()).toBe(mockRunner.description); + expect(findRunnerLink().attributes('href')).toBe(mockRunner.adminUrl); + }); + + it('hides the empty runner text', () => { + expect(findEmptyRunner().exists()).toBe(false); + }); + }); + + describe('Job without runner', () => { + beforeEach(() => { + createComponent({ job: mockJobWithoutRunner }); + }); + + it('shows default `empty` text', () => { + expect(findEmptyRunner().exists()).toBe(true); + expect(findEmptyRunner().text()).toBe(RUNNER_EMPTY_TEXT); + }); + + it('hides the runner link', () => { + expect(findRunnerLink().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js new file mode 100644 index 00000000000..047dc9a6599 --- /dev/null +++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js @@ -0,0 +1,207 @@ +import { mount } from '@vue/test-utils'; +import { + SUPER_SIDEBAR_PEEK_OPEN_DELAY, + SUPER_SIDEBAR_PEEK_CLOSE_DELAY, +} from '~/super_sidebar/constants'; +import SidebarPeek, { + STATE_CLOSED, + STATE_WILL_OPEN, + STATE_OPEN, + STATE_WILL_CLOSE, +} from '~/super_sidebar/components/sidebar_peek_behavior.vue'; + +// These are measured at runtime in the browser, but statically defined here +// since Jest does not do layout/styling. +const X_NEAR_WINDOW_EDGE = 5; +const X_SIDEBAR_EDGE = 10; +const X_AWAY_FROM_SIDEBAR = 20; + +jest.mock('~/lib/utils/css_utils', () => ({ + getCssClassDimensions: (className) => { + if (className === 'gl-w-3') { + return { width: X_NEAR_WINDOW_EDGE }; + } + + if (className === 'super-sidebar') { + return { width: X_SIDEBAR_EDGE }; + } + + throw new Error(`No mock for CSS class ${className}`); + }, +})); + +describe('SidebarPeek component', () => { + let wrapper; + + const createComponent = () => { + wrapper = mount(SidebarPeek); + }; + + const moveMouse = (clientX) => { + const event = new MouseEvent('mousemove', { + clientX, + }); + + document.dispatchEvent(event); + }; + + const moveMouseOutOfDocument = () => { + const event = new MouseEvent('mouseleave'); + document.documentElement.dispatchEvent(event); + }; + + const lastNChangeEvents = (n = 1) => wrapper.emitted('change').slice(-n).flat(); + + beforeEach(() => { + createComponent(); + }); + + it('begins in the closed state', () => { + expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED]); + }); + + it('does not emit duplicate events in a region', () => { + moveMouse(0); + moveMouse(1); + moveMouse(2); + + expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED, STATE_WILL_OPEN]); + }); + + it('transitions to will-open when in peek region', () => { + moveMouse(X_NEAR_WINDOW_EDGE); + + expect(lastNChangeEvents(1)).toEqual([STATE_CLOSED]); + + moveMouse(X_NEAR_WINDOW_EDGE - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]); + }); + + it('transitions will-open -> open after delay', () => { + moveMouse(0); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]); + + jest.advanceTimersByTime(1); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]); + }); + + it('cancels transition will-open -> open if mouse out of peek region', () => { + moveMouse(0); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1); + + moveMouse(X_NEAR_WINDOW_EDGE); + + jest.runOnlyPendingTimers(); + + expect(lastNChangeEvents(3)).toEqual([STATE_CLOSED, STATE_WILL_OPEN, STATE_CLOSED]); + }); + + it('transitions open -> will-close if mouse out of sidebar region', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_SIDEBAR_EDGE - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]); + + moveMouse(X_SIDEBAR_EDGE); + + expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]); + }); + + it('transitions will-close -> closed after delay', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_SIDEBAR_EDGE); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]); + + jest.advanceTimersByTime(1); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]); + }); + + it('cancels transition will-close -> close if mouse move in sidebar region', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_SIDEBAR_EDGE); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]); + + moveMouse(X_SIDEBAR_EDGE - 1); + jest.runOnlyPendingTimers(); + + expect(lastNChangeEvents(3)).toEqual([STATE_OPEN, STATE_WILL_CLOSE, STATE_OPEN]); + }); + + it('immediately transitions open -> closed if mouse moves far away', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_AWAY_FROM_SIDEBAR); + + expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_CLOSED]); + }); + + it('immediately transitions will-close -> closed if mouse moves far away', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_AWAY_FROM_SIDEBAR - 1); + moveMouse(X_AWAY_FROM_SIDEBAR); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]); + }); + + it('cleans up its mousemove listener before destroy', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + wrapper.destroy(); + moveMouse(X_AWAY_FROM_SIDEBAR); + + expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]); + }); + + it('cleans up its timers before destroy', () => { + moveMouse(0); + + wrapper.destroy(); + jest.runOnlyPendingTimers(); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]); + }); + + it('transitions will-open -> closed if cursor leaves document', () => { + moveMouse(0); + moveMouseOutOfDocument(); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_CLOSED]); + }); + + it('transitions open -> will-close if cursor leaves document', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + moveMouseOutOfDocument(); + + expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]); + }); + + it('cleans up document mouseleave listener before destroy', () => { + moveMouse(0); + + wrapper.destroy(); + + moveMouseOutOfDocument(); + + expect(lastNChangeEvents(1)).not.toEqual([STATE_CLOSED]); + }); +}); diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js index c3921e0a939..b76c637caf4 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js @@ -4,13 +4,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue'; import HelpCenter from '~/super_sidebar/components/help_center.vue'; import UserBar from '~/super_sidebar/components/user_bar.vue'; +import SidebarPeekBehavior, { + STATE_CLOSED, + STATE_WILL_OPEN, + STATE_OPEN, + STATE_WILL_CLOSE, +} from '~/super_sidebar/components/sidebar_peek_behavior.vue'; import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue'; import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue'; import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue'; -import { - SUPER_SIDEBAR_PEEK_OPEN_DELAY, - SUPER_SIDEBAR_PEEK_CLOSE_DELAY, -} from '~/super_sidebar/constants'; +import { sidebarState } from '~/super_sidebar/constants'; import { toggleSuperSidebarCollapsed, isCollapsed, @@ -18,6 +21,8 @@ import { import { stubComponent } from 'helpers/stub_component'; import { sidebarData as mockSidebarData } from '../mock_data'; +const initialSidebarState = { ...sidebarState }; + jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager'); const closeContextSwitcherMock = jest.fn(); @@ -28,16 +33,19 @@ const TrialStatusPopoverStub = { template: `<div data-testid="${trialStatusPopoverStubTestId}" />`, }; +const peekClass = 'super-sidebar-peek'; +const peekHintClass = 'super-sidebar-peek-hint'; + describe('SuperSidebar component', () => { let wrapper; const findSidebar = () => wrapper.findByTestId('super-sidebar'); - const findHoverArea = () => wrapper.findByTestId('super-sidebar-hover-area'); const findUserBar = () => wrapper.findComponent(UserBar); const findContextSwitcher = () => wrapper.findComponent(ContextSwitcher); const findNavContainer = () => wrapper.findByTestId('nav-container'); const findHelpCenter = () => wrapper.findComponent(HelpCenter); const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget); + const findPeekBehavior = () => wrapper.findComponent(SidebarPeekBehavior); const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId); const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId); const findSidebarMenu = () => wrapper.findComponent(SidebarMenu); @@ -45,14 +53,11 @@ describe('SuperSidebar component', () => { const createWrapper = ({ provide = {}, sidebarData = mockSidebarData, - sidebarState = {}, + sidebarState: state = {}, } = {}) => { + Object.assign(sidebarState, state); + wrapper = shallowMountExtended(SuperSidebar, { - data() { - return { - ...sidebarState, - }; - }, provide: { showTrialStatusWidget: false, ...provide, @@ -70,6 +75,10 @@ describe('SuperSidebar component', () => { }); }; + beforeEach(() => { + Object.assign(sidebarState, initialSidebarState); + }); + describe('default', () => { it('adds inert attribute when collapsed', () => { createWrapper({ sidebarState: { isCollapsed: true } }); @@ -154,12 +163,18 @@ describe('SuperSidebar component', () => { expect(findTrialStatusWidget().exists()).toBe(false); expect(findTrialStatusPopover().exists()).toBe(false); }); + + it('does not have peek behavior', () => { + createWrapper(); + + expect(findPeekBehavior().exists()).toBe(false); + }); }); describe('on collapse', () => { beforeEach(() => { createWrapper(); - wrapper.vm.isCollapsed = true; + sidebarState.isCollapsed = true; }); it('closes the context switcher', () => { @@ -167,91 +182,39 @@ describe('SuperSidebar component', () => { }); }); - describe('when peeking on hover', () => { - const peekClass = 'super-sidebar-peek'; - - it('updates inert attribute and peek class', async () => { - createWrapper({ - provide: { glFeatures: { superSidebarPeek: true } }, - sidebarState: { isCollapsed: true }, - }); + describe('peek behavior', () => { + it(`initially makes sidebar inert and peekable (${STATE_CLOSED})`, () => { + createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } }); - findHoverArea().trigger('mouseenter'); - - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1); - await nextTick(); - - // Not quite enough time has elapsed yet for sidebar to open - expect(findSidebar().classes()).not.toContain(peekClass); expect(findSidebar().attributes('inert')).toBe('inert'); + expect(findSidebar().classes()).not.toContain(peekHintClass); + expect(findSidebar().classes()).not.toContain(peekClass); + }); - jest.advanceTimersByTime(1); - await nextTick(); - - // Exactly enough time has elapsed to open - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - // Important: assume the cursor enters the sidebar - findSidebar().trigger('mouseenter'); - - jest.runAllTimers(); - await nextTick(); - - // Sidebar remains peeked open indefinitely without a mouseleave - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - findSidebar().trigger('mouseleave'); - - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); - await nextTick(); - - // Not quite enough time has elapsed yet for sidebar to hide - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); + it(`makes sidebar inert and shows peek hint when peek state is ${STATE_WILL_OPEN}`, async () => { + createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } }); - jest.advanceTimersByTime(1); + findPeekBehavior().vm.$emit('change', STATE_WILL_OPEN); await nextTick(); - // Exactly enough time has elapsed for sidebar to hide - expect(findSidebar().classes()).not.toContain('super-sidebar-peek'); expect(findSidebar().attributes('inert')).toBe('inert'); + expect(findSidebar().classes()).toContain(peekHintClass); + expect(findSidebar().classes()).not.toContain(peekClass); }); - it('eventually closes the sidebar if cursor never enters sidebar', async () => { - createWrapper({ - provide: { glFeatures: { superSidebarPeek: true } }, - sidebarState: { isCollapsed: true }, - }); + it.each([STATE_OPEN, STATE_WILL_CLOSE])( + 'makes sidebar interactive and visible when peek state is %s', + async (state) => { + createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } }); - findHoverArea().trigger('mouseenter'); + findPeekBehavior().vm.$emit('change', state); + await nextTick(); - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY); - await nextTick(); - - // Sidebar is now open - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - // Important: do *not* fire a mouseenter event on the sidebar here. This - // imitates what happens if the cursor moves away from the sidebar before - // it actually appears. - - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); - await nextTick(); - - // Not quite enough time has elapsed yet for sidebar to hide - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - jest.advanceTimersByTime(1); - await nextTick(); - - // Exactly enough time has elapsed for sidebar to hide - expect(findSidebar().classes()).not.toContain('super-sidebar-peek'); - expect(findSidebar().attributes('inert')).toBe('inert'); - }); + expect(findSidebar().attributes('inert')).toBe(undefined); + expect(findSidebar().classes()).toContain(peekClass); + expect(findSidebar().classes()).not.toContain(peekHintClass); + }, + ); }); describe('nav container', () => { diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js index 4028d91c82f..909f4249e28 100644 --- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js +++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js @@ -42,24 +42,28 @@ describe('Super Sidebar Collapsed State Manager', () => { describe('toggleSuperSidebarCollapsed', () => { it.each` - collapsed | saveCookie | windowWidth | hasClass - ${true} | ${true} | ${xl} | ${true} - ${true} | ${false} | ${xl} | ${true} - ${true} | ${true} | ${sm} | ${true} - ${true} | ${false} | ${sm} | ${true} - ${false} | ${true} | ${xl} | ${false} - ${false} | ${false} | ${xl} | ${false} - ${false} | ${true} | ${sm} | ${false} - ${false} | ${false} | ${sm} | ${false} + collapsed | saveCookie | windowWidth | hasClass | superSidebarPeek | isPeekable + ${true} | ${true} | ${xl} | ${true} | ${false} | ${false} + ${true} | ${true} | ${xl} | ${true} | ${true} | ${true} + ${true} | ${false} | ${xl} | ${true} | ${false} | ${false} + ${true} | ${true} | ${sm} | ${true} | ${false} | ${false} + ${true} | ${false} | ${sm} | ${true} | ${false} | ${false} + ${false} | ${true} | ${xl} | ${false} | ${false} | ${false} + ${false} | ${true} | ${xl} | ${false} | ${true} | ${false} + ${false} | ${false} | ${xl} | ${false} | ${false} | ${false} + ${false} | ${true} | ${sm} | ${false} | ${false} | ${false} + ${false} | ${false} | ${sm} | ${false} | ${false} | ${false} `( 'when collapsed is $collapsed, saveCookie is $saveCookie, and windowWidth is $windowWidth then page class contains `page-with-super-sidebar-collapsed` is $hasClass', - ({ collapsed, saveCookie, windowWidth, hasClass }) => { + ({ collapsed, saveCookie, windowWidth, hasClass, superSidebarPeek, isPeekable }) => { jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth); + gon.features = { superSidebarPeek }; toggleSuperSidebarCollapsed(collapsed, saveCookie); pageHasCollapsedClass(hasClass); expect(sidebarState.isCollapsed).toBe(collapsed); + expect(sidebarState.isPeekable).toBe(isPeekable); if (saveCookie && windowWidth >= xl) { expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, collapsed, { diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index dec2327db0f..d32e148ef79 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -62,10 +62,19 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }, }); }; + + const ContentEditorStub = stubComponent(ContentEditor); + const findMarkdownField = () => wrapper.findComponent(MarkdownField); const findTextarea = () => wrapper.find('textarea'); const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - const findContentEditor = () => wrapper.findComponent(ContentEditor); + const findContentEditor = () => { + const result = wrapper.findComponent(ContentEditor); + + // In Vue.js 3 there are nuances stubbing component with custom stub on mount + // So we try to search for stub also + return result.exists() ? result : wrapper.findComponent(ContentEditorStub); + }; const enableContentEditor = async () => { findMarkdownField().vm.$emit('enableContentEditor'); @@ -185,7 +194,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { it('autosizes the textarea when the value changes', async () => { buildWrapper(); await findTextarea().setValue('Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines'); - + await nextTick(); expect(Autosize.update).toHaveBeenCalled(); }); @@ -276,7 +285,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { it(`emits ${EDITING_MODE_MARKDOWN_FIELD} event when enableMarkdownEditor emitted from content editor`, async () => { buildWrapper({ - stubs: { ContentEditor: stubComponent(ContentEditor) }, + stubs: { ContentEditor: ContentEditorStub }, }); await enableContentEditor(); @@ -383,7 +392,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { beforeEach(() => { buildWrapper({ propsData: { autofocus: true }, - stubs: { ContentEditor: stubComponent(ContentEditor) }, + stubs: { ContentEditor: ContentEditorStub }, }); }); |