1+ /*!
2+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+ * SPDX-License-Identifier: AGPL-3.0-or-later
4+ */
5+
6+ import type { TeamFolder } from '../../api.ts'
7+
8+ import { shallowMount } from '@vue/test-utils'
9+ import { beforeEach , describe , expect , it , vi } from 'vitest'
10+ import Circle from '../models/circle.ts'
11+ import CircleDetails from './CircleDetails.vue'
12+
13+ /**
14+ * The component reads these at construction time (in data()), so the mock
15+ * values must be in place before mount. `vi.hoisted` keeps them available to
16+ * the factory mock below.
17+ */
18+ const loadState = vi . hoisted ( ( ) => vi . fn ( ( app : string , key : string , fallback : unknown ) => fallback ) )
19+ const getTeamFolder = vi . hoisted ( ( ) => vi . fn < ( teamId : string ) => Promise < TeamFolder | null > > ( async ( ) => null ) )
20+ const upgradeTeamFolder = vi . hoisted ( ( ) => vi . fn < ( teamId : string ) => Promise < TeamFolder > > ( async ( ) => ( { id : 1 , mountPoint : 'Team' } ) ) )
21+
22+ vi . mock ( '@nextcloud/initial-state' , ( ) => ( { loadState } ) )
23+ vi . mock ( '@nextcloud/auth' , ( ) => ( {
24+ getCurrentUser : ( ) => ( { uid : 'owner-1' , isAdmin : false } ) ,
25+ } ) )
26+ vi . mock ( '@nextcloud/router' , async ( importOriginal ) => {
27+ const actual = await importOriginal < typeof import ( '@nextcloud/router' ) > ( )
28+ return {
29+ ...actual ,
30+ generateUrl : ( tpl : string ) => tpl ,
31+ generateOcsUrl : ( tpl : string ) => tpl ,
32+ generateRemoteUrl : ( tpl : string ) => tpl ,
33+ }
34+ } )
35+ vi . mock ( '@nextcloud/axios' , ( ) => ( {
36+ default : {
37+ get : vi . fn ( async ( ) => ( { data : { ocs : { data : { resources : [ ] } } } } ) ) ,
38+ post : vi . fn ( async ( ) => ( { data : { ocs : { data : { } } } } ) ) ,
39+ request : vi . fn ( async ( ) => ( { data : { } } ) ) ,
40+ } ,
41+ } ) )
42+ vi . mock ( '@nextcloud/dialogs' , ( ) => ( {
43+ FilePickerClosed : { } ,
44+ FilePickerType : { Choose : 1 } ,
45+ getFilePickerBuilder : ( ) => ( {
46+ setMultiSelect : ( ) => ( { setMimeTypeFilter : ( ) => ( { setType : ( ) => ( { allowDirectories : ( ) => ( { build : ( ) => ( { } ) } ) } ) } ) } ) ,
47+ } ) ,
48+ showError : vi . fn ( ) ,
49+ showSuccess : vi . fn ( ) ,
50+ } ) )
51+ vi . mock ( '@nextcloud/event-bus' , ( ) => ( { emit : vi . fn ( ) } ) )
52+ vi . mock ( '../../api.ts' , ( ) => ( { getTeamFolder, upgradeTeamFolder } ) )
53+
54+ /**
55+ * Build a Circle instance with the fields the component under test needs.
56+ *
57+ * @param overrides - partial raw data merged over the defaults
58+ */
59+ function makeCircle ( overrides : Record < string , unknown > = { } ) : Circle {
60+ return new Circle ( {
61+ id : 'team-1' ,
62+ displayName : 'Marketing' ,
63+ description : '' ,
64+ creation : 0 ,
65+ population : 1 ,
66+ populationInherited : 1 ,
67+ config : 0 ,
68+ settings : { } ,
69+ owner : { singleId : 'owner-1' , userId : 'owner-1' , displayName : 'Owner' , level : 9 , type : 1 , isUser : true , instance : '' , source : '' } ,
70+ initiator : { singleId : 'owner-1' , userId : 'owner-1' , displayName : 'Owner' , level : 9 , type : 1 , isUser : true , instance : '' , source : '' } ,
71+ ...overrides ,
72+ } )
73+ }
74+
75+ const MEMBER_INITIATOR = { singleId : 'm-1' , userId : 'm-1' , displayName : 'Member' , level : 1 , type : 1 , isUser : true , instance : '' , source : '' }
76+
77+ /**
78+ * Mount the component with owner-level permissions and explicit feature flags.
79+ *
80+ * @param overrides - circle raw-data overrides (e.g. initiator level)
81+ * @param flags - feature flags for loadState + optional pre-linked team folder
82+ */
83+ function mountDetails (
84+ overrides : Record < string , unknown > = { } ,
85+ flags : { autoCreate ?: boolean ; providerAvailable ?: boolean ; teamFolder ?: TeamFolder | null } = { } ,
86+ ) {
87+ loadState . mockImplementation ( ( app : string , key : string , fallback : unknown ) => {
88+ if ( app !== 'circles' ) return fallback
89+ if ( key === 'teamFolderAutoCreate' ) return flags . autoCreate ?? true
90+ if ( key === 'teamFolderProviderAvailable' ) return flags . providerAvailable ?? true
91+ return fallback
92+ } )
93+ getTeamFolder . mockImplementation ( async ( ) => flags . teamFolder ?? null )
94+ const circle = makeCircle ( overrides )
95+ return shallowMount ( CircleDetails , {
96+ props : { circle } ,
97+ global : {
98+ mocks : {
99+ t : ( pkg : string , text : string , vars ?: Record < string , unknown > ) => {
100+ if ( vars && typeof text === 'string' ) {
101+ return text . replace ( / \{ ( \w + ) \} / g, ( _ , k : string ) => String ( vars [ k ] ?? `{${ k } }` ) )
102+ }
103+ return text
104+ } ,
105+ $store : {
106+ getters : { getCircle : ( ) => null } ,
107+ dispatch : vi . fn ( ) ,
108+ commit : vi . fn ( ) ,
109+ } ,
110+ $router : { push : vi . fn ( ) , resolve : ( ) => ( { href : '#' } ) } ,
111+ } ,
112+ stubs : {
113+ // Render NcNoteCard and NcButton slot/text content so banner
114+ // text is visible in the rendered HTML.
115+ NcNoteCard : {
116+ template : '<div class="nc-notecard-stub"><slot /></div>' ,
117+ } ,
118+ NcButton : {
119+ template : '<button class="nc-button-stub"><slot /></button>' ,
120+ } ,
121+ } ,
122+ } ,
123+ } )
124+ }
125+
126+ describe ( 'CircleDetails folder button' , ( ) => {
127+ beforeEach ( ( ) => {
128+ vi . clearAllMocks ( )
129+ } )
130+
131+ it ( 'renders no folder button in the Create section (banner handles it)' , ( ) => {
132+ const wrapper = mountDetails ( { } , { autoCreate : true , providerAvailable : true } )
133+ expect ( wrapper . vm . folderButtonType ) . toBeNull ( )
134+ // The Create section has Talk/Collective/Calendar buttons but no 'teamfolder'
135+ const buttons = wrapper . findAllComponents ( { name : 'TeamResourceButton' } )
136+ const ids = buttons . map ( ( b ) => b . props ( 'resourceType' ) ?. id )
137+ expect ( ids ) . not . toContain ( 'teamfolder' )
138+ expect ( ids ) . not . toContain ( 'folder' )
139+ } )
140+
141+ it ( 'renders no folder button when a team folder already exists' , async ( ) => {
142+ const wrapper = mountDetails ( { } , {
143+ autoCreate : true ,
144+ providerAvailable : true ,
145+ teamFolder : { id : 42 , mountPoint : 'Marketing' } ,
146+ } )
147+ await vi . waitFor ( ( ) => expect ( wrapper . vm . teamFolder ) . not . toBeNull ( ) )
148+ expect ( wrapper . vm . folderButtonType ) . toBeNull ( )
149+ } )
150+ } )
151+
152+ describe ( 'CircleDetails team folder upgrade banner' , ( ) => {
153+ beforeEach ( ( ) => {
154+ vi . clearAllMocks ( )
155+ } )
156+
157+ it ( 'renders the banner with create text for owners when provider is available' , async ( ) => {
158+ const wrapper = mountDetails ( { } , { autoCreate : true , providerAvailable : true } )
159+ await vi . waitFor ( ( ) => expect ( wrapper . vm . loadingTeamFolder ) . toBe ( false ) )
160+
161+ expect ( wrapper . vm . showTeamFolderBanner ) . toBe ( true )
162+ const html = wrapper . html ( )
163+ expect ( html ) . toContain ( 'Create one to share files with the whole team.' )
164+ expect ( html ) . toContain ( 'Create team folder' )
165+ } )
166+
167+ it ( 'renders the banner with "ask owner" text for non-owners' , async ( ) => {
168+ const wrapper = mountDetails ( { initiator : MEMBER_INITIATOR } , { autoCreate : true , providerAvailable : true } )
169+ await vi . waitFor ( ( ) => expect ( wrapper . vm . loadingTeamFolder ) . toBe ( false ) )
170+
171+ const html = wrapper . html ( )
172+ expect ( html ) . toContain ( 'Ask a team owner to create one.' )
173+ expect ( html ) . not . toContain ( 'Create team folder' )
174+ } )
175+
176+ it ( 'renders the banner with "ask admin" text when provider is missing' , async ( ) => {
177+ const wrapper = mountDetails ( { } , { autoCreate : true , providerAvailable : false } )
178+ await vi . waitFor ( ( ) => expect ( wrapper . vm . loadingTeamFolder ) . toBe ( false ) )
179+
180+ const html = wrapper . html ( )
181+ expect ( html ) . toContain ( 'Ask your administrator to enable the Team Folders app.' )
182+ expect ( html ) . not . toContain ( 'Create team folder' )
183+ } )
184+
185+ it ( 'hides the banner when a team folder exists' , async ( ) => {
186+ const wrapper = mountDetails ( { } , {
187+ autoCreate : true ,
188+ providerAvailable : true ,
189+ teamFolder : { id : 42 , mountPoint : 'Marketing' } ,
190+ } )
191+ await vi . waitFor ( ( ) => expect ( wrapper . vm . teamFolder ) . not . toBeNull ( ) )
192+
193+ expect ( wrapper . vm . showTeamFolderBanner ) . toBe ( false )
194+ const html = wrapper . html ( )
195+ expect ( html ) . not . toContain ( 'This team does not have a team folder yet.' )
196+ } )
197+
198+ it ( 'hides the banner while loading' , ( ) => {
199+ const wrapper = mountDetails ( { } , { autoCreate : true , providerAvailable : true } )
200+ // loadTeamFolder is in-flight; loadingTeamFolder is true
201+ expect ( wrapper . vm . loadingTeamFolder ) . toBe ( true )
202+ expect ( wrapper . vm . showTeamFolderBanner ) . toBe ( false )
203+ } )
204+
205+ it ( 'shows the banner even when auto-create is off (owner can still create)' , async ( ) => {
206+ const wrapper = mountDetails ( { } , { autoCreate : false , providerAvailable : true } )
207+ await vi . waitFor ( ( ) => expect ( wrapper . vm . loadingTeamFolder ) . toBe ( false ) )
208+
209+ const html = wrapper . html ( )
210+ expect ( html ) . toContain ( 'Create one to share files with the whole team.' )
211+ expect ( html ) . toContain ( 'Create team folder' )
212+ } )
213+ } )
0 commit comments