Commit ebaad2df authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'jswain_revert_revert_whats_new_tabs' into 'master'

Bust the cache for ReleaseHighlights

See merge request gitlab-org/gitlab!49276
parents 4d16d9b0 b954fcfd
...@@ -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,17 +3,27 @@ ...@@ -3,17 +3,27 @@
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 key = self.cache_key("items:page-#{page}")
Rails.cache.fetch(key, expires_in: CACHE_DURATION) do
items = self.load_items(page: page) items = self.load_items(page: page)
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
...@@ -36,14 +46,13 @@ class ReleaseHighlight ...@@ -36,14 +46,13 @@ class ReleaseHighlight
end end
def self.file_paths def self.file_paths
@file_paths ||= Rails.cache.fetch('release_highlight:file_paths', expires_in: CACHE_DURATION) do @file_paths ||= Rails.cache.fetch(self.cache_key('file_paths'), expires_in: CACHE_DURATION) do
Dir.glob(FILES_PATH).sort.reverse Dir.glob(FILES_PATH).sort.reverse
end end
end end
def self.cache_key(page) def self.cache_key(key)
filename = /\d*\_\d*\_\d*/.match(self.file_paths&.first) ['release_highlight', key, Gitlab.revision].join(':')
"release_highlight:items:file-#{filename}:page-#{page}"
end end
def self.next_page(current_page: 1) def self.next_page(current_page: 1)
...@@ -53,15 +62,29 @@ class ReleaseHighlight ...@@ -53,15 +62,29 @@ 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 key = self.cache_key('recent_item_count')
self.paginated&.[](:items)&.first&.[]('release')
Gitlab::ProcessMemoryCache.cache_backend.fetch(key, expires_in: CACHE_DURATION) do
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 key = self.cache_key('versions')
self.paginated&.[](:items)&.count
Gitlab::ProcessMemoryCache.cache_backend.fetch(key, expires_in: CACHE_DURATION) do
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 }
...@@ -31936,6 +31936,9 @@ msgstr "" ...@@ -31936,6 +31936,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,30 +3,46 @@ ...@@ -3,30 +3,46 @@
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(:dot_com) { false }
before do before do
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)
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end
describe '.for_version' do
subject { ReleaseHighlight.for_version(version: version) }
expect(Rails).to receive(:cache).twice.and_return(cache_mock) let(:version) { '1.1' }
expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
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
end
after do describe '.paginated' do
ReleaseHighlight.instance_variable_set(:@file_paths, nil) let(:dot_com) { false }
before do
allow(Gitlab).to receive(:com?).and_return(dot_com)
end end
context 'with page param' do context 'with page param' do
subject { ReleaseHighlight.paginated(page: page) } subject { ReleaseHighlight.paginated(page: page) }
before do
allow(cache_mock).to receive(:fetch).and_yield
end
context 'when there is another page of results' do context 'when there is another page of results' do
let(:page) { 2 } let(:page) { 2 }
...@@ -57,11 +73,14 @@ RSpec.describe ReleaseHighlight do ...@@ -57,11 +73,14 @@ RSpec.describe ReleaseHighlight do
context 'with no page param' do context 'with no page param' do
subject { ReleaseHighlight.paginated } subject { ReleaseHighlight.paginated }
before do it 'uses multiple levels of cache' do
expect(cache_mock).to receive(:fetch).with('release_highlight:items:file-20201225_01_05:page-1', expires_in: 1.hour).and_yield expect(Rails.cache).to receive(:fetch).with("release_highlight:items:page-1:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original
expect(Rails.cache).to receive(:fetch).with("release_highlight:file_paths:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original
subject
end end
it 'returns platform specific items and uses a cache key' do it 'returns platform specific items' do
expect(subject[:items].count).to eq(1) expect(subject[:items].count).to eq(1)
expect(subject[:items].first['title']).to eq("bright and sunshinin' day") expect(subject[:items].first['title']).to eq("bright and sunshinin' day")
expect(subject[:next_page]).to eq(2) expect(subject[:next_page]).to eq(2)
...@@ -90,35 +109,18 @@ RSpec.describe ReleaseHighlight do ...@@ -90,35 +109,18 @@ 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
let(:release_item) { double(:item) }
before do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] })
allow(release_item).to receive(:[]).with('release').and_return(84.0)
end
it { is_expected.to eq(84.0) } it 'uses process memory cache' do
end expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:recent_item_count:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
context 'when most recent release highlights do NOT exist' do subject
before do
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
end
it { is_expected.to be_nil }
end end
end
describe '#most_recent_item_count' do
subject { ReleaseHighlight.most_recent_item_count }
context 'when recent release items exist' do context 'when recent release items exist' do
it 'returns the count from the most recent file' do it 'returns the count from the most recent file' do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] }) allow(ReleaseHighlight).to receive(:paginated).and_return(double(:paginated, items: [double(:item)]))
expect(subject).to eq(1) expect(subject).to eq(1)
end end
...@@ -132,4 +134,40 @@ RSpec.describe ReleaseHighlight do ...@@ -132,4 +134,40 @@ RSpec.describe ReleaseHighlight do
end end
end end
end end
describe '.versions' do
subject { described_class.versions }
it 'uses process memory cache' do
expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:versions:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION })
subject
end
it 'returns versions from the file paths' do
expect(subject).to eq(['1.5', '1.2', '1.1'])
end
context 'when there are more than 12 versions' do
let(:file_paths) do
i = 0
Array.new(20) { "20201225_01_#{i += 1}.yml" }
end
it 'limits to 12 versions' do
allow(ReleaseHighlight).to receive(:file_paths).and_return(file_paths)
expect(subject.count).to eq(12)
end
end
end
describe 'QueryResult' do
subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) }
let(:items) { [:item] }
it 'responds to map' do
expect(subject.map(&:to_s)).to eq(items.map(&:to_s))
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