diff --git a/src/components/AdjustableSidebarWidth.vue b/src/components/AdjustableSidebarWidth.vue index c97544d7a..a7b46e605 100644 --- a/src/components/AdjustableSidebarWidth.vue +++ b/src/components/AdjustableSidebarWidth.vue @@ -157,6 +157,7 @@ export default { focusTrapInstance: null, mobileTopOffset: 0, topOffset: 0, + scrollLockContainer: null, }; }, computed: { @@ -353,18 +354,23 @@ export default { * Toggles the scroll lock on/off */ async toggleScrollLock(lock) { - const scrollLockContainer = document.getElementById(this.scrollLockID); - if (lock) { - await this.$nextTick(); - scrollLock.lockScroll(scrollLockContainer); - // lock focus - this.focusTrapInstance.start(); - // hide sibling elements from VO - changeElementVOVisibility.hide(this.$refs.aside); - } else { - scrollLock.unlockScroll(scrollLockContainer); + // if applicable, turn off lock on previous container + if (this.scrollLockContainer) { + scrollLock.unlockScroll(this.scrollLockContainer); this.focusTrapInstance.stop(); changeElementVOVisibility.show(this.$refs.aside); + this.scrollLockContainer = null; + } + if (lock) { + await this.$nextTick(); + this.scrollLockContainer = document.getElementById(this.scrollLockID); + if (this.scrollLockContainer) { + scrollLock.lockScroll(this.scrollLockContainer); + // lock focus + this.focusTrapInstance.start(); + // hide sibling elements from VO + changeElementVOVisibility.hide(this.$refs.aside); + } } }, storeTopOffset: throttle(function storeTopOffset() { diff --git a/src/components/DocumentationLayout.vue b/src/components/DocumentationLayout.vue index f53dfe20c..53a9c47dd 100644 --- a/src/components/DocumentationLayout.vue +++ b/src/components/DocumentationLayout.vue @@ -30,46 +30,48 @@ > - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + @@ -80,14 +82,14 @@ import { PortalTarget } from 'portal-vue'; import QuickNavigationButton from 'docc-render/components/Navigator/QuickNavigationButton.vue'; import QuickNavigationModal from 'docc-render/components/Navigator/QuickNavigationModal.vue'; -import AdjustableSidebarWidth from 'docc-render/components/AdjustableSidebarWidth.vue'; +import AdjustableSidebarWidth from 'theme/components/AdjustableSidebarWidth.vue'; import Navigator from 'docc-render/components/Navigator.vue'; import onPageLoadScrollToFragment from 'docc-render/mixins/onPageLoadScrollToFragment'; import { BreakpointName } from 'docc-render/utils/breakpoints'; import { storage } from 'docc-render/utils/storage'; import { getSetting } from 'docc-render/utils/theme-settings'; -import NavigatorDataProvider from 'theme/components/Navigator/NavigatorDataProvider.vue'; +import indexDataGetter from 'theme/mixins/indexDataGetter'; import DocumentationNav from 'theme/components/DocumentationTopic/DocumentationNav.vue'; const NAVIGATOR_HIDDEN_ON_LARGE_KEY = 'navigator-hidden-large'; @@ -98,13 +100,12 @@ export default { components: { Navigator, AdjustableSidebarWidth, - NavigatorDataProvider, Nav: DocumentationNav, QuickNavigationButton, QuickNavigationModal, PortalTarget, }, - mixins: [onPageLoadScrollToFragment], + mixins: [onPageLoadScrollToFragment, indexDataGetter], props: { enableNavigator: Boolean, diffAvailability: { diff --git a/src/components/DocumentationTopic.vue b/src/components/DocumentationTopic.vue index d56486b59..888d8018e 100644 --- a/src/components/DocumentationTopic.vue +++ b/src/components/DocumentationTopic.vue @@ -183,6 +183,7 @@ import { normalizeRelativePath } from 'docc-render/utils/assets'; import { last } from 'docc-render/utils/arrays'; import AppStore from 'docc-render/stores/AppStore'; +import IndexStore from 'docc-render/stores/IndexStore'; import Aside from 'docc-render/components/ContentNode/Aside.vue'; import BetaLegalText from 'theme/components/DocumentationTopic/BetaLegalText.vue'; import LanguageSwitcher from 'theme/components/DocumentationTopic/Summary/LanguageSwitcher.vue'; @@ -425,7 +426,7 @@ export default { }, data() { return { - appState: AppStore.state, + indexState: IndexStore.state, topicState: this.store.state, declListExpanded: false, // Hide all other declarations by default }; @@ -721,7 +722,7 @@ export default { this.store.setReferences(this.references); }, watch: { - 'appState.includedArchiveIdentifiers': function updateRefs() { + 'indexState.includedArchiveIdentifiers': function updateRefs() { this.store.updateReferences(); }, // update the references in the store, in case they update, but the component is not re-created diff --git a/src/components/Navigator.vue b/src/components/Navigator.vue index bbc069faf..52460b6c7 100644 --- a/src/components/Navigator.vue +++ b/src/components/Navigator.vue @@ -27,6 +27,9 @@ @close="$emit('close')" > + + + @@ -91,7 +94,7 @@ export default { type: Array, required: true, }, - technology: { + technologyProps: { type: Object, required: false, }, @@ -153,13 +156,6 @@ export default { * The root item is always a module */ type: () => TopicTypes.module, - technologyProps: ({ technology }) => ( - !technology ? null : { - technology: technology.title, - technologyPath: technology.path || technology.url, - isTechnologyBeta: technology.beta, - } - ), }, }; diff --git a/src/components/Navigator/NavigatorCard.vue b/src/components/Navigator/NavigatorCard.vue index e3a0d2a82..5b4199382 100644 --- a/src/components/Navigator/NavigatorCard.vue +++ b/src/components/Navigator/NavigatorCard.vue @@ -395,6 +395,9 @@ export default { item.updateSize(); } }, + technology() { + this.clearFilters(); + }, }, methods: { setUnlessEqual(property, data) { diff --git a/src/components/Navigator/NavigatorDataProvider.vue b/src/components/Navigator/NavigatorDataProvider.vue deleted file mode 100644 index 23c5c9d22..000000000 --- a/src/components/Navigator/NavigatorDataProvider.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - diff --git a/src/mixins/indexDataFetcher.js b/src/mixins/indexDataFetcher.js new file mode 100644 index 000000000..03af2327c --- /dev/null +++ b/src/mixins/indexDataFetcher.js @@ -0,0 +1,49 @@ +/** + * This source file is part of the Swift.org open source project + * + * Copyright (c) 2024 Apple Inc. and the Swift project authors + * Licensed under Apache License v2.0 with Runtime Library Exception + * + * See https://swift.org/LICENSE.txt for license information + * See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ +import { fetchData } from 'docc-render/utils/data'; +import { pathJoin } from 'docc-render/utils/assets'; +import { flattenNavigationIndex, extractTechnologyProps } from 'docc-render/utils/navigatorData'; +import IndexStore from 'docc-render/stores/IndexStore'; + +export default { + computed: { + indexDataPath() { + const slug = this.$route?.params?.locale || ''; + return pathJoin(['/index/', slug, 'index.json']); + }, + }, + methods: { + async fetchIndexPathsData() { + return fetchData(this.indexDataPath); + }, + async fetchIndexData() { + try { + IndexStore.reset(); + const { + includedArchiveIdentifiers = [], + interfaceLanguages, + references = {}, + } = await this.fetchIndexPathsData(); + IndexStore.setFlatChildren(flattenNavigationIndex(interfaceLanguages)); + IndexStore.setTechnologyProps(extractTechnologyProps(interfaceLanguages)); + IndexStore.setReferences(references); + IndexStore.setIncludedArchiveIdentifiers(includedArchiveIdentifiers); + } catch (e) { + IndexStore.setErrorFetching(true); + } + }, + }, + watch: { + indexDataPath: { + handler: 'fetchIndexData', + immediate: true, + }, + }, +}; diff --git a/src/mixins/indexDataGetter.js b/src/mixins/indexDataGetter.js new file mode 100644 index 000000000..c692f093b --- /dev/null +++ b/src/mixins/indexDataGetter.js @@ -0,0 +1,63 @@ +/** + * This source file is part of the Swift.org open source project + * + * Copyright (c) 2024 Apple Inc. and the Swift project authors + * Licensed under Apache License v2.0 with Runtime Library Exception + * + * See https://swift.org/LICENSE.txt for license information + * See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ +import IndexStore from 'docc-render/stores/IndexStore'; +import Language from 'docc-render/constants/Language'; + +export default { + computed: { + indexNodes({ indexState: { flatChildren }, interfaceLanguage }) { + if (!flatChildren) return []; + return flatChildren[interfaceLanguage] ?? (flatChildren[Language.swift.key.url] || []); + }, + apiChanges({ + indexState: { + apiChanges, + apiChangesVersion, + errorFetchingDiffs, + }, + interfaceLanguage, + selectedAPIChangesVersion, + }) { + if (!apiChanges || errorFetchingDiffs) return undefined; + return apiChangesVersion === selectedAPIChangesVersion + ? apiChanges[interfaceLanguage] || apiChanges[Language.swift.key.url] : undefined; + }, + technologyProps({ indexState: { technologyProps }, interfaceLanguage, technology }) { + // Select technology props from fetched index data for the current language, fallback to swift + // If none available, fallback to technology data of the curr page or null + return technologyProps[interfaceLanguage] ?? technologyProps[Language.swift.key.url] + ?? (technology ? { + technology: technology.title, + technologyPath: technology.path || technology.url, + isTechnologyBeta: technology.beta, + } : null); + }, + navigatorProps: ({ + indexNodes, + indexState: { + flatChildren, + references, + errorFetching, + }, + apiChanges, + }) => ({ + flatChildren: indexNodes, + navigatorReferences: references, + apiChanges, + isFetching: !flatChildren && !errorFetching, + errorFetching, + }), + }, + data() { + return { + indexState: IndexStore.state, + }; + }, +}; diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js index 76e15aa3a..ca8231183 100644 --- a/src/stores/AppStore.js +++ b/src/stores/AppStore.js @@ -30,7 +30,6 @@ export default { supportsAutoColorScheme, systemColorScheme: ColorScheme.light, availableLocales: [], - includedArchiveIdentifiers: [], }, reset() { this.state.imageLoadingStrategy = process.env.VUE_APP_TARGET === 'ide' @@ -38,7 +37,6 @@ export default { this.state.preferredColorScheme = Settings.preferredColorScheme || defaultColorScheme; this.state.supportsAutoColorScheme = supportsAutoColorScheme; this.state.systemColorScheme = ColorScheme.light; - this.state.includedArchiveIdentifiers = []; }, setImageLoadingStrategy(strategy) { this.state.imageLoadingStrategy = strategy; @@ -61,9 +59,6 @@ export default { setSystemColorScheme(value) { this.state.systemColorScheme = value; }, - setIncludedArchiveIdentifiers(value) { - this.state.includedArchiveIdentifiers = value; - }, syncPreferredColorScheme() { if (!!Settings.preferredColorScheme && Settings.preferredColorScheme !== this.state.preferredColorScheme) { diff --git a/src/stores/IndexStore.js b/src/stores/IndexStore.js new file mode 100644 index 000000000..39333424f --- /dev/null +++ b/src/stores/IndexStore.js @@ -0,0 +1,56 @@ +/** + * This source file is part of the Swift.org open source project + * + * Copyright (c) 2024 Apple Inc. and the Swift project authors + * Licensed under Apache License v2.0 with Runtime Library Exception + * + * See https://swift.org/LICENSE.txt for license information + * See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +export default { + state: { + flatChildren: null, + references: {}, + apiChanges: null, + apiChangesVersion: null, + includedArchiveIdentifiers: [], + errorFetching: false, + errorFetchingDiffs: false, + technologyProps: {}, + }, + reset() { + this.state.flatChildren = null; + this.state.references = {}; + this.state.apiChanges = null; + this.state.apiChangesVersion = null; + this.state.includedArchiveIdentifiers = []; + this.state.errorFetching = false; + this.state.errorFetchingDiffs = false; + this.state.technologyProps = {}; + }, + setFlatChildren(children) { + this.state.flatChildren = children; + }, + setReferences(references) { + this.state.references = references; + }, + setApiChanges(diff) { + this.state.apiChanges = diff; + }, + setApiChangesVersion(version) { + this.state.apiChangesVersion = version; + }, + setIncludedArchiveIdentifiers(includedArchiveIdentifiers) { + this.state.includedArchiveIdentifiers = includedArchiveIdentifiers; + }, + setErrorFetching(state) { + this.state.errorFetching = state; + }, + setErrorFetchingDiffs(state) { + this.state.errorFetchingDiffs = state; + }, + setTechnologyProps(technology) { + this.state.technologyProps = technology; + }, +}; diff --git a/src/utils/navigatorData.js b/src/utils/navigatorData.js index 3e47b9e23..b3c77109f 100644 --- a/src/utils/navigatorData.js +++ b/src/utils/navigatorData.js @@ -177,3 +177,30 @@ export function getSiblings(uid, childrenMap, children) { if (!item) return []; return getChildren(item.parent, childrenMap, children); } + +/** + * Flatten data for each language variant + * @return { languageVariant: NavigatorFlatItem[] } + */ +export function flattenNavigationIndex(indexData) { + return Object.entries(indexData).reduce((acc, [language, data]) => { + acc[language] = flattenNestedData( + data[0].children || [], null, 0, data[0].beta, + ); + return acc; + }, {}); +} + +/** + * Extract technology data for each language variant + */ +export function extractTechnologyProps(indexData) { + return Object.entries(indexData).reduce((acc, [language, data]) => { + acc[language] = { + technology: data[0].title, + technologyPath: data[0].path || data[0].url, + isTechnologyBeta: data[0].beta, + }; + return acc; + }, {}); +} diff --git a/src/utils/references.js b/src/utils/references.js index 1690a74c7..f8d7ed917 100644 --- a/src/utils/references.js +++ b/src/utils/references.js @@ -8,7 +8,7 @@ * See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ -import AppStore from 'docc-render/stores/AppStore'; +import IndexStore from 'docc-render/stores/IndexStore'; const TopicReferenceTypes = new Set([ 'section', @@ -16,7 +16,7 @@ const TopicReferenceTypes = new Set([ ]); function isFromIncludedArchive(id) { - const { includedArchiveIdentifiers } = AppStore.state; + const { includedArchiveIdentifiers } = IndexStore.state; // for backwards compatibility purposes, treat all references as being // from included archives if there is no data for it diff --git a/src/views/DocumentationTopic.vue b/src/views/DocumentationTopic.vue index 4ba8f7129..e33415ab9 100644 --- a/src/views/DocumentationTopic.vue +++ b/src/views/DocumentationTopic.vue @@ -52,8 +52,9 @@ import { shouldFetchDataForRouteUpdate, } from 'docc-render/utils/data'; import DocumentationTopic from 'theme/components/DocumentationTopic.vue'; -import DocumentationLayout from 'docc-render/components/DocumentationLayout.vue'; +import DocumentationLayout from 'theme/components/DocumentationLayout.vue'; import DocumentationTopicStore from 'docc-render/stores/DocumentationTopicStore'; +import indexDataFetcher from 'theme/mixins/indexDataFetcher'; import Language from 'docc-render/constants/Language'; import OnThisPageRegistrator from 'docc-render/mixins/onThisPageRegistrator'; import { updateLocale } from 'theme/utils/i18n-utils'; @@ -74,7 +75,7 @@ export default { Topic: DocumentationTopic, DocumentationLayout, }, - mixins: [OnThisPageRegistrator, communicationBridgeUtils], + mixins: [OnThisPageRegistrator, communicationBridgeUtils, indexDataFetcher], props: { enableMinimized: { type: Boolean, diff --git a/tests/unit/components/AdjustableSidebarWidth.spec.js b/tests/unit/components/AdjustableSidebarWidth.spec.js index 6e120fef3..9c27a116b 100644 --- a/tests/unit/components/AdjustableSidebarWidth.spec.js +++ b/tests/unit/components/AdjustableSidebarWidth.spec.js @@ -219,6 +219,7 @@ describe('AdjustableSidebarWidth', () => { // called once on mount, once now expect(boundingClientSpy).toHaveBeenCalledTimes(2); // assert scroll lock and other helpers initiated + expect(wrapper.vm.scrollLockContainer).toBe(scrollLockTarget); // save scroll lock target expect(scrollLock.lockScroll).toHaveBeenCalledWith(scrollLockTarget); expect(changeElementVOVisibility.hide).toHaveBeenCalledWith(aside.element); expect(FocusTrap.mock.results[0].value.start).toHaveBeenCalledTimes(1); @@ -229,6 +230,7 @@ describe('AdjustableSidebarWidth', () => { // assert class expect(aside.classes()).not.toContain('show-on-mobile'); // assert helper status + expect(wrapper.vm.scrollLockContainer).toBe(null); // clear scroll lock target expect(scrollLock.unlockScroll).toHaveBeenCalledWith(scrollLockTarget); expect(changeElementVOVisibility.show).toHaveBeenCalledWith(aside.element); expect(FocusTrap.mock.results[0].value.start).toHaveBeenCalledTimes(1); diff --git a/tests/unit/components/DocumentationLayout.spec.js b/tests/unit/components/DocumentationLayout.spec.js index 0eeb73a9d..754cc74f5 100644 --- a/tests/unit/components/DocumentationLayout.spec.js +++ b/tests/unit/components/DocumentationLayout.spec.js @@ -8,14 +8,12 @@ * See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ -import * as dataUtils from 'docc-render/utils/data'; import { shallowMount } from '@vue/test-utils'; import DocumentationTopicStore from 'docc-render/stores/DocumentationTopicStore'; import onPageLoadScrollToFragment from 'docc-render/mixins/onPageLoadScrollToFragment'; import DocumentationNav from 'docc-render/components/DocumentationTopic/DocumentationNav.vue'; import NavBase from 'docc-render/components/NavBase.vue'; import AdjustableSidebarWidth from '@/components/AdjustableSidebarWidth.vue'; -import NavigatorDataProvider from '@/components/Navigator/NavigatorDataProvider.vue'; import Language from '@/constants/Language'; import Navigator from '@/components/Navigator.vue'; import { storage } from '@/utils/storage'; @@ -30,21 +28,39 @@ jest.mock('docc-render/utils/scroll-lock'); jest.mock('docc-render/utils/storage'); jest.mock('docc-render/utils/theme-settings'); -storage.get.mockImplementation((key, value) => value); - -const TechnologyWithChildren = { - path: '/documentation/foo', - children: [], +const swiftChildren = [ + 'swiftChildrenMock', +]; +const objcChildren = [ + 'objcChildrenMock', +]; + +const swiftProps = { + technology: 'swift', + technologyPath: '/documentation/swift', + isTechnologyBeta: false, }; -const navigatorReferences = { foo: {} }; - -jest.spyOn(dataUtils, 'fetchIndexPathsData').mockResolvedValue({ - interfaceLanguages: { - [Language.swift.key.url]: [TechnologyWithChildren], +jest.mock('docc-render/stores/IndexStore', () => ({ + state: { + flatChildren: { + swift: swiftChildren, + }, + references: { foo: {} }, + apiChanges: { + interfaceLanguages: { + swift: [], + }, + }, + includedArchiveIdentifiers: ['foo', 'bar'], + errorFetching: false, + technologyProps: { + swift: swiftProps, + }, }, - references: navigatorReferences, -}); +})); + +storage.get.mockImplementation((key, value) => value); getSetting.mockReturnValue(false); const { @@ -106,7 +122,6 @@ const AdjustableSidebarWidthSmallStub = { const stubs = { AdjustableSidebarWidth, - NavigatorDataProvider, DocumentationLayout, }; @@ -160,45 +175,21 @@ describe('DocumentationLayout', () => { enableNavigator: true, fixedWidth: propsData.navigatorFixedWidth, }); - const { - technology, - parentTopicIdentifiers, - } = propsData; - expect(wrapper.find(NavigatorDataProvider).props()).toEqual({ - interfaceLanguage: Language.swift.key.url, - technologyUrl: technology.url, - apiChangesVersion: '', - }); + // its rendered by default const navigator = wrapper.find(Navigator); expect(navigator.exists()).toBe(true); expect(navigator.props()).toEqual({ - errorFetching: false, - isFetching: true, - // assert we are passing the first set of paths always - parentTopicIdentifiers, - references, - scrollLockID: AdjustableSidebarWidth.constants.SCROLL_LOCK_ID, - // assert we are passing the default technology, if we dont have the children yet - technology, - apiChanges: null, - flatChildren: [], - navigatorReferences: {}, - renderFilterOnTop: false, - }); - expect(dataUtils.fetchIndexPathsData).toHaveBeenCalledTimes(1); - await flushPromises(); - expect(navigator.props()).toEqual({ - errorFetching: false, isFetching: false, scrollLockID: AdjustableSidebarWidth.constants.SCROLL_LOCK_ID, renderFilterOnTop: false, - parentTopicIdentifiers, references, - technology: TechnologyWithChildren, apiChanges: null, - flatChildren: [], - navigatorReferences, + flatChildren: swiftChildren, + navigatorReferences: { foo: {} }, + errorFetching: false, + parentTopicIdentifiers: propsData.parentTopicIdentifiers, + technologyProps: swiftProps, }); // assert the nav is in wide format const nav = wrapper.find(Nav); @@ -273,12 +264,69 @@ describe('DocumentationLayout', () => { expect(quickNavigationModalComponent.exists()).toBe(false); }); + it('QuickNavigation renders Swift items', async () => { + getSetting.mockReturnValueOnce(true); + wrapper = createWrapper({ + stubs: { + ...stubs, + Nav: DocumentationNav, + NavBase, + }, + }); + wrapper.setProps({ + enableNavigator: true, + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find(QuickNavigationModal).props('children')).toEqual(swiftChildren); + }); + + it('QuickNavigation falls back to swift items, if no objc items', async () => { + getSetting.mockReturnValueOnce(true); + wrapper = createWrapper({ + stubs: { + ...stubs, + Nav: DocumentationNav, + NavBase, + }, + }); + wrapper.setProps({ + enableNavigator: true, + interfaceLanguage: Language.objectiveC.key.url, + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find(QuickNavigationModal).props('children')).toEqual(swiftChildren); + }); + + it('QuickNavigation renders objc items', async () => { + getSetting.mockReturnValueOnce(true); + wrapper = createWrapper({ + stubs: { + ...stubs, + Nav: DocumentationNav, + NavBase, + }, + }); + wrapper.setProps({ + enableNavigator: true, + interfaceLanguage: Language.objectiveC.key.url, + }); + wrapper.setData({ + indexState: { + flatChildren: { + [Language.swift.key.url]: swiftChildren, + [Language.objectiveC.key.url]: objcChildren, + }, + }, + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find(QuickNavigationModal).props('children')).toEqual(objcChildren); + }); + describe('if breakpoint is small', () => { beforeEach(() => { wrapper = createWrapper({ stubs: { AdjustableSidebarWidth: AdjustableSidebarWidthSmallStub, - NavigatorDataProvider, }, }); }); @@ -317,15 +365,6 @@ describe('DocumentationLayout', () => { }); }); - it('provides the selected api changes, to the NavigatorDataProvider', () => { - wrapper.setProps({ - enableNavigator: true, - selectedAPIChangesVersion: 'latest_major', - }); - const dataProvider = wrapper.find(NavigatorDataProvider); - expect(dataProvider.props('apiChangesVersion')).toEqual('latest_major'); - }); - it('renders the Navigator with data when no reference is found for a top-level item', () => { const technologies = { id: 'topic://not-existing', @@ -345,10 +384,7 @@ describe('DocumentationLayout', () => { const navigator = wrapper.find(Navigator); expect(navigator.exists()).toBe(true); // assert the technology is the last fallback - expect(navigator.props('technology')).toEqual({ - title: 'FooTechnology', - url: '/documentation/foo', - }); + expect(navigator.props('technologyProps')).toEqual(swiftProps); }); it('renders the Navigator with data when no reference is found, even when there is a reference data error', () => { @@ -372,10 +408,7 @@ describe('DocumentationLayout', () => { const navigator = wrapper.find(Navigator); expect(navigator.exists()).toBe(true); // assert the technology is the last fallback - expect(navigator.props('technology')).toEqual({ - title: 'FooTechnology', - url: '/documentation/foo', - }); + expect(navigator.props('technologyProps')).toEqual(swiftProps); }); it('renders the Navigator with data when no hierarchy and reference is found for the current page', () => { @@ -388,7 +421,7 @@ describe('DocumentationLayout', () => { const navigator = wrapper.find(Navigator); expect(navigator.exists()).toBe(true); // assert the technology is the last fallback - expect(navigator.props('technology')).toEqual(propsData.technology); + expect(navigator.props('technologyProps')).toEqual(swiftProps); }); it('renders without a sidebar', () => { @@ -418,10 +451,6 @@ describe('DocumentationLayout', () => { .toEqual(expect.arrayContaining(['topic-wrapper', 'full-width-container'])); }); - it('renders without NavigatorDataProvider', async () => { - expect(wrapper.find(NavigatorDataProvider).exists()).toBe(false); - }); - it('handles the `@close`, on Navigator, for Mobile breakpoints', async () => { wrapper.setProps({ enableNavigator: true, diff --git a/tests/unit/components/DocumentationTopic.spec.js b/tests/unit/components/DocumentationTopic.spec.js index 0dd901c1f..fd9e3a1ea 100644 --- a/tests/unit/components/DocumentationTopic.spec.js +++ b/tests/unit/components/DocumentationTopic.spec.js @@ -1171,7 +1171,7 @@ describe('DocumentationTopic', () => { expect(mockStore.setReferences).toHaveBeenCalledWith(newReferences); }); - it('calls `store.updateReferences` when `appState.includedArchiveIdentifiers` changes', async () => { + it('calls `store.updateReferences` when `indexState.includedArchiveIdentifiers` changes', async () => { const store = { state: { references: {} }, reset: jest.fn(), @@ -1185,7 +1185,7 @@ describe('DocumentationTopic', () => { expect(store.updateReferences).not.toHaveBeenCalled(); wrapper.setData({ - appState: { includedArchiveIdentifiers: ['Foo', 'Bar'] }, + indexState: { includedArchiveIdentifiers: ['Foo', 'Bar'] }, }); await wrapper.vm.$nextTick(); expect(store.updateReferences).toHaveBeenCalled(); diff --git a/tests/unit/components/Navigator.spec.js b/tests/unit/components/Navigator.spec.js index fbfc42f94..ad5e97335 100644 --- a/tests/unit/components/Navigator.spec.js +++ b/tests/unit/components/Navigator.spec.js @@ -19,56 +19,10 @@ jest.mock('docc-render/utils/throttle', () => jest.fn(v => v)); const { LoadingNavigatorCard } = Navigator.components; -const technology = { - title: 'FooTechnology', - children: [ - { - title: 'Group Marker', - type: TopicTypes.groupMarker, - }, - { - title: 'Child0', - path: '/foo/child0', - type: 'article', - children: [ - { - title: 'Group Marker, Child 0', - type: TopicTypes.groupMarker, - }, - { - title: 'Child0_GrandChild0', - path: '/foo/child0/grandchild0', - type: 'tutorial', - }, - { - title: 'Child0_GrandChild1', - path: '/foo/child0/grandchild1', - type: 'tutorial', - children: [{ - title: 'Child0_GrandChild0_GreatGrandChild0', - path: '/foo/child0/grandchild0/greatgrandchild0', - type: 'tutorial', - }], - }, - { - title: 'Child0_GrandChild2', - path: '/foo/child0/grandchild2', - type: 'tutorial', - }, - ], - }, - { - title: 'Child1', - path: '/foo/child1/', - type: 'tutorial', - children: [{ - title: 'Child1_GrandChild0', - path: '/foo/child1/grandchild0', - type: 'method', - }], - }, - ], - path: '/path/to/technology', +const technologyProps = { + technology: 'FooTechnology', + technologyPath: '/path/to/technology', + isTechnologyBeta: false, }; const references = { @@ -83,11 +37,6 @@ const parentTopicIdentifiers = [ 'root', 'first', 'second', ]; -const fallbackTechnology = { - title: 'FallbackTechnology', - url: '/url/to/technology', -}; - const mocks = { $route: { path: '/current/path', @@ -100,7 +49,7 @@ const navigatorReferences = { const defaultProps = { parentTopicIdentifiers, - technology, + technologyProps, references, scrollLockID: 'foo', renderFilterOnTop: false, @@ -141,9 +90,9 @@ describe('Navigator', () => { // will assert in another test children: [], type: TopicTypes.module, - technology: technology.title, - technologyPath: technology.path, - isTechnologyBeta: false, + technology: technologyProps.technology, + technologyPath: technologyProps.technologyPath, + isTechnologyBeta: technologyProps.isTechnologyBeta, scrollLockID: defaultProps.scrollLockID, renderFilterOnTop: defaultProps.renderFilterOnTop, errorFetching: false, @@ -192,29 +141,6 @@ describe('Navigator', () => { expect(wrapper.find('[aria-live="polite"]').text()).toBe('navigator.navigator-is navigator.state.ready'); }); - it('falls back to using the `technology.url` for the `technology-path`', () => { - const wrapper = createWrapper({ - propsData: { - technology: fallbackTechnology, - }, - }); - expect(wrapper.find(NavigatorCard).props()).toEqual({ - activePath: [references.first.url, references.second.url, mocks.$route.path], - // will assert in another test - children: [], - type: TopicTypes.module, - technology: fallbackTechnology.title, - technologyPath: fallbackTechnology.url, - isTechnologyBeta: false, - scrollLockID: defaultProps.scrollLockID, - renderFilterOnTop: defaultProps.renderFilterOnTop, - errorFetching: false, - apiChanges: null, - navigatorReferences, - hideAvailableTags: false, - }); - }); - it('passes the errorFetching prop', () => { const wrapper = createWrapper({ propsData: { diff --git a/tests/unit/components/Navigator/NavigatorCard.spec.js b/tests/unit/components/Navigator/NavigatorCard.spec.js index 8f817a1d7..f7b8131db 100644 --- a/tests/unit/components/Navigator/NavigatorCard.spec.js +++ b/tests/unit/components/Navigator/NavigatorCard.spec.js @@ -195,6 +195,7 @@ const createWrapper = ({ propsData, ...others } = {}) => shallowMount(NavigatorC }); const clearPersistedStateSpy = jest.spyOn(NavigatorCard.methods, 'clearPersistedState'); +const clearFiltersSpy = jest.spyOn(NavigatorCard.methods, 'clearFilters'); let getChildPositionInScroller; const DEFAULT_STORED_STATE = { @@ -1526,7 +1527,7 @@ describe('NavigatorCard', () => { expect(clearPersistedStateSpy).toHaveBeenCalledTimes(1); }); - it('does not restore the state, if the technology is different', async () => { + it('does not restore the state if the technology is different', async () => { sessionStorage.get.mockImplementation(() => mergeSessionState({ technology: 'some-other', nodesToRender: [root0.uid], @@ -1653,6 +1654,15 @@ describe('NavigatorCard', () => { expect(wrapper.findAll(NavigatorCardItem)).toHaveLength(4); }); + it('clears the filter if the technology is different', async () => { + const wrapper = createWrapper(); + wrapper.setProps({ + technology: 'newTechnology', + }); + await flushPromises(); + expect(clearFiltersSpy).toHaveBeenCalledTimes(1); + }); + it('keeps the open state, if there are API changes', async () => { sessionStorage.get.mockImplementation(() => mergeSessionState({ // simulate we have collapses all, but the top item diff --git a/tests/unit/components/Navigator/NavigatorDataProvider.spec.js b/tests/unit/components/Navigator/NavigatorDataProvider.spec.js deleted file mode 100644 index ada98e124..000000000 --- a/tests/unit/components/Navigator/NavigatorDataProvider.spec.js +++ /dev/null @@ -1,570 +0,0 @@ -/** - * This source file is part of the Swift.org open source project - * - * Copyright (c) 2022 Apple Inc. and the Swift project authors - * Licensed under Apache License v2.0 with Runtime Library Exception - * - * See https://swift.org/LICENSE.txt for license information - * See https://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -import NavigatorDataProvider from '@/components/Navigator/NavigatorDataProvider.vue'; -import { shallowMount } from '@vue/test-utils'; -import AppStore from 'docc-render/stores/AppStore'; -import Language from 'docc-render/constants/Language'; -import { TopicTypes } from '@/constants/TopicTypes'; -import { fetchIndexPathsData } from '@/utils/data'; -import { INDEX_ROOT_KEY } from '@/constants/sidebar'; -import { flushPromises } from '../../../../test-utils'; - -jest.mock('docc-render/utils/data'); - -const technologyUrl = '/documentation/foo'; - -const extendedTechnologies = { - path: '/documentation/foo', - children: [ - { - id: 'Group Marker', - title: 'Group Marker', - type: TopicTypes.groupMarker, - }, - { - id: 'Child0', - title: 'Child0', - path: '/documentation/foo/child0', - type: 'article', - children: [ - { - id: 'Group Marker, Child 0', - title: 'Group Marker, Child 0', - type: TopicTypes.groupMarker, - }, - { - id: 'Child0_GrandChild0', - title: 'Child0_GrandChild0', - path: '/documentation/foo/child0/grandchild0', - type: 'tutorial', - }, - { - id: 'Child0_GrandChild1', - title: 'Child0_GrandChild1', - path: '/documentation/foo/child0/grandchild1', - type: 'tutorial', - children: [{ - id: 'Child0_GrandChild0_GreatGrandChild0', - title: 'Child0_GrandChild0_GreatGrandChild0', - path: '/documentation/foo/child0/grandchild0/greatgrandchild0', - type: 'tutorial', - }], - }, - { - id: 'Child0_GrandChild2', - title: 'Child0_GrandChild2', - path: '/documentation/foo/child0/grandchild2', - type: 'tutorial', - }, - ], - }, - { - id: 'Child1', - title: 'Child1', - path: '/documentation/foo/child1/', - type: 'tutorial', - children: [{ - id: 'Child1_GrandChild0', - title: 'Child1_GrandChild0', - path: '/documentation/foo/child1/grandchild0', - type: 'method', - }], - }, - ], -}; - -const flatChildren = [ - { - uid: -196255993, - parent: '', - index: 0, - siblingsCount: 3, - depth: 0, - childUIDs: [], - }, - { - uid: -196255992, - parent: '', - index: 1, - siblingsCount: 3, - depth: 0, - childUIDs: [], - }, - { - uid: -196255991, - parent: '', - index: 2, - siblingsCount: 3, - depth: 0, - childUIDs: [], - }, -]; - -const swiftIndexOne = { - id: 'foo', - path: technologyUrl, - children: [1, 2, 3], -}; -const objectiveCIndexOne = { - id: 'foo-objc', - path: technologyUrl, - children: [1], -}; - -const references = { - foo: { bar: 'bar' }, -}; - -const includedArchiveIdentifiers = [ - 'foo', - 'bar', -]; - -const response = { - includedArchiveIdentifiers, - interfaceLanguages: { - [Language.swift.key.url]: [ - swiftIndexOne, - ], - [Language.objectiveC.key.url]: [ - objectiveCIndexOne, - ], - }, - references, -}; - -fetchIndexPathsData.mockResolvedValue(response); - -const defaultProps = { - technologyUrl, -}; - -let props = {}; - -const createWrapper = ({ propsData, ...others } = {}) => shallowMount(NavigatorDataProvider, { - propsData: { - ...defaultProps, - ...propsData, - }, - scopedSlots: { - default: (p) => { - props = p; - return 'Text'; - }, - }, - mocks: { - $route: { - params: { - locale: 'en-US', - }, - }, - }, - ...others, -}); - -describe('NavigatorDataProvider', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(() => { - AppStore.reset(); - }); - - it('fetches data when mounting NavigatorDataProvider', async () => { - expect(fetchIndexPathsData).toHaveBeenCalledTimes(0); - createWrapper(); - expect(fetchIndexPathsData).toHaveBeenCalledTimes(1); - expect(props).toHaveProperty('isFetching', true); - await flushPromises(); - expect(props).toEqual({ - apiChanges: null, - isFetchingAPIChanges: false, - isFetching: false, - technology: swiftIndexOne, - errorFetching: false, - flatChildren, - references, - }); - }); - - it('fetches data, even if being passed a none-root technology url', async () => { - expect(fetchIndexPathsData).toHaveBeenCalledTimes(0); - createWrapper({ - propsData: { - technologyUrl: `${technologyUrl}/bar/baz`, - }, - }); - expect(fetchIndexPathsData).toHaveBeenCalledTimes(1); - expect(props).toHaveProperty('isFetching', true); - await flushPromises(); - expect(props).toEqual({ - apiChanges: null, - isFetchingAPIChanges: false, - isFetching: false, - technology: swiftIndexOne, - errorFetching: false, - flatChildren, - references, - }); - }); - - it('sets errorFetching to true, when request errored', async () => { - expect(fetchIndexPathsData).toHaveBeenCalledTimes(0); - fetchIndexPathsData.mockRejectedValueOnce('Error'); - createWrapper(); - expect(fetchIndexPathsData).toHaveBeenCalledTimes(1); - expect(props).toHaveProperty('isFetching', true); - await flushPromises(); - expect(props).toEqual({ - apiChanges: null, - isFetchingAPIChanges: false, - isFetching: false, - technology: undefined, - errorFetching: true, - flatChildren: [], - references: {}, - }); - }); - - it('returns objc data', async () => { - expect(fetchIndexPathsData).toHaveBeenCalledTimes(0); - createWrapper({ - propsData: { - interfaceLanguage: Language.objectiveC.key.url, - }, - }); - expect(fetchIndexPathsData).toHaveBeenCalledTimes(1); - expect(props).toHaveProperty('isFetching', true); - await flushPromises(); - expect(props).toEqual({ - apiChanges: null, - isFetchingAPIChanges: false, - errorFetching: false, - isFetching: false, - technology: objectiveCIndexOne, - flatChildren: [{ - uid: -196255993, - parent: '', - index: 0, - siblingsCount: 1, - depth: 0, - childUIDs: [], - }], - references, - }); - }); - - it('falls back to swift items, if no objc items', async () => { - expect(fetchIndexPathsData).toHaveBeenCalledTimes(0); - fetchIndexPathsData.mockResolvedValueOnce({ - interfaceLanguages: { - [Language.swift.key.url]: response.interfaceLanguages[Language.swift.key.url], - }, - references, - }); - createWrapper({ - propsData: { - interfaceLanguage: Language.objectiveC.key.url, - }, - }); - expect(fetchIndexPathsData).toHaveBeenCalledTimes(1); - expect(props).toHaveProperty('isFetching', true); - await flushPromises(); - expect(props).toEqual({ - apiChanges: null, - isFetchingAPIChanges: false, - errorFetching: false, - isFetching: false, - technology: swiftIndexOne, - flatChildren, - references, - }); - }); - - it('returns the first technology, if none matches', async () => { - createWrapper({ - propsData: { - technologyUrl: '/documentation/bar', - }, - }); - await flushPromises(); - expect(props).toEqual({ - apiChanges: null, - isFetchingAPIChanges: false, - errorFetching: false, - isFetching: false, - technology: swiftIndexOne, - flatChildren, - references, - }); - }); - - it('returns undefined technology when index response is empty', async () => { - const emptyResponse = { interfaceLanguages: {} }; - fetchIndexPathsData.mockResolvedValue(emptyResponse); - createWrapper(); - await flushPromises(); - expect(props).toEqual({ - apiChanges: null, - isFetchingAPIChanges: false, - isFetching: false, - technology: undefined, - errorFetching: false, - flatChildren: [], - references: undefined, - }); - }); - - it('counts the amount of deprecated items a groupMarker has', async () => { - const technologyClone = JSON.parse(JSON.stringify(extendedTechnologies)); - technologyClone.children[1].deprecated = true; - technologyClone.children[2].deprecated = true; - technologyClone.children[1].children[1].deprecated = true; - fetchIndexPathsData.mockResolvedValue({ - interfaceLanguages: { - [Language.swift.key.url]: [ - technologyClone, - ], - }, - }); - createWrapper(); - await flushPromises(); - expect(props.flatChildren[0]).toHaveProperty('deprecatedChildrenCount', 2); - expect(props.flatChildren).toMatchSnapshot(); - }); - - it('removes the `beta` flag from children, if the parent is a `beta`', async () => { - const technologyClone = JSON.parse(JSON.stringify(extendedTechnologies)); - technologyClone.beta = true; - technologyClone.children[1].beta = true; - technologyClone.children[1].children[0].beta = true; - fetchIndexPathsData.mockResolvedValue({ - interfaceLanguages: { - [Language.swift.key.url]: [ - technologyClone, - ], - }, - }); - createWrapper(); - await flushPromises(); - expect(props).toMatchSnapshot(); - }); - - it('removes the `beta` flag from children, if the parent is a `beta`', async () => { - const technologyClone = JSON.parse(JSON.stringify(extendedTechnologies)); - technologyClone.children[1].beta = true; - technologyClone.children[1].children[1].beta = true; - // case where the direct parent is NOT `Beta`, but an ancestor is - technologyClone.children[1].children[2].children[0].beta = true; - // set an end node as beta - technologyClone.children[2].children[0].beta = true; - fetchIndexPathsData.mockResolvedValue({ - interfaceLanguages: { - [Language.swift.key.url]: [ - technologyClone, - ], - }, - }); - createWrapper(); - await flushPromises(); - expect(props).toMatchSnapshot(); - }); - - it('flattens deeply nested children and provides them to the NavigatorCard', async () => { - fetchIndexPathsData.mockResolvedValue({ - interfaceLanguages: { - [Language.swift.key.url]: [{ - path: '/documentation/foo', - children: [ - { - title: 'Group Marker', - type: TopicTypes.groupMarker, - }, - { - title: 'Child0', - path: '/foo/child0', - type: 'article', - children: [ - { - title: 'Group Marker, Child 0', - type: TopicTypes.groupMarker, - }, - { - title: 'Child0_GrandChild0', - path: '/foo/child0/grandchild0', - type: 'tutorial', - }, - { - title: 'Child0_GrandChild1', - path: '/foo/child0/grandchild1', - type: 'tutorial', - children: [{ - title: 'Child0_GrandChild0_GreatGrandChild0', - path: '/foo/child0/grandchild0/greatgrandchild0', - type: 'tutorial', - }], - }, - { - title: 'Child0_GrandChild2', - path: '/foo/child0/grandchild2', - type: 'tutorial', - }, - ], - }, - { - title: 'Child1', - path: '/foo/child1/', - type: 'tutorial', - children: [{ - title: 'Child1_GrandChild0', - path: '/foo/child1/grandchild0', - type: 'method', - }], - }, - ], - }], - }, - }); - createWrapper(); - await flushPromises(); - expect(props.flatChildren).toEqual([ - { - childUIDs: [ - 551503844, - -97593391, - ], - deprecatedChildrenCount: 0, - depth: 0, - index: 0, - parent: INDEX_ROOT_KEY, - siblingsCount: 3, - title: 'Group Marker', - type: 'groupMarker', - uid: -196255993, - }, - { - childUIDs: [ - -361407047, - 1438225895, - 1439149417, - 1440072939, - ], - depth: 0, - groupMarkerUID: -196255993, - index: 1, - parent: INDEX_ROOT_KEY, - path: '/foo/child0', - siblingsCount: 3, - title: 'Child0', - type: 'article', - uid: 551503844, - }, - { - childUIDs: [ - 1438225895, - 1439149417, - 1440072939, - ], - deprecatedChildrenCount: 0, - depth: 1, - index: 0, - parent: 551503844, - siblingsCount: 4, - title: 'Group Marker, Child 0', - type: 'groupMarker', - uid: -361407047, - }, - { - childUIDs: [], - depth: 1, - groupMarkerUID: -361407047, - index: 1, - parent: 551503844, - path: '/foo/child0/grandchild0', - siblingsCount: 4, - title: 'Child0_GrandChild0', - type: 'tutorial', - uid: 1438225895, - }, - { - childUIDs: [ - 305326087, - ], - depth: 1, - groupMarkerUID: -361407047, - index: 2, - parent: 551503844, - path: '/foo/child0/grandchild1', - siblingsCount: 4, - title: 'Child0_GrandChild1', - type: 'tutorial', - uid: 1439149417, - }, - { - childUIDs: [], - depth: 2, - index: 0, - parent: 1439149417, - path: '/foo/child0/grandchild0/greatgrandchild0', - siblingsCount: 1, - title: 'Child0_GrandChild0_GreatGrandChild0', - type: 'tutorial', - uid: 305326087, - }, - { - childUIDs: [], - depth: 1, - groupMarkerUID: -361407047, - index: 3, - parent: 551503844, - path: '/foo/child0/grandchild2', - siblingsCount: 4, - title: 'Child0_GrandChild2', - type: 'tutorial', - uid: 1440072939, - }, - { - childUIDs: [ - -827353283, - ], - depth: 0, - groupMarkerUID: -196255993, - index: 2, - parent: INDEX_ROOT_KEY, - path: '/foo/child1/', - siblingsCount: 3, - title: 'Child1', - type: 'tutorial', - uid: -97593391, - }, - { - childUIDs: [], - depth: 1, - index: 0, - parent: -97593391, - path: '/foo/child1/grandchild0', - siblingsCount: 1, - title: 'Child1_GrandChild0', - type: 'method', - uid: -827353283, - }, - ]); - }); - - it('sets `includedArchiveIdentifiers` state in the app store', async () => { - expect(AppStore.state.includedArchiveIdentifiers).toEqual([]); - fetchIndexPathsData.mockResolvedValue(response); - createWrapper(); - await flushPromises(); - expect(AppStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers); - }); -}); diff --git a/tests/unit/components/Navigator/__snapshots__/NavigatorDataProvider.spec.js.snap b/tests/unit/components/Navigator/__snapshots__/NavigatorDataProvider.spec.js.snap deleted file mode 100644 index f65dfb359..000000000 --- a/tests/unit/components/Navigator/__snapshots__/NavigatorDataProvider.spec.js.snap +++ /dev/null @@ -1,559 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NavigatorDataProvider counts the amount of deprecated items a groupMarker has 1`] = ` -Array [ - Object { - "childUIDs": Array [ - 365745369, - -1561138820, - ], - "deprecatedChildrenCount": 2, - "depth": 0, - "id": "Group Marker", - "index": 0, - "parent": , - "siblingsCount": 3, - "title": "Group Marker", - "type": "groupMarker", - "uid": -196255993, - }, - Object { - "childUIDs": Array [ - -1893192488, - -2046366661, - -2045443139, - -2044519617, - ], - "deprecated": true, - "depth": 0, - "groupMarkerUID": -196255993, - "id": "Child0", - "index": 1, - "parent": , - "path": "/documentation/foo/child0", - "siblingsCount": 3, - "title": "Child0", - "type": "article", - "uid": 365745369, - }, - Object { - "childUIDs": Array [ - -2046366661, - -2045443139, - -2044519617, - ], - "deprecatedChildrenCount": 1, - "depth": 1, - "id": "Group Marker, Child 0", - "index": 0, - "parent": 365745369, - "siblingsCount": 4, - "title": "Group Marker, Child 0", - "type": "groupMarker", - "uid": -1893192488, - }, - Object { - "childUIDs": Array [], - "deprecated": true, - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild0", - "index": 1, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild0", - "siblingsCount": 4, - "title": "Child0_GrandChild0", - "type": "tutorial", - "uid": -2046366661, - }, - Object { - "childUIDs": Array [ - 366459535, - ], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild1", - "index": 2, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild1", - "siblingsCount": 4, - "title": "Child0_GrandChild1", - "type": "tutorial", - "uid": -2045443139, - }, - Object { - "childUIDs": Array [], - "depth": 2, - "id": "Child0_GrandChild0_GreatGrandChild0", - "index": 0, - "parent": -2045443139, - "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", - "siblingsCount": 1, - "title": "Child0_GrandChild0_GreatGrandChild0", - "type": "tutorial", - "uid": 366459535, - }, - Object { - "childUIDs": Array [], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild2", - "index": 3, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild2", - "siblingsCount": 4, - "title": "Child0_GrandChild2", - "type": "tutorial", - "uid": -2044519617, - }, - Object { - "childUIDs": Array [ - 1069111095, - ], - "deprecated": true, - "depth": 0, - "groupMarkerUID": -196255993, - "id": "Child1", - "index": 2, - "parent": , - "path": "/documentation/foo/child1/", - "siblingsCount": 3, - "title": "Child1", - "type": "tutorial", - "uid": -1561138820, - }, - Object { - "childUIDs": Array [], - "depth": 1, - "id": "Child1_GrandChild0", - "index": 0, - "parent": -1561138820, - "path": "/documentation/foo/child1/grandchild0", - "siblingsCount": 1, - "title": "Child1_GrandChild0", - "type": "method", - "uid": 1069111095, - }, -] -`; - -exports[`NavigatorDataProvider removes the \`beta\` flag from children, if the parent is a \`beta\` 1`] = ` -Object { - "apiChanges": null, - "errorFetching": false, - "flatChildren": Array [ - Object { - "childUIDs": Array [ - 365745369, - -1561138820, - ], - "deprecatedChildrenCount": 0, - "depth": 0, - "id": "Group Marker", - "index": 0, - "parent": , - "siblingsCount": 3, - "title": "Group Marker", - "type": "groupMarker", - "uid": -196255993, - }, - Object { - "beta": false, - "childUIDs": Array [ - -1893192488, - -2046366661, - -2045443139, - -2044519617, - ], - "depth": 0, - "groupMarkerUID": -196255993, - "id": "Child0", - "index": 1, - "parent": , - "path": "/documentation/foo/child0", - "siblingsCount": 3, - "title": "Child0", - "type": "article", - "uid": 365745369, - }, - Object { - "beta": false, - "childUIDs": Array [ - -2046366661, - -2045443139, - -2044519617, - ], - "deprecatedChildrenCount": 0, - "depth": 1, - "id": "Group Marker, Child 0", - "index": 0, - "parent": 365745369, - "siblingsCount": 4, - "title": "Group Marker, Child 0", - "type": "groupMarker", - "uid": -1893192488, - }, - Object { - "childUIDs": Array [], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild0", - "index": 1, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild0", - "siblingsCount": 4, - "title": "Child0_GrandChild0", - "type": "tutorial", - "uid": -2046366661, - }, - Object { - "childUIDs": Array [ - 366459535, - ], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild1", - "index": 2, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild1", - "siblingsCount": 4, - "title": "Child0_GrandChild1", - "type": "tutorial", - "uid": -2045443139, - }, - Object { - "childUIDs": Array [], - "depth": 2, - "id": "Child0_GrandChild0_GreatGrandChild0", - "index": 0, - "parent": -2045443139, - "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", - "siblingsCount": 1, - "title": "Child0_GrandChild0_GreatGrandChild0", - "type": "tutorial", - "uid": 366459535, - }, - Object { - "childUIDs": Array [], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild2", - "index": 3, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild2", - "siblingsCount": 4, - "title": "Child0_GrandChild2", - "type": "tutorial", - "uid": -2044519617, - }, - Object { - "childUIDs": Array [ - 1069111095, - ], - "depth": 0, - "groupMarkerUID": -196255993, - "id": "Child1", - "index": 2, - "parent": , - "path": "/documentation/foo/child1/", - "siblingsCount": 3, - "title": "Child1", - "type": "tutorial", - "uid": -1561138820, - }, - Object { - "childUIDs": Array [], - "depth": 1, - "id": "Child1_GrandChild0", - "index": 0, - "parent": -1561138820, - "path": "/documentation/foo/child1/grandchild0", - "siblingsCount": 1, - "title": "Child1_GrandChild0", - "type": "method", - "uid": 1069111095, - }, - ], - "isFetching": false, - "isFetchingAPIChanges": false, - "references": undefined, - "technology": Object { - "beta": true, - "children": Array [ - Object { - "id": "Group Marker", - "title": "Group Marker", - "type": "groupMarker", - }, - Object { - "beta": true, - "children": Array [ - Object { - "beta": true, - "id": "Group Marker, Child 0", - "title": "Group Marker, Child 0", - "type": "groupMarker", - }, - Object { - "id": "Child0_GrandChild0", - "path": "/documentation/foo/child0/grandchild0", - "title": "Child0_GrandChild0", - "type": "tutorial", - }, - Object { - "children": Array [ - Object { - "id": "Child0_GrandChild0_GreatGrandChild0", - "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", - "title": "Child0_GrandChild0_GreatGrandChild0", - "type": "tutorial", - }, - ], - "id": "Child0_GrandChild1", - "path": "/documentation/foo/child0/grandchild1", - "title": "Child0_GrandChild1", - "type": "tutorial", - }, - Object { - "id": "Child0_GrandChild2", - "path": "/documentation/foo/child0/grandchild2", - "title": "Child0_GrandChild2", - "type": "tutorial", - }, - ], - "id": "Child0", - "path": "/documentation/foo/child0", - "title": "Child0", - "type": "article", - }, - Object { - "children": Array [ - Object { - "id": "Child1_GrandChild0", - "path": "/documentation/foo/child1/grandchild0", - "title": "Child1_GrandChild0", - "type": "method", - }, - ], - "id": "Child1", - "path": "/documentation/foo/child1/", - "title": "Child1", - "type": "tutorial", - }, - ], - "path": "/documentation/foo", - }, -} -`; - -exports[`NavigatorDataProvider removes the \`beta\` flag from children, if the parent is a \`beta\` 2`] = ` -Object { - "apiChanges": null, - "errorFetching": false, - "flatChildren": Array [ - Object { - "childUIDs": Array [ - 365745369, - -1561138820, - ], - "deprecatedChildrenCount": 0, - "depth": 0, - "id": "Group Marker", - "index": 0, - "parent": , - "siblingsCount": 3, - "title": "Group Marker", - "type": "groupMarker", - "uid": -196255993, - }, - Object { - "beta": true, - "childUIDs": Array [ - -1893192488, - -2046366661, - -2045443139, - -2044519617, - ], - "depth": 0, - "groupMarkerUID": -196255993, - "id": "Child0", - "index": 1, - "parent": , - "path": "/documentation/foo/child0", - "siblingsCount": 3, - "title": "Child0", - "type": "article", - "uid": 365745369, - }, - Object { - "childUIDs": Array [ - -2046366661, - -2045443139, - -2044519617, - ], - "deprecatedChildrenCount": 0, - "depth": 1, - "id": "Group Marker, Child 0", - "index": 0, - "parent": 365745369, - "siblingsCount": 4, - "title": "Group Marker, Child 0", - "type": "groupMarker", - "uid": -1893192488, - }, - Object { - "beta": false, - "childUIDs": Array [], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild0", - "index": 1, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild0", - "siblingsCount": 4, - "title": "Child0_GrandChild0", - "type": "tutorial", - "uid": -2046366661, - }, - Object { - "childUIDs": Array [ - 366459535, - ], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild1", - "index": 2, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild1", - "siblingsCount": 4, - "title": "Child0_GrandChild1", - "type": "tutorial", - "uid": -2045443139, - }, - Object { - "beta": false, - "childUIDs": Array [], - "depth": 2, - "id": "Child0_GrandChild0_GreatGrandChild0", - "index": 0, - "parent": -2045443139, - "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", - "siblingsCount": 1, - "title": "Child0_GrandChild0_GreatGrandChild0", - "type": "tutorial", - "uid": 366459535, - }, - Object { - "childUIDs": Array [], - "depth": 1, - "groupMarkerUID": -1893192488, - "id": "Child0_GrandChild2", - "index": 3, - "parent": 365745369, - "path": "/documentation/foo/child0/grandchild2", - "siblingsCount": 4, - "title": "Child0_GrandChild2", - "type": "tutorial", - "uid": -2044519617, - }, - Object { - "childUIDs": Array [ - 1069111095, - ], - "depth": 0, - "groupMarkerUID": -196255993, - "id": "Child1", - "index": 2, - "parent": , - "path": "/documentation/foo/child1/", - "siblingsCount": 3, - "title": "Child1", - "type": "tutorial", - "uid": -1561138820, - }, - Object { - "beta": true, - "childUIDs": Array [], - "depth": 1, - "id": "Child1_GrandChild0", - "index": 0, - "parent": -1561138820, - "path": "/documentation/foo/child1/grandchild0", - "siblingsCount": 1, - "title": "Child1_GrandChild0", - "type": "method", - "uid": 1069111095, - }, - ], - "isFetching": false, - "isFetchingAPIChanges": false, - "references": undefined, - "technology": Object { - "children": Array [ - Object { - "id": "Group Marker", - "title": "Group Marker", - "type": "groupMarker", - }, - Object { - "beta": true, - "children": Array [ - Object { - "id": "Group Marker, Child 0", - "title": "Group Marker, Child 0", - "type": "groupMarker", - }, - Object { - "beta": true, - "id": "Child0_GrandChild0", - "path": "/documentation/foo/child0/grandchild0", - "title": "Child0_GrandChild0", - "type": "tutorial", - }, - Object { - "children": Array [ - Object { - "beta": true, - "id": "Child0_GrandChild0_GreatGrandChild0", - "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", - "title": "Child0_GrandChild0_GreatGrandChild0", - "type": "tutorial", - }, - ], - "id": "Child0_GrandChild1", - "path": "/documentation/foo/child0/grandchild1", - "title": "Child0_GrandChild1", - "type": "tutorial", - }, - Object { - "id": "Child0_GrandChild2", - "path": "/documentation/foo/child0/grandchild2", - "title": "Child0_GrandChild2", - "type": "tutorial", - }, - ], - "id": "Child0", - "path": "/documentation/foo/child0", - "title": "Child0", - "type": "article", - }, - Object { - "children": Array [ - Object { - "beta": true, - "id": "Child1_GrandChild0", - "path": "/documentation/foo/child1/grandchild0", - "title": "Child1_GrandChild0", - "type": "method", - }, - ], - "id": "Child1", - "path": "/documentation/foo/child1/", - "title": "Child1", - "type": "tutorial", - }, - ], - "path": "/documentation/foo", - }, -} -`; diff --git a/tests/unit/mixins/__snapshots__/indexDataFetcher.spec.js.snap b/tests/unit/mixins/__snapshots__/indexDataFetcher.spec.js.snap new file mode 100644 index 000000000..97ef768fd --- /dev/null +++ b/tests/unit/mixins/__snapshots__/indexDataFetcher.spec.js.snap @@ -0,0 +1,418 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`indexDataFetcher counts the amount of deprecated items a groupMarker has 1`] = ` +Object { + "swift": Array [ + Object { + "childUIDs": Array [ + 365745369, + -1561138820, + ], + "deprecatedChildrenCount": 2, + "depth": 0, + "id": "Group Marker", + "index": 0, + "parent": , + "siblingsCount": 3, + "title": "Group Marker", + "type": "groupMarker", + "uid": -196255993, + }, + Object { + "childUIDs": Array [ + -1893192488, + -2046366661, + -2045443139, + -2044519617, + ], + "deprecated": true, + "depth": 0, + "groupMarkerUID": -196255993, + "id": "Child0", + "index": 1, + "parent": , + "path": "/documentation/foo/child0", + "siblingsCount": 3, + "title": "Child0", + "type": "article", + "uid": 365745369, + }, + Object { + "childUIDs": Array [ + -2046366661, + -2045443139, + -2044519617, + ], + "deprecatedChildrenCount": 1, + "depth": 1, + "id": "Group Marker, Child 0", + "index": 0, + "parent": 365745369, + "siblingsCount": 4, + "title": "Group Marker, Child 0", + "type": "groupMarker", + "uid": -1893192488, + }, + Object { + "childUIDs": Array [], + "deprecated": true, + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild0", + "index": 1, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild0", + "siblingsCount": 4, + "title": "Child0_GrandChild0", + "type": "tutorial", + "uid": -2046366661, + }, + Object { + "childUIDs": Array [ + 366459535, + ], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild1", + "index": 2, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild1", + "siblingsCount": 4, + "title": "Child0_GrandChild1", + "type": "tutorial", + "uid": -2045443139, + }, + Object { + "childUIDs": Array [], + "depth": 2, + "id": "Child0_GrandChild0_GreatGrandChild0", + "index": 0, + "parent": -2045443139, + "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", + "siblingsCount": 1, + "title": "Child0_GrandChild0_GreatGrandChild0", + "type": "tutorial", + "uid": 366459535, + }, + Object { + "childUIDs": Array [], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild2", + "index": 3, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild2", + "siblingsCount": 4, + "title": "Child0_GrandChild2", + "type": "tutorial", + "uid": -2044519617, + }, + Object { + "childUIDs": Array [ + 1069111095, + ], + "deprecated": true, + "depth": 0, + "groupMarkerUID": -196255993, + "id": "Child1", + "index": 2, + "parent": , + "path": "/documentation/foo/child1/", + "siblingsCount": 3, + "title": "Child1", + "type": "tutorial", + "uid": -1561138820, + }, + Object { + "childUIDs": Array [], + "depth": 1, + "id": "Child1_GrandChild0", + "index": 0, + "parent": -1561138820, + "path": "/documentation/foo/child1/grandchild0", + "siblingsCount": 1, + "title": "Child1_GrandChild0", + "type": "method", + "uid": 1069111095, + }, + ], +} +`; + +exports[`indexDataFetcher removes the \`beta\` flag from children, if the parent is a \`beta\` 1`] = ` +Object { + "swift": Array [ + Object { + "childUIDs": Array [ + 365745369, + -1561138820, + ], + "deprecatedChildrenCount": 0, + "depth": 0, + "id": "Group Marker", + "index": 0, + "parent": , + "siblingsCount": 3, + "title": "Group Marker", + "type": "groupMarker", + "uid": -196255993, + }, + Object { + "beta": false, + "childUIDs": Array [ + -1893192488, + -2046366661, + -2045443139, + -2044519617, + ], + "depth": 0, + "groupMarkerUID": -196255993, + "id": "Child0", + "index": 1, + "parent": , + "path": "/documentation/foo/child0", + "siblingsCount": 3, + "title": "Child0", + "type": "article", + "uid": 365745369, + }, + Object { + "beta": false, + "childUIDs": Array [ + -2046366661, + -2045443139, + -2044519617, + ], + "deprecatedChildrenCount": 0, + "depth": 1, + "id": "Group Marker, Child 0", + "index": 0, + "parent": 365745369, + "siblingsCount": 4, + "title": "Group Marker, Child 0", + "type": "groupMarker", + "uid": -1893192488, + }, + Object { + "childUIDs": Array [], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild0", + "index": 1, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild0", + "siblingsCount": 4, + "title": "Child0_GrandChild0", + "type": "tutorial", + "uid": -2046366661, + }, + Object { + "childUIDs": Array [ + 366459535, + ], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild1", + "index": 2, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild1", + "siblingsCount": 4, + "title": "Child0_GrandChild1", + "type": "tutorial", + "uid": -2045443139, + }, + Object { + "childUIDs": Array [], + "depth": 2, + "id": "Child0_GrandChild0_GreatGrandChild0", + "index": 0, + "parent": -2045443139, + "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", + "siblingsCount": 1, + "title": "Child0_GrandChild0_GreatGrandChild0", + "type": "tutorial", + "uid": 366459535, + }, + Object { + "childUIDs": Array [], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild2", + "index": 3, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild2", + "siblingsCount": 4, + "title": "Child0_GrandChild2", + "type": "tutorial", + "uid": -2044519617, + }, + Object { + "childUIDs": Array [ + 1069111095, + ], + "depth": 0, + "groupMarkerUID": -196255993, + "id": "Child1", + "index": 2, + "parent": , + "path": "/documentation/foo/child1/", + "siblingsCount": 3, + "title": "Child1", + "type": "tutorial", + "uid": -1561138820, + }, + Object { + "childUIDs": Array [], + "depth": 1, + "id": "Child1_GrandChild0", + "index": 0, + "parent": -1561138820, + "path": "/documentation/foo/child1/grandchild0", + "siblingsCount": 1, + "title": "Child1_GrandChild0", + "type": "method", + "uid": 1069111095, + }, + ], +} +`; + +exports[`indexDataFetcher removes the \`beta\` flag from children, if the parent is a \`beta\` 2`] = ` +Object { + "swift": Array [ + Object { + "childUIDs": Array [ + 365745369, + -1561138820, + ], + "deprecatedChildrenCount": 0, + "depth": 0, + "id": "Group Marker", + "index": 0, + "parent": , + "siblingsCount": 3, + "title": "Group Marker", + "type": "groupMarker", + "uid": -196255993, + }, + Object { + "beta": true, + "childUIDs": Array [ + -1893192488, + -2046366661, + -2045443139, + -2044519617, + ], + "depth": 0, + "groupMarkerUID": -196255993, + "id": "Child0", + "index": 1, + "parent": , + "path": "/documentation/foo/child0", + "siblingsCount": 3, + "title": "Child0", + "type": "article", + "uid": 365745369, + }, + Object { + "childUIDs": Array [ + -2046366661, + -2045443139, + -2044519617, + ], + "deprecatedChildrenCount": 0, + "depth": 1, + "id": "Group Marker, Child 0", + "index": 0, + "parent": 365745369, + "siblingsCount": 4, + "title": "Group Marker, Child 0", + "type": "groupMarker", + "uid": -1893192488, + }, + Object { + "beta": false, + "childUIDs": Array [], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild0", + "index": 1, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild0", + "siblingsCount": 4, + "title": "Child0_GrandChild0", + "type": "tutorial", + "uid": -2046366661, + }, + Object { + "childUIDs": Array [ + 366459535, + ], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild1", + "index": 2, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild1", + "siblingsCount": 4, + "title": "Child0_GrandChild1", + "type": "tutorial", + "uid": -2045443139, + }, + Object { + "beta": false, + "childUIDs": Array [], + "depth": 2, + "id": "Child0_GrandChild0_GreatGrandChild0", + "index": 0, + "parent": -2045443139, + "path": "/documentation/foo/child0/grandchild0/greatgrandchild0", + "siblingsCount": 1, + "title": "Child0_GrandChild0_GreatGrandChild0", + "type": "tutorial", + "uid": 366459535, + }, + Object { + "childUIDs": Array [], + "depth": 1, + "groupMarkerUID": -1893192488, + "id": "Child0_GrandChild2", + "index": 3, + "parent": 365745369, + "path": "/documentation/foo/child0/grandchild2", + "siblingsCount": 4, + "title": "Child0_GrandChild2", + "type": "tutorial", + "uid": -2044519617, + }, + Object { + "childUIDs": Array [ + 1069111095, + ], + "depth": 0, + "groupMarkerUID": -196255993, + "id": "Child1", + "index": 2, + "parent": , + "path": "/documentation/foo/child1/", + "siblingsCount": 3, + "title": "Child1", + "type": "tutorial", + "uid": -1561138820, + }, + Object { + "beta": true, + "childUIDs": Array [], + "depth": 1, + "id": "Child1_GrandChild0", + "index": 0, + "parent": -1561138820, + "path": "/documentation/foo/child1/grandchild0", + "siblingsCount": 1, + "title": "Child1_GrandChild0", + "type": "method", + "uid": 1069111095, + }, + ], +} +`; diff --git a/tests/unit/mixins/indexDataFetcher.spec.js b/tests/unit/mixins/indexDataFetcher.spec.js new file mode 100644 index 000000000..aefb01d7a --- /dev/null +++ b/tests/unit/mixins/indexDataFetcher.spec.js @@ -0,0 +1,509 @@ +/** + * This source file is part of the Swift.org open source project + * + * Copyright (c) 2024 Apple Inc. and the Swift project authors + * Licensed under Apache License v2.0 with Runtime Library Exception + * + * See https://swift.org/LICENSE.txt for license information + * See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ +import indexDataFetcher from 'docc-render/mixins/indexDataFetcher'; +import IndexStore from 'docc-render/stores/IndexStore'; +import { shallowMount } from '@vue/test-utils'; +import { fetchData } from 'docc-render/utils/data'; + +import Language from 'docc-render/constants/Language'; +import { TopicTypes } from '@/constants/TopicTypes'; +import { INDEX_ROOT_KEY } from '@/constants/sidebar'; +import { flushPromises } from '../../../test-utils'; + +jest.mock('docc-render/utils/data'); + +const technologyUrl = '/documentation/foo'; + +const extendedTechnologies = { + path: '/documentation/foo', + children: [ + { + id: 'Group Marker', + title: 'Group Marker', + type: TopicTypes.groupMarker, + }, + { + id: 'Child0', + title: 'Child0', + path: '/documentation/foo/child0', + type: 'article', + children: [ + { + id: 'Group Marker, Child 0', + title: 'Group Marker, Child 0', + type: TopicTypes.groupMarker, + }, + { + id: 'Child0_GrandChild0', + title: 'Child0_GrandChild0', + path: '/documentation/foo/child0/grandchild0', + type: 'tutorial', + }, + { + id: 'Child0_GrandChild1', + title: 'Child0_GrandChild1', + path: '/documentation/foo/child0/grandchild1', + type: 'tutorial', + children: [{ + id: 'Child0_GrandChild0_GreatGrandChild0', + title: 'Child0_GrandChild0_GreatGrandChild0', + path: '/documentation/foo/child0/grandchild0/greatgrandchild0', + type: 'tutorial', + }], + }, + { + id: 'Child0_GrandChild2', + title: 'Child0_GrandChild2', + path: '/documentation/foo/child0/grandchild2', + type: 'tutorial', + }, + ], + }, + { + id: 'Child1', + title: 'Child1', + path: '/documentation/foo/child1/', + type: 'tutorial', + children: [{ + id: 'Child1_GrandChild0', + title: 'Child1_GrandChild0', + path: '/documentation/foo/child1/grandchild0', + type: 'method', + }], + }, + ], +}; + +const flatChildren = { + [Language.swift.key.url]: [ + { + uid: -196255993, + parent: '', + index: 0, + siblingsCount: 3, + depth: 0, + childUIDs: [], + }, + { + uid: -196255992, + parent: '', + index: 1, + siblingsCount: 3, + depth: 0, + childUIDs: [], + }, + { + uid: -196255991, + parent: '', + index: 2, + siblingsCount: 3, + depth: 0, + childUIDs: [], + }, + ], + [Language.objectiveC.key.url]: [ + { + uid: -196255993, + parent: '', + index: 0, + siblingsCount: 1, + depth: 0, + childUIDs: [], + }, + ], +}; + +const swiftIndexOne = { + id: 'foo', + path: technologyUrl, + children: [1, 2, 3], +}; +const objectiveCIndexOne = { + id: 'foo-objc', + path: technologyUrl, + children: [1], +}; + +const references = { + foo: { bar: 'bar' }, +}; + +const includedArchiveIdentifiers = [ + 'foo', + 'bar', +]; + +const interfaceLanguages = { + [Language.swift.key.url]: [ + swiftIndexOne, + ], + [Language.objectiveC.key.url]: [ + objectiveCIndexOne, + ], +}; + +const response = { + includedArchiveIdentifiers, + interfaceLanguages, + references, +}; + +const technologyProps = { + [Language.swift.key.url]: { + technology: undefined, + technologyPath: technologyUrl, + isTechnologyBeta: undefined, + }, + [Language.objectiveC.key.url]: { + technology: undefined, + technologyPath: technologyUrl, + isTechnologyBeta: undefined, + }, +}; + +fetchData.mockReturnValue(response); + +const Component = { + name: 'MyComponent', + template: '', + mixins: [indexDataFetcher], +}; + +const createWrapper = ({ mocks } = {}) => shallowMount(Component, { mocks }); + +describe('indexDataFetcher', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + IndexStore.reset(); + }); + + it('fetches data when parent component imports `indexDataFetcher`', async () => { + expect(fetchData).toHaveBeenCalledTimes(0); + const wrapper = createWrapper({ + mocks: { + $route: { + path: 'some-path', + }, + }, + }); + expect(fetchData).toHaveBeenCalledTimes(1); + await wrapper.vm.$nextTick(); + + expect(IndexStore.state.references).toEqual(references); + expect(IndexStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers); + expect(IndexStore.state.flatChildren).toEqual(flatChildren); + expect(IndexStore.state.technologyProps).toEqual(technologyProps); + }); + + it('fetches data, even if being passed a none-root technology url', async () => { + expect(fetchData).toHaveBeenCalledTimes(0); + const wrapper = createWrapper({ + mocks: { + $route: { + path: `${technologyUrl}/bar/baz`, + }, + }, + }); + expect(fetchData).toHaveBeenCalledTimes(1); + await wrapper.vm.$nextTick(); + + expect(IndexStore.state.references).toEqual(references); + expect(IndexStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers); + expect(IndexStore.state.flatChildren).toEqual(flatChildren); + expect(IndexStore.state.technologyProps).toEqual(technologyProps); + }); + + it('sets errorFetching to true, when request errored', async () => { + expect(fetchData).toHaveBeenCalledTimes(0); + fetchData.mockRejectedValueOnce('Error'); + createWrapper(); + expect(fetchData).toHaveBeenCalledTimes(1); + await flushPromises(); + expect(IndexStore.state.errorFetching).toEqual(true); + }); + + it('returns the first technology, if none matches', async () => { + createWrapper({ + mocks: { + $route: { + path: '/documentation/bar', + }, + }, + }); + await flushPromises(); + expect(IndexStore.state.technologyProps).toEqual(technologyProps); + }); + + it('returns undefined technology when index response is empty', async () => { + const emptyResponse = { interfaceLanguages: {} }; + fetchData.mockResolvedValue(emptyResponse); + createWrapper(); + await flushPromises(); + expect(IndexStore.state).toEqual({ + flatChildren: {}, + references: {}, + apiChanges: null, + apiChangesVersion: null, + includedArchiveIdentifiers: [], + errorFetching: false, + errorFetchingDiffs: false, + technologyProps: {}, + }); + }); + + it('counts the amount of deprecated items a groupMarker has', async () => { + const technologyClone = JSON.parse(JSON.stringify(extendedTechnologies)); + technologyClone.children[1].deprecated = true; + technologyClone.children[2].deprecated = true; + technologyClone.children[1].children[1].deprecated = true; + fetchData.mockResolvedValue({ + interfaceLanguages: { + [Language.swift.key.url]: [ + technologyClone, + ], + }, + }); + createWrapper(); + await flushPromises(); + expect(IndexStore.state.flatChildren[Language.swift.key.url][0]).toHaveProperty('deprecatedChildrenCount', 2); + expect(IndexStore.state.flatChildren).toMatchSnapshot(); + }); + + it('removes the `beta` flag from children, if the parent is a `beta`', async () => { + const technologyClone = JSON.parse(JSON.stringify(extendedTechnologies)); + technologyClone.beta = true; + technologyClone.children[1].beta = true; + technologyClone.children[1].children[0].beta = true; + fetchData.mockResolvedValue({ + interfaceLanguages: { + [Language.swift.key.url]: [ + technologyClone, + ], + }, + }); + createWrapper(); + await flushPromises(); + expect(IndexStore.state.flatChildren).toMatchSnapshot(); + }); + + it('removes the `beta` flag from children, if the parent is a `beta`', async () => { + const technologyClone = JSON.parse(JSON.stringify(extendedTechnologies)); + technologyClone.children[1].beta = true; + technologyClone.children[1].children[1].beta = true; + // case where the direct parent is NOT `Beta`, but an ancestor is + technologyClone.children[1].children[2].children[0].beta = true; + // set an end node as beta + technologyClone.children[2].children[0].beta = true; + fetchData.mockResolvedValue({ + interfaceLanguages: { + [Language.swift.key.url]: [ + technologyClone, + ], + }, + }); + createWrapper(); + await flushPromises(); + expect(IndexStore.state.flatChildren).toMatchSnapshot(); + }); + + it('flattens deeply nested children and sets it to `IndexStore`', async () => { + fetchData.mockResolvedValue({ + interfaceLanguages: { + [Language.swift.key.url]: [{ + path: '/documentation/foo', + children: [ + { + title: 'Group Marker', + type: TopicTypes.groupMarker, + }, + { + title: 'Child0', + path: '/foo/child0', + type: 'article', + children: [ + { + title: 'Group Marker, Child 0', + type: TopicTypes.groupMarker, + }, + { + title: 'Child0_GrandChild0', + path: '/foo/child0/grandchild0', + type: 'tutorial', + }, + { + title: 'Child0_GrandChild1', + path: '/foo/child0/grandchild1', + type: 'tutorial', + children: [{ + title: 'Child0_GrandChild0_GreatGrandChild0', + path: '/foo/child0/grandchild0/greatgrandchild0', + type: 'tutorial', + }], + }, + { + title: 'Child0_GrandChild2', + path: '/foo/child0/grandchild2', + type: 'tutorial', + }, + ], + }, + { + title: 'Child1', + path: '/foo/child1/', + type: 'tutorial', + children: [{ + title: 'Child1_GrandChild0', + path: '/foo/child1/grandchild0', + type: 'method', + }], + }, + ], + }], + }, + }); + createWrapper(); + await flushPromises(); + expect(IndexStore.state.flatChildren).toEqual({ + [Language.swift.key.url]: [ + { + childUIDs: [ + 551503844, + -97593391, + ], + deprecatedChildrenCount: 0, + depth: 0, + index: 0, + parent: INDEX_ROOT_KEY, + siblingsCount: 3, + title: 'Group Marker', + type: 'groupMarker', + uid: -196255993, + }, + { + childUIDs: [ + -361407047, + 1438225895, + 1439149417, + 1440072939, + ], + depth: 0, + groupMarkerUID: -196255993, + index: 1, + parent: INDEX_ROOT_KEY, + path: '/foo/child0', + siblingsCount: 3, + title: 'Child0', + type: 'article', + uid: 551503844, + }, + { + childUIDs: [ + 1438225895, + 1439149417, + 1440072939, + ], + deprecatedChildrenCount: 0, + depth: 1, + index: 0, + parent: 551503844, + siblingsCount: 4, + title: 'Group Marker, Child 0', + type: 'groupMarker', + uid: -361407047, + }, + { + childUIDs: [], + depth: 1, + groupMarkerUID: -361407047, + index: 1, + parent: 551503844, + path: '/foo/child0/grandchild0', + siblingsCount: 4, + title: 'Child0_GrandChild0', + type: 'tutorial', + uid: 1438225895, + }, + { + childUIDs: [ + 305326087, + ], + depth: 1, + groupMarkerUID: -361407047, + index: 2, + parent: 551503844, + path: '/foo/child0/grandchild1', + siblingsCount: 4, + title: 'Child0_GrandChild1', + type: 'tutorial', + uid: 1439149417, + }, + { + childUIDs: [], + depth: 2, + index: 0, + parent: 1439149417, + path: '/foo/child0/grandchild0/greatgrandchild0', + siblingsCount: 1, + title: 'Child0_GrandChild0_GreatGrandChild0', + type: 'tutorial', + uid: 305326087, + }, + { + childUIDs: [], + depth: 1, + groupMarkerUID: -361407047, + index: 3, + parent: 551503844, + path: '/foo/child0/grandchild2', + siblingsCount: 4, + title: 'Child0_GrandChild2', + type: 'tutorial', + uid: 1440072939, + }, + { + childUIDs: [ + -827353283, + ], + depth: 0, + groupMarkerUID: -196255993, + index: 2, + parent: INDEX_ROOT_KEY, + path: '/foo/child1/', + siblingsCount: 3, + title: 'Child1', + type: 'tutorial', + uid: -97593391, + }, + { + childUIDs: [], + depth: 1, + index: 0, + parent: -97593391, + path: '/foo/child1/grandchild0', + siblingsCount: 1, + title: 'Child1_GrandChild0', + type: 'method', + uid: -827353283, + }, + ], + }); + }); + + it('sets `includedArchiveIdentifiers` state in the index store', async () => { + expect(IndexStore.state.includedArchiveIdentifiers).toEqual([]); + fetchData.mockResolvedValue(response); + createWrapper(); + await flushPromises(); + expect(IndexStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers); + }); +}); diff --git a/tests/unit/mixins/indexDataGetter.spec.js b/tests/unit/mixins/indexDataGetter.spec.js new file mode 100644 index 000000000..19ac2d5ae --- /dev/null +++ b/tests/unit/mixins/indexDataGetter.spec.js @@ -0,0 +1,228 @@ +/** + * This source file is part of the Swift.org open source project + * + * Copyright (c) 2024 Apple Inc. and the Swift project authors + * Licensed under Apache License v2.0 with Runtime Library Exception + * + * See https://swift.org/LICENSE.txt for license information + * See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ +import { shallowMount } from '@vue/test-utils'; +import Language from 'docc-render/constants/Language'; +import indexDataGetter from 'docc-render/mixins/indexDataGetter'; +import IndexStore from 'docc-render/stores/IndexStore'; + +const swiftIndex = [{ + id: 'foo', + path: '/documentation/swift', + children: [1, 2, 3], +}]; + +const objectiveCIndex = [{ + id: 'foo-objc', + path: '/documentation/objc', + children: [1], +}]; + +const references = { + foo: { identifier: 'foo' }, + bar: { identifier: 'bar' }, +}; + +const swiftDiff = { + '/documentation/swift': 'modified', +}; + +const objectiveCDiff = { + '/documentation/objc': 'modified', +}; + +const includedArchiveIdentifiers = ['foo', 'bar']; + +const swiftProps = { + technology: 'swift', + technologyPath: '/documentation/swift', + isTechnologyBeta: false, +}; + +const objectiveCProps = { + technology: 'objective-c', + technologyPath: '/documentation/objc', + isTechnologyBeta: false, +}; + +const mockState = () => ({ + flatChildren: { + [Language.swift.key.url]: swiftIndex, + }, + references, + apiChanges: { + [Language.swift.key.url]: swiftDiff, + }, + apiChangesVersion: 'version0', + includedArchiveIdentifiers, + errorFetching: false, + errorFetchingDiffs: false, + technologyProps: { + [Language.swift.key.url]: swiftProps, + }, +}); + +const Component = { + name: 'MyComponent', + template: '', + mixins: [indexDataGetter], + props: { + interfaceLanguage: { + type: String, + required: false, + }, + technology: { + type: Object, + required: false, + }, + selectedAPIChangesVersion: { + type: String, + required: false, + }, + }, +}; + +const technology = { + title: 'title', + path: '/documentation/boo', +}; + +const createWrapper = () => shallowMount(Component, { + propsData: { + interfaceLanguage: Language.swift.key.url, + technology, + selectedAPIChangesVersion: 'version0', + }, +}); + +describe('indexDataGetter', () => { + it('selects correct language variant when it exists`', async () => { + IndexStore.state = mockState(); + const wrapper = createWrapper(); + expect(wrapper.vm.indexNodes).toEqual(swiftIndex); + expect(wrapper.vm.technologyProps).toEqual(swiftProps); + + wrapper.setData({ + indexState: { + flatChildren: { + [Language.objectiveC.key.url]: objectiveCIndex, + }, + technologyProps: { + [Language.objectiveC.key.url]: objectiveCProps, + }, + apiChanges: { + [Language.objectiveC.key.url]: objectiveCDiff, + }, + }, + }); + wrapper.setProps({ + interfaceLanguage: Language.objectiveC.key.url, + }); + expect(wrapper.vm.indexNodes).toEqual(objectiveCIndex); + expect(wrapper.vm.technologyProps).toEqual(objectiveCProps); + expect(wrapper.vm.apiChanges).toEqual(objectiveCDiff); + }); + + it('selects swift variant when language is objc but its data does not exist`', async () => { + IndexStore.state = mockState(); + const wrapper = createWrapper(); + wrapper.setProps({ + interfaceLanguage: Language.objectiveC.key.url, + }); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.indexNodes).toEqual(swiftIndex); + expect(wrapper.vm.technologyProps).toEqual(swiftProps); + expect(wrapper.vm.apiChanges).toEqual(swiftDiff); + }); + + it('return undefined `apiChanges` if `apiChangesVersion` does not match selected version`', () => { + IndexStore.state = { + ...mockState(), + apiChangesVersion: 'version1', + }; + const wrapper = createWrapper(); + expect(wrapper.vm.apiChanges).toEqual(undefined); + }); + + it('return undefined `apiChanges` if `errorFetchingDiffs``', () => { + IndexStore.state = { + ...mockState(), + errorFetchingDiffs: true, + }; + const wrapper = createWrapper(); + expect(wrapper.vm.apiChanges).toEqual(undefined); + }); + + it('when no index data is unavailable through IndexStore, fallback to technology provided in prop`', () => { + IndexStore.state = { + ...mockState(), + flatChildren: {}, + technologyProps: {}, + }; + const wrapper = createWrapper(); + expect(wrapper.vm.indexNodes).toEqual([]); + // fallback to technology if provided thru prop + expect(wrapper.vm.technologyProps).toEqual({ + technology: technology.title, + technologyPath: technology.path, + isTechnologyBeta: technology.beta, + }); + + wrapper.setProps({ + technology: null, + }); + expect(wrapper.vm.indexNodes).toEqual([]); + // if not provided, set to null + expect(wrapper.vm.technologyProps).toEqual(null); + }); + + it('falls back to using the `technology.url` if no path', () => { + const fallbackTechnology = { + title: 'FallbackTechnology', + url: '/url/to/technology', + }; + IndexStore.state = { + ...mockState(), + technologyProps: {}, + }; + const wrapper = createWrapper(); + wrapper.setProps({ + technology: fallbackTechnology, + }); + + expect(wrapper.vm.technologyProps.technologyPath).toEqual(fallbackTechnology.url); + }); + + it('computes the correct fetching status`', () => { + IndexStore.state = { + ...mockState(), + flatChildren: null, + errorFetching: true, + }; + const wrapper = createWrapper(); + // not `isFetching` if no fetched data and there was an error + expect(wrapper.vm.navigatorProps.isFetching).toBe(false); + + // fetching if no fetched data and no error + wrapper.setData({ + indexState: { + errorFetching: false, + }, + }); + expect(wrapper.vm.navigatorProps.isFetching).toBe(true); + + // not fetching if fetched data is not null + wrapper.setData({ + indexState: { + flatChildren: {}, + }, + }); + expect(wrapper.vm.navigatorProps.isFetching).toBe(false); + }); +}); diff --git a/tests/unit/stores/AppStore.spec.js b/tests/unit/stores/AppStore.spec.js index aae3cc65c..e961c1e55 100644 --- a/tests/unit/stores/AppStore.spec.js +++ b/tests/unit/stores/AppStore.spec.js @@ -22,7 +22,6 @@ describe('AppStore', () => { systemColorScheme: ColorScheme.light, preferredLocale: null, availableLocales: [], - includedArchiveIdentifiers: [], }); }); @@ -38,7 +37,6 @@ describe('AppStore', () => { systemColorScheme: ColorScheme.light, preferredLocale: null, availableLocales: [], - includedArchiveIdentifiers: [], }); // restore target @@ -74,20 +72,11 @@ describe('AppStore', () => { }); }); - describe('setIncludedArchiveIdentifiers', () => { - it('sets the included archive identifiers', () => { - const includedArchiveIdentifiers = ['foo', 'bar']; - AppStore.setIncludedArchiveIdentifiers(includedArchiveIdentifiers); - expect(AppStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers); - }); - }); - it('resets the state', () => { AppStore.setImageLoadingStrategy(ImageLoadingStrategy.eager); AppStore.setPreferredColorScheme(ColorScheme.auto); AppStore.setSystemColorScheme(ColorScheme.dark); AppStore.syncPreferredColorScheme(); - AppStore.setIncludedArchiveIdentifiers(['a']); AppStore.reset(); expect(AppStore.state).toEqual({ @@ -97,7 +86,6 @@ describe('AppStore', () => { systemColorScheme: ColorScheme.light, preferredLocale: null, availableLocales: [], - includedArchiveIdentifiers: [], }); }); }); diff --git a/tests/unit/stores/IndexStore.spec.js b/tests/unit/stores/IndexStore.spec.js new file mode 100644 index 000000000..76f6990cd --- /dev/null +++ b/tests/unit/stores/IndexStore.spec.js @@ -0,0 +1,128 @@ +/** + * This source file is part of the Swift.org open source project + * + * Copyright (c) 2024 Apple Inc. and the Swift project authors + * Licensed under Apache License v2.0 with Runtime Library Exception + * + * See https://swift.org/LICENSE.txt for license information + * See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import IndexStore from 'docc-render/stores/IndexStore'; +import Language from 'docc-render/constants/Language'; + +const flatChildren = { + [Language.swift.key.url]: [ + { + title: 'item 1', + }, + ], + [Language.objectiveC.key.url]: [ + { + title: 'item 2', + }, + ], +}; + +const references = { + foo: { identifier: 'foo' }, + bar: { identifier: 'bar' }, +}; + +const apiChanges = { + interfaceLanguages: { + [Language.swift.key.url]: [], + }, +}; + +const apiChangesVersion = 'version_name'; + +const includedArchiveIdentifiers = ['foo', 'bar']; + +const technologyProps = { + [Language.swift.key.url]: { + technology: 'title', + technologyPath: 'path', + isTechnologyBeta: false, + }, +}; + +describe('IndexStore', () => { + const defaultState = { + flatChildren: null, + references: {}, + apiChanges: null, + apiChangesVersion: null, + includedArchiveIdentifiers: [], + errorFetching: false, + errorFetchingDiffs: false, + technologyProps: {}, + }; + + beforeEach(() => { + localStorage.clear(); + IndexStore.reset(); + }); + + it('has a default state', () => { + expect(IndexStore.state).toEqual(defaultState); + }); + + describe('reset', () => { + it('restores the default state', () => { + IndexStore.state.flatChildren = flatChildren; + IndexStore.state.references = references; + IndexStore.state.apiChanges = apiChanges; + IndexStore.state.apiChangesVersion = apiChangesVersion; + IndexStore.state.includedArchiveIdentifiers = includedArchiveIdentifiers; + IndexStore.state.errorFetching = true; + IndexStore.state.errorFetchingDiffs = true; + IndexStore.state.technologyProps = technologyProps; + + expect(IndexStore.state).not.toEqual(defaultState); + IndexStore.reset(); + // assert all the state is reset + expect(IndexStore.state).toEqual(defaultState); + }); + }); + + it('sets `flatChildren`', () => { + IndexStore.setFlatChildren(flatChildren); + expect(IndexStore.state.flatChildren).toEqual(flatChildren); + }); + + it('sets `references`', () => { + IndexStore.setReferences(references); + expect(IndexStore.state.references).toEqual(references); + }); + + it('sets `apiChanges`', () => { + IndexStore.setApiChanges(apiChanges); + expect(IndexStore.state.apiChanges).toEqual(apiChanges); + }); + + it('sets `apiChangesVersion`', () => { + IndexStore.setApiChangesVersion(apiChangesVersion); + expect(IndexStore.state.apiChangesVersion).toEqual(apiChangesVersion); + }); + + it('sets `includedArchiveIdentifiers`', () => { + IndexStore.setIncludedArchiveIdentifiers(includedArchiveIdentifiers); + expect(IndexStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers); + }); + + it('sets `errorFetching`', () => { + IndexStore.setErrorFetching(true); + expect(IndexStore.state.errorFetching).toEqual(true); + }); + + it('sets `errorFetchingDiffs`', () => { + IndexStore.setErrorFetchingDiffs(true); + expect(IndexStore.state.errorFetchingDiffs).toEqual(true); + }); + + it('sets `technologyProps`', () => { + IndexStore.setTechnologyProps(technologyProps); + expect(IndexStore.state.technologyProps).toEqual(technologyProps); + }); +}); diff --git a/tests/unit/utils/references.spec.js b/tests/unit/utils/references.spec.js index a6a2cd7b7..14df91131 100644 --- a/tests/unit/utils/references.spec.js +++ b/tests/unit/utils/references.spec.js @@ -8,7 +8,7 @@ * See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ -import AppStore from 'docc-render/stores/AppStore'; +import IndexStore from 'docc-render/stores/IndexStore'; import { filterInactiveReferences } from 'docc-render/utils/references'; const aa = { @@ -52,17 +52,17 @@ const references = { describe('filterInactiveReferences', () => { it('does not filter any refs when `includedArchiveIdentifiers` is empty', () => { - AppStore.setIncludedArchiveIdentifiers([]); + IndexStore.setIncludedArchiveIdentifiers([]); expect(filterInactiveReferences(references)).toEqual(references); }); it('does not filter any refs when `includedArchiveIdentifiers` includes all ref archives', () => { - AppStore.setIncludedArchiveIdentifiers(['A', 'B', 'BB']); + IndexStore.setIncludedArchiveIdentifiers(['A', 'B', 'BB']); expect(filterInactiveReferences(references)).toEqual(references); }); it('removes `url` from non-external refs that aren\'t part of included archive', () => { - AppStore.setIncludedArchiveIdentifiers(['B']); + IndexStore.setIncludedArchiveIdentifiers(['B']); const filteredRefs = filterInactiveReferences(references); expect(Object.keys(filteredRefs)).toEqual(Object.keys(references));