Commit 4a05b69d authored by Jay Swain's avatar Jay Swain

Add whats_new version tabs for self-managed

This commit adds in the version specific tabs to the "whats new" drawer
for self-managed instances.

The WhatsNewController now returns data from 2 different parameters,
version OR page. The result is now a Struct that has attributes, instead
of a Hash.

part of:
https://gitlab.com/gitlab-org/growth/engineering/-/issues/5388
parent dcdd3d04
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { import {
GlDrawer, GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll, GlInfiniteScroll,
GlResizeObserverDirective, GlResizeObserverDirective,
GlTabs,
GlTab,
GlBadge,
GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import SkeletonLoader from './skeleton_loader.vue'; import SkeletonLoader from './skeleton_loader.vue';
import Feature from './feature.vue';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height'; import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
...@@ -17,11 +19,13 @@ const trackingMixin = Tracking.mixin(); ...@@ -17,11 +19,13 @@ const trackingMixin = Tracking.mixin();
export default { export default {
components: { components: {
GlDrawer, GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll, GlInfiniteScroll,
GlTabs,
GlTab,
SkeletonLoader, SkeletonLoader,
Feature,
GlBadge,
GlLoadingIcon,
}, },
directives: { directives: {
GlResizeObserver: GlResizeObserverDirective, GlResizeObserver: GlResizeObserverDirective,
...@@ -31,11 +35,19 @@ export default { ...@@ -31,11 +35,19 @@ export default {
storageKey: { storageKey: {
type: String, type: String,
required: true, required: true,
default: null, },
versions: {
type: Array,
required: true,
},
gitlabDotCom: {
type: Boolean,
required: false,
default: false,
}, },
}, },
computed: { computed: {
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight']), ...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']),
}, },
mounted() { mounted() {
this.openDrawer(this.storageKey); this.openDrawer(this.storageKey);
...@@ -49,14 +61,25 @@ export default { ...@@ -49,14 +61,25 @@ export default {
methods: { methods: {
...mapActions(['openDrawer', 'closeDrawer', 'fetchItems', 'setDrawerBodyHeight']), ...mapActions(['openDrawer', 'closeDrawer', 'fetchItems', 'setDrawerBodyHeight']),
bottomReached() { bottomReached() {
if (this.pageInfo.nextPage) { const page = this.pageInfo.nextPage;
this.fetchItems(this.pageInfo.nextPage); if (page) {
this.fetchItems({ page });
} }
}, },
handleResize() { handleResize() {
const height = getDrawerBodyHeight(this.$refs.drawer.$el); const height = getDrawerBodyHeight(this.$refs.drawer.$el);
this.setDrawerBodyHeight(height); this.setDrawerBodyHeight(height);
}, },
featuresForVersion(version) {
return this.features.filter(feature => {
return feature.release === parseFloat(version);
});
},
fetchVersion(version) {
if (this.featuresForVersion(version).length === 0) {
this.fetchItems({ version });
}
},
}, },
}; };
</script> </script>
...@@ -73,64 +96,39 @@ export default { ...@@ -73,64 +96,39 @@ export default {
<template #header> <template #header>
<h4 class="page-title gl-my-2">{{ __("What's new at GitLab") }}</h4> <h4 class="page-title gl-my-2">{{ __("What's new at GitLab") }}</h4>
</template> </template>
<gl-infinite-scroll <template v-if="features.length">
v-if="features.length" <gl-infinite-scroll
:fetched-items="features.length" v-if="gitlabDotCom"
:max-list-height="drawerBodyHeight" :fetched-items="features.length"
class="gl-p-0" :max-list-height="drawerBodyHeight"
@bottomReached="bottomReached" class="gl-p-0"
> @bottomReached="bottomReached"
<template #items> >
<div <template #items>
v-for="feature in features" <feature v-for="feature in features" :key="feature.title" :feature="feature" />
:key="feature.title" </template>
class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" </gl-infinite-scroll>
<gl-tabs v-else :style="{ height: `${drawerBodyHeight}px` }" class="gl-p-0">
<gl-tab
v-for="(version, index) in versions"
:key="version"
@click="fetchVersion(version)"
> >
<gl-link <template #title>
:href="feature.url" <span>{{ version }}</span>
target="_blank" <gl-badge v-if="index === 0">{{ __('Your Version') }}</gl-badge>
class="whats-new-item-title-link" </template>
data-track-event="click_whats_new_item" <gl-loading-icon v-if="fetching" size="lg" class="text-center" />
:data-track-label="feature.title" <template v-else>
:data-track-property="feature.url" <feature
> v-for="feature in featuresForVersion(version)"
<h5 class="gl-font-lg">{{ feature.title }}</h5> :key="feature.title"
</gl-link> :feature="feature"
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="package_name in feature.packages"
:key="package_name"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ package_name }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/> />
</gl-link> </template>
<p class="gl-pt-3">{{ feature.body }}</p> </gl-tab>
<gl-link </gl-tabs>
:href="feature.url" </template>
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>
</gl-infinite-scroll>
<div v-else class="gl-mt-5"> <div v-else class="gl-mt-5">
<skeleton-loader /> <skeleton-loader />
<skeleton-loader /> <skeleton-loader />
......
<script>
import { GlBadge, GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlBadge,
GlIcon,
GlLink,
},
props: {
feature: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<gl-link
:href="feature.url"
target="_blank"
class="whats-new-item-title-link"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<h5 class="gl-font-lg" data-test-id="feature-title">{{ feature.title }}</h5>
</gl-link>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="packageName in feature.packages"
:key="packageName"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ packageName }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/>
</gl-link>
<p class="gl-pt-3">{{ feature.body }}</p>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>
...@@ -10,8 +10,6 @@ export default el => { ...@@ -10,8 +10,6 @@ export default el => {
if (whatsNewApp) { if (whatsNewApp) {
store.dispatch('openDrawer'); store.dispatch('openDrawer');
} else { } else {
const storageKey = getStorageKey(el);
whatsNewApp = new Vue({ whatsNewApp = new Vue({
el, el,
store, store,
...@@ -28,7 +26,11 @@ export default el => { ...@@ -28,7 +26,11 @@ export default el => {
}, },
render(createElement) { render(createElement) {
return createElement('app', { return createElement('app', {
props: { storageKey }, props: {
storageKey: getStorageKey(el),
versions: JSON.parse(el.getAttribute('data-versions')),
gitlabDotCom: el.getAttribute('data-gitlab-dot-com'),
},
}); });
}, },
}); });
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
localStorage.setItem(storageKey, JSON.stringify(false)); localStorage.setItem(storageKey, JSON.stringify(false));
} }
}, },
fetchItems({ commit, state }, page) { fetchItems({ commit, state }, { page, version } = { page: null, version: null }) {
if (state.fetching) { if (state.fetching) {
return false; return false;
} }
...@@ -24,6 +24,7 @@ export default { ...@@ -24,6 +24,7 @@ export default {
.get('/-/whats_new', { .get('/-/whats_new', {
params: { params: {
page, page,
version,
}, },
}) })
.then(({ data, headers }) => { .then(({ data, headers }) => {
......
...@@ -6,6 +6,32 @@ ...@@ -6,6 +6,32 @@
.gl-infinite-scroll-legend { .gl-infinite-scroll-legend {
@include gl-display-none; @include gl-display-none;
} }
.gl-tabs {
@include gl-overflow-y-auto;
}
.gl-tabs-nav {
flex-wrap: nowrap;
overflow-x: scroll;
align-items: stretch;
.nav-item {
@include gl-flex-shrink-0;
a {
@include gl-h-full;
line-height: 1.5;
}
}
}
.gl-spinner-container {
@include gl-w-full;
@include gl-absolute;
top: 50%;
transform: translateY(-50%);
}
} }
.with-performance-bar .whats-new-drawer { .with-performance-bar .whats-new-drawer {
......
# frozen_string_literal: true # frozen_string_literal: true
class WhatsNewController < ApplicationController class WhatsNewController < ApplicationController
include Gitlab::Utils::StrongMemoize
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :check_feature_flag, :check_valid_page_param, :set_pagination_headers before_action :check_feature_flag
before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? }
feature_category :navigation feature_category :navigation
def index def index
respond_to do |format| respond_to do |format|
format.js do format.js do
render json: most_recent_items render json: highlight_items
end end
end end
end end
...@@ -29,15 +32,25 @@ class WhatsNewController < ApplicationController ...@@ -29,15 +32,25 @@ class WhatsNewController < ApplicationController
params[:page]&.to_i || 1 params[:page]&.to_i || 1
end end
def most_recent def highlights
@most_recent ||= ReleaseHighlight.paginated(page: current_page) strong_memoize(:highlights) do
if has_version_param?
ReleaseHighlight.for_version(version: params[:version])
else
ReleaseHighlight.paginated(page: current_page)
end
end
end end
def most_recent_items def highlight_items
most_recent[:items].map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) } highlights.map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) }
end end
def set_pagination_headers def set_pagination_headers
response.set_header('X-Next-Page', most_recent[:next_page]) response.set_header('X-Next-Page', highlights.next_page)
end
def has_version_param?
params[:version].present?
end end
end end
...@@ -6,10 +6,14 @@ module WhatsNewHelper ...@@ -6,10 +6,14 @@ module WhatsNewHelper
end end
def whats_new_storage_key def whats_new_storage_key
most_recent_version = ReleaseHighlight.most_recent_version most_recent_version = ReleaseHighlight.versions&.first
return unless most_recent_version return unless most_recent_version
['display-whats-new-notification', most_recent_version].join('-') ['display-whats-new-notification', most_recent_version].join('-')
end end
def whats_new_versions
ReleaseHighlight.versions
end
end end
...@@ -3,6 +3,17 @@ ...@@ -3,6 +3,17 @@
class ReleaseHighlight class ReleaseHighlight
CACHE_DURATION = 1.hour CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
RELEASE_VERSIONS_IN_A_YEAR = 12
def self.for_version(version:)
index = self.versions.index(version)
return if index.nil?
page = index + 1
self.paginated(page: page)
end
def self.paginated(page: 1) def self.paginated(page: 1)
Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do
...@@ -10,10 +21,7 @@ class ReleaseHighlight ...@@ -10,10 +21,7 @@ class ReleaseHighlight
next if items.nil? next if items.nil?
{ QueryResult.new(items: items, next_page: next_page(current_page: page))
items: items,
next_page: next_page(current_page: page)
}
end end
end end
...@@ -53,15 +61,25 @@ class ReleaseHighlight ...@@ -53,15 +61,25 @@ class ReleaseHighlight
next_page if self.file_paths[next_index] next_page if self.file_paths[next_index]
end end
def self.most_recent_version def self.most_recent_item_count
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:release_version', expires_in: CACHE_DURATION) do Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.first&.[]('release') self.paginated&.items&.count
end end
end end
def self.most_recent_item_count def self.versions
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:versions', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.count versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path|
/\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".")
end
versions.uniq
end end
end end
QueryResult = Struct.new(:items, :next_page, keyword_init: true) do
include Enumerable
delegate :each, to: :items
end
end end
...@@ -102,7 +102,7 @@ ...@@ -102,7 +102,7 @@
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if ::Feature.enabled?(:whats_new_drawer, current_user) - if ::Feature.enabled?(:whats_new_drawer, current_user)
#whats-new-app{ data: { storage_key: whats_new_storage_key } } #whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } }
- if can?(current_user, :update_user_status, current_user) - if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: user_status_data } .js-set-status-modal-wrapper{ data: user_status_data }
...@@ -31714,6 +31714,9 @@ msgstr "" ...@@ -31714,6 +31714,9 @@ msgstr ""
msgid "Your U2F device was registered!" msgid "Your U2F device was registered!"
msgstr "" msgstr ""
msgid "Your Version"
msgstr ""
msgid "Your WebAuthn device did not send a valid JSON response." msgid "Your WebAuthn device did not send a valid JSON response."
msgstr "" msgstr ""
......
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlDrawer, GlInfiniteScroll } from '@gitlab/ui'; import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import App from '~/whats_new/components/app.vue'; import App from '~/whats_new/components/app.vue';
...@@ -16,12 +16,18 @@ const localVue = createLocalVue(); ...@@ -16,12 +16,18 @@ const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
describe('App', () => { describe('App', () => {
const propsData = { storageKey: 'storage-key' };
let wrapper; let wrapper;
let store; let store;
let actions; let actions;
let state; let state;
let trackingSpy; let trackingSpy;
let gitlabDotCom = true;
const buildProps = () => ({
storageKey: 'storage-key',
versions: ['3.11', '3.10'],
gitlabDotCom,
});
const buildWrapper = () => { const buildWrapper = () => {
actions = { actions = {
...@@ -45,7 +51,7 @@ describe('App', () => { ...@@ -45,7 +51,7 @@ describe('App', () => {
wrapper = mount(App, { wrapper = mount(App, {
localVue, localVue,
store, store,
propsData, propsData: buildProps(),
directives: { directives: {
GlResizeObserver: createMockDirective(), GlResizeObserver: createMockDirective(),
}, },
...@@ -53,112 +59,171 @@ describe('App', () => { ...@@ -53,112 +59,171 @@ describe('App', () => {
}; };
const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll); const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll);
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
beforeEach(async () => { const setup = async () => {
document.body.dataset.page = 'test-page'; document.body.dataset.page = 'test-page';
document.body.dataset.namespaceId = 'namespace-840'; document.body.dataset.namespaceId = 'namespace-840';
trackingSpy = mockTracking('_category_', null, jest.spyOn); trackingSpy = mockTracking('_category_', null, jest.spyOn);
buildWrapper(); buildWrapper();
wrapper.vm.$store.state.features = [{ title: 'Whats New Drawer', url: 'www.url.com' }]; wrapper.vm.$store.state.features = [
{ title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 },
];
wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT; wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT;
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
unmockTracking(); unmockTracking();
}); });
const getDrawer = () => wrapper.find(GlDrawer); describe('gitlab.com', () => {
beforeEach(() => {
setup();
});
it('contains a drawer', () => { const getDrawer = () => wrapper.find(GlDrawer);
expect(getDrawer().exists()).toBe(true);
});
it('dispatches openDrawer and tracking calls when mounted', () => { it('contains a drawer', () => {
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key'); expect(getDrawer().exists()).toBe(true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
label: 'namespace_id',
value: 'namespace-840',
}); });
});
it('dispatches closeDrawer when clicking close', () => { it('dispatches openDrawer and tracking calls when mounted', () => {
getDrawer().vm.$emit('close'); expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key');
expect(actions.closeDrawer).toHaveBeenCalled(); expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
}); label: 'namespace_id',
value: 'namespace-840',
});
});
it.each([true, false])('passes open property', async openState => { it('dispatches closeDrawer when clicking close', () => {
wrapper.vm.$store.state.open = openState; getDrawer().vm.$emit('close');
expect(actions.closeDrawer).toHaveBeenCalled();
});
await wrapper.vm.$nextTick(); it.each([true, false])('passes open property', async openState => {
wrapper.vm.$store.state.open = openState;
expect(getDrawer().props('open')).toBe(openState); await wrapper.vm.$nextTick();
});
it('renders features when provided via ajax', () => { expect(getDrawer().props('open')).toBe(openState);
expect(actions.fetchItems).toHaveBeenCalled(); });
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
});
it('send an event when feature item is clicked', () => { it('renders features when provided via ajax', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); expect(actions.fetchItems).toHaveBeenCalled();
expect(wrapper.find('[data-test-id="feature-title"]').text()).toBe('Whats New Drawer');
});
const link = wrapper.find('.whats-new-item-title-link'); it('send an event when feature item is clicked', () => {
triggerEvent(link.element); trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
expect(trackingSpy.mock.calls[1]).toMatchObject([ const link = wrapper.find('.whats-new-item-title-link');
'_category_', triggerEvent(link.element);
'click_whats_new_item',
{ expect(trackingSpy.mock.calls[1]).toMatchObject([
label: 'Whats New Drawer', '_category_',
property: 'www.url.com', 'click_whats_new_item',
}, {
]); label: 'Whats New Drawer',
}); property: 'www.url.com',
},
]);
});
it('renders infinite scroll', () => {
const scroll = findInfiniteScroll();
expect(scroll.props()).toMatchObject({
fetchedItems: wrapper.vm.$store.state.features.length,
maxListHeight: MOCK_DRAWER_BODY_HEIGHT,
});
});
describe('bottomReached', () => {
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
it('renders infinite scroll', () => { beforeEach(() => {
const scroll = findInfiniteScroll(); actions.fetchItems.mockClear();
});
expect(scroll.props()).toMatchObject({ it('when nextPage exists it calls fetchItems', () => {
fetchedItems: wrapper.vm.$store.state.features.length, wrapper.vm.$store.state.pageInfo = { nextPage: 840 };
maxListHeight: MOCK_DRAWER_BODY_HEIGHT, emitBottomReached();
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { page: 840 });
});
it('when nextPage does not exist it does not call fetchItems', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: null };
emitBottomReached();
expect(actions.fetchItems).not.toHaveBeenCalled();
});
});
it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => {
const { value } = getBinding(getDrawer().element, 'gl-resize-observer');
value();
expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element);
expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith(
expect.any(Object),
MOCK_DRAWER_BODY_HEIGHT,
);
}); });
}); });
describe('bottomReached', () => { describe('self managed', () => {
const findTabs = () => wrapper.find(GlTabs);
const clickSecondTab = async () => {
const secondTab = wrapper.findAll('.nav-link').at(1);
await secondTab.trigger('click');
await new Promise(resolve => requestAnimationFrame(resolve));
};
beforeEach(() => { beforeEach(() => {
actions.fetchItems.mockClear(); gitlabDotCom = false;
setup();
}); });
it('when nextPage exists it calls fetchItems', () => { it('renders tabs with drawer body height and content', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: 840 }; const scroll = findInfiniteScroll();
emitBottomReached(); const tabs = findTabs();
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), 840); expect(scroll.exists()).toBe(false);
expect(tabs.attributes().style).toBe(`height: ${MOCK_DRAWER_BODY_HEIGHT}px;`);
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
}); });
it('when nextPage does not exist it does not call fetchItems', () => { describe('fetchVersion', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: null }; beforeEach(() => {
emitBottomReached(); actions.fetchItems.mockClear();
});
expect(actions.fetchItems).not.toHaveBeenCalled(); it('when version isnt fetched, clicking a tab calls fetchItems', async () => {
}); const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
}); await clickSecondTab();
it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => { expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
const { value } = getBinding(getDrawer().element, 'gl-resize-observer'); expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { version: '3.10' });
});
value(); it('when version has been fetched, clicking a tab calls fetchItems', async () => {
wrapper.vm.$store.state.features.push({ title: 'GitLab Stories', release: 3.1 });
await wrapper.vm.$nextTick();
expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element); const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
await clickSecondTab();
expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith( expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
expect.any(Object), expect(actions.fetchItems).not.toHaveBeenCalled();
MOCK_DRAWER_BODY_HEIGHT, expect(wrapper.find('.tab-pane.active h5').text()).toBe('GitLab Stories');
); });
});
}); });
}); });
...@@ -41,6 +41,23 @@ describe('whats new actions', () => { ...@@ -41,6 +41,23 @@ describe('whats new actions', () => {
axiosMock.restore(); axiosMock.restore();
}); });
it('passes arguments', () => {
axiosMock.reset();
axiosMock
.onGet('/-/whats_new', { params: { page: 8, version: 40 } })
.replyOnce(200, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
{ page: 8, version: 40 },
{},
expect.arrayContaining([
{ type: types.ADD_FEATURES, payload: [{ title: 'GitLab Stories' }] },
]),
);
});
it('if already fetching, does not fetch', () => { it('if already fetching, does not fetch', () => {
testAction(actions.fetchItems, {}, { fetching: true }, []); testAction(actions.fetchItems, {}, { fetching: true }, []);
}); });
......
...@@ -10,7 +10,7 @@ RSpec.describe WhatsNewHelper do ...@@ -10,7 +10,7 @@ RSpec.describe WhatsNewHelper do
let(:release_item) { double(:item) } let(:release_item) { double(:item) }
before do before do
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(84.0) allow(ReleaseHighlight).to receive(:versions).and_return([84.0])
end end
it { is_expected.to eq('display-whats-new-notification-84.0') } it { is_expected.to eq('display-whats-new-notification-84.0') }
...@@ -18,7 +18,7 @@ RSpec.describe WhatsNewHelper do ...@@ -18,7 +18,7 @@ RSpec.describe WhatsNewHelper do
context 'when most recent release highlights do NOT exist' do context 'when most recent release highlights do NOT exist' do
before do before do
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(nil) allow(ReleaseHighlight).to receive(:versions).and_return(nil)
end end
it { is_expected.to be_nil } it { is_expected.to be_nil }
...@@ -44,4 +44,14 @@ RSpec.describe WhatsNewHelper do ...@@ -44,4 +44,14 @@ RSpec.describe WhatsNewHelper do
end end
end end
end end
describe '#whats_new_versions' do
let(:versions) { [84.0] }
it 'returns ReleaseHighlight.versions' do
expect(ReleaseHighlight).to receive(:versions).and_return(versions)
expect(helper.whats_new_versions).to eq(versions)
end
end
end end
...@@ -3,21 +3,44 @@ ...@@ -3,21 +3,44 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ReleaseHighlight do RSpec.describe ReleaseHighlight do
describe '#paginated' do let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } let(:cache_mock) { double(:cache_mock) }
let(:cache_mock) { double(:cache_mock) }
before do
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
allow(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end
describe '.for_version' do
subject { ReleaseHighlight.for_version(version: version) }
let(:version) { '1.1' }
context 'with version param that exists' do
it 'returns items from that version' do
expect(subject.items.first['title']).to eq("It's gonna be a bright")
end
end
context 'with version param that does NOT exist' do
let(:version) { '84.0' }
it 'returns nil' do
expect(subject).to be_nil
end
end
end
describe '.paginated' do
let(:dot_com) { false } let(:dot_com) { false }
before do before do
allow(Gitlab).to receive(:com?).and_return(dot_com) allow(Gitlab).to receive(:com?).and_return(dot_com)
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
expect(Rails).to receive(:cache).twice.and_return(cache_mock) expect(Rails).to receive(:cache).twice.and_return(cache_mock)
expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end end
context 'with page param' do context 'with page param' do
...@@ -90,46 +113,51 @@ RSpec.describe ReleaseHighlight do ...@@ -90,46 +113,51 @@ RSpec.describe ReleaseHighlight do
end end
end end
describe '.most_recent_version' do describe '.most_recent_item_count' do
subject { ReleaseHighlight.most_recent_version } subject { ReleaseHighlight.most_recent_item_count }
context 'when version exist' do context 'when recent release items exist' do
let(:release_item) { double(:item) } it 'returns the count from the most recent file' do
allow(ReleaseHighlight).to receive(:paginated).and_return(double(:paginated, items: [double(:item)]))
before do expect(subject).to eq(1)
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] })
allow(release_item).to receive(:[]).with('release').and_return(84.0)
end end
it { is_expected.to eq(84.0) }
end end
context 'when most recent release highlights do NOT exist' do context 'when recent release items do NOT exist' do
before do it 'returns nil' do
allow(ReleaseHighlight).to receive(:paginated).and_return(nil) allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
end
it { is_expected.to be_nil } expect(subject).to be_nil
end
end end
end end
describe '#most_recent_item_count' do describe '.versions' do
subject { ReleaseHighlight.most_recent_item_count } it 'returns versions from the file paths' do
expect(ReleaseHighlight.versions).to eq(['1.5', '1.2', '1.1'])
end
context 'when recent release items exist' do context 'when there are more than 12 versions' do
it 'returns the count from the most recent file' do let(:file_paths) do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] }) i = 0
Array.new(20) { "20201225_01_#{i += 1}.yml" }
end
expect(subject).to eq(1) it 'limits to 12 versions' do
allow(ReleaseHighlight).to receive(:file_paths).and_return(file_paths)
expect(ReleaseHighlight.versions.count).to eq(12)
end end
end end
end
context 'when recent release items do NOT exist' do describe 'QueryResult' do
it 'returns nil' do subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) }
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
expect(subject).to be_nil let(:items) { [:item] }
end
it 'responds to map' do
expect(subject.map(&:to_s)).to eq(items.map(&:to_s))
end end
end end
end end
...@@ -4,22 +4,22 @@ require 'spec_helper' ...@@ -4,22 +4,22 @@ require 'spec_helper'
RSpec.describe WhatsNewController do RSpec.describe WhatsNewController do
describe 'whats_new_path' do describe 'whats_new_path' do
let(:item) { double(:item) }
let(:highlights) { double(:highlight, items: [item], map: [item].map, next_page: 2) }
context 'with whats_new_drawer feature enabled' do context 'with whats_new_drawer feature enabled' do
before do before do
stub_feature_flags(whats_new_drawer: true) stub_feature_flags(whats_new_drawer: true)
end end
context 'with no page param' do context 'with no page param' do
let(:most_recent) { { items: [item], next_page: 2 } }
let(:item) { double(:item) }
it 'responds with paginated data and headers' do it 'responds with paginated data and headers' do
allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(most_recent) allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item) allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path, xhr: true get whats_new_path, xhr: true
expect(response.body).to eq(most_recent[:items].to_json) expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to eq(2) expect(response.headers['X-Next-Page']).to eq(2)
end end
end end
...@@ -37,6 +37,18 @@ RSpec.describe WhatsNewController do ...@@ -37,6 +37,18 @@ RSpec.describe WhatsNewController do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'with version param' do
it 'returns items without pagination headers' do
allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path(version: 42), xhr: true
expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to be_nil
end
end
end end
context 'with whats_new_drawer feature disabled' do context 'with whats_new_drawer feature disabled' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment