[chore] added feature spec for file picker

- select and deselect and select all
- navigating with carets and breadcrumbs
- check for disabled state
pull/11571/head
Eric Schubert 2 years ago
parent caeabef057
commit af13efe36d
No known key found for this signature in database
GPG Key ID: 1D346C019BD4BAA2
  1. 1
      frontend/src/app/shared/components/file-links/file-link-list/file-link-list.html
  2. 3
      frontend/src/app/shared/components/file-links/file-picker-modal/file-picker-modal.html
  3. 2
      frontend/src/app/shared/components/file-links/storage-file-list-item/storage-file-list-item.html
  4. 2
      frontend/src/app/spot/components/breadcrumbs/breadcrumbs.component.html
  5. 7
      modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud_storage_query.rb
  6. 180
      modules/storages/spec/factories/webdav_data_factory.rb
  7. 109
      modules/storages/spec/features/create_file_links_spec.rb
  8. 104
      modules/storages/spec/support/components/file_picker_dialog.rb
  9. 1
      spec/factories/oauth_client_token_factory.rb

@ -39,6 +39,7 @@
<button
type="button"
class="spot-link op-file-list--action-button"
data-qa-selector="op-file-list--link-existing-file-button"
(click)="openLinkFilesDialog()"
>
<span class="spot-icon spot-icon_add-link"></span>

@ -1,5 +1,6 @@
<div
class="spot-modal spot-modal_wide"
data-qa-selector="op-files-picker-modal"
>
<div class="spot-modal--header">
<span class="spot-modal--header-title">{{text.header}}</span>
@ -43,6 +44,7 @@
<button
type="button"
class="spot-link spot-action-bar--action"
data-qa-selector="op-files-picker-modal--select-all"
(click)="selectAllOfCurrentLevel()"
>
<span class="spot-icon spot-icon_select-all"></span>
@ -59,6 +61,7 @@
<button
type="button"
class="button spot-action-bar--action"
data-qa-selector="op-files-picker-modal--confirm"
[ngClass]="{ '-highlight': selectedFileCount > 0 }"
[disabled]="selectedFileCount === 0"
(click)="createSelectedFileLinks()"

@ -10,6 +10,7 @@
<label
slot="trigger"
class="spot-list--item-action"
data-qa-selector="op-files-picker-modal--list-item"
[ngClass]="{ 'spot-list--item-action_disabled': content.disabled }"
>
<spot-checkbox
@ -45,6 +46,7 @@
<button
*ngIf="content.isDirectory"
class="spot-link"
data-qa-selector="op-files-picker-modal--list-item-caret"
(click)="content.enterDirectory()"
>
<span class="spot-icon spot-icon_arrow-right2"></span>

@ -4,6 +4,7 @@
<div
*ngIf="(first && !last) || i === content.crumbs.length - 2 || i === content.crumbs.length - 3"
class="spot-breadcrumbs--crumb"
data-qa-selector="op-breadcrumb"
[ngClass]="{
'spot-breadcrumbs--crumb_root': first,
'spot-breadcrumbs--crumb_parent': i === content.crumbs.length - 2,
@ -34,6 +35,7 @@
<div
*ngIf="last"
class="spot-breadcrumbs--crumb spot-breadcrumbs--crumb_last"
data-qa-selector="op-breadcrumb"
>
<span *ngIf="crumb.icon" class="spot-icon spot-icon_{{crumb.icon}}"></span>
<span

@ -96,18 +96,19 @@ module Storages::Peripherals::StorageInteraction
end
def storage_file(file_element)
name = name(file_element)
location = name(file_element)
name = CGI.unescape(location.split('/').last)
::Storages::StorageFile.new(
id(file_element),
CGI.unescape(name.split('/').last),
name,
size(file_element),
mime_type(file_element),
nil,
last_modified_at(file_element),
created_by(file_element),
nil,
"/#{name}"
location
)
end

@ -0,0 +1,180 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
FactoryBot.define do
factory :webdav_data, class: 'String' do
skip_create
initialize_with do
Nokogiri::XML::Builder.new do |xml|
xml['d'].multistatus(
'xmlns:d' => 'DAV:',
'xmlns:s' => 'http://sabredav.org/ns',
'xmlns:oc' => 'http://owncloud.org/ns',
'xmlns:nc' => 'http://nextcloud.org/ns'
) do
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('6')
xml['oc'].size('20028269')
xml['d'].getlastmodified('Fri, 28 Oct 2022 14:27:36 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
xml['d'].propstat do
xml['d'].prop do
xml['d'].getcontenttype
end
xml['d'].status('HTTP/1.1 404 Not Found')
end
end
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/Folder1/')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('11')
xml['oc'].size('6592')
xml['d'].getlastmodified('Fri, 28 Oct 2022 14:31:26 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
xml['d'].propstat do
xml['d'].prop do
xml['d'].getcontenttype
end
xml['d'].status('HTTP/1.1 404 Not Found')
end
end
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/README.md')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('12')
xml['oc'].size('1024')
xml['d'].getcontenttype('text/markdown')
xml['d'].getlastmodified('Thu, 14 Jul 2022 08:42:15 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
end
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/Manual.pdf')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('13')
xml['oc'].size('12706214')
xml['d'].getcontenttype('application/pdf')
xml['d'].getlastmodified('Thu, 14 Jul 2022 08:42:15 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
end
end
end.to_xml
end
end
factory :webdav_data_folder, class: 'String' do
skip_create
initialize_with do
Nokogiri::XML::Builder.new do |xml|
xml['d'].multistatus(
'xmlns:d' => 'DAV:',
'xmlns:s' => 'http://sabredav.org/ns',
'xmlns:oc' => 'http://owncloud.org/ns',
'xmlns:nc' => 'http://nextcloud.org/ns'
) do
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/Folder1')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('11')
xml['oc'].size('6592')
xml['d'].getlastmodified('Fri, 28 Oct 2022 14:31:26 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
xml['d'].propstat do
xml['d'].prop do
xml['d'].getcontenttype
end
xml['d'].status('HTTP/1.1 404 Not Found')
end
end
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/Folder1/logo.png')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('21')
xml['oc'].size('2048')
xml['d'].getcontenttype('image/png')
xml['d'].getlastmodified('Fri, 28 Oct 2022 14:31:26 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
end
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/Folder1/jingle.ogg')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('22')
xml['oc'].size('22736218')
xml['d'].getcontenttype('audio/ogg')
xml['d'].getlastmodified('Fri, 28 Oct 2022 14:31:26 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
end
xml['d'].response do
xml['d'].href('/remote.php/dav/files/admin/Folder1/notes.txt')
xml['d'].propstat do
xml['d'].prop do
xml['oc'].fileid('23')
xml['oc'].size('128')
xml['d'].getcontenttype('text/plain')
xml['d'].getlastmodified('Fri, 28 Oct 2022 14:31:26 GMT')
xml['oc'].send('owner-display-name', 'admin')
end
xml['d'].status('HTTP/1.1 200 OK')
end
end
end
end.to_xml
end
end
end

@ -0,0 +1,109 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require_relative '../spec_helper'
describe 'Creating file links in work package', webmock: true, type: :feature, js: true do
let(:permissions) { %i(view_work_packages edit_work_packages view_file_links manage_file_links) }
let(:project) { create(:project) }
let(:current_user) { create(:user, member_in_project: project, member_with_permissions: permissions) }
let(:work_package) { create(:work_package, project:, description: 'Initial description') }
let(:storage) { create(:storage) }
let(:oauth_client) { create(:oauth_client, integration: storage) }
let(:oauth_client_token) { create(:oauth_client_token, oauth_client:, user: current_user) }
let(:project_storage) { create(:project_storage, project:, storage:) }
let(:file_link) { create(:file_link, container: work_package, storage:, origin_id: '22', origin_name: 'jingle.ogg') }
let(:connection_manager) do
connection_manager = instance_double(::OAuthClients::ConnectionManager)
allow(connection_manager).to receive(:refresh_token).and_return(ServiceResult.success(result: oauth_client_token))
allow(connection_manager).to receive(:get_access_token).and_return(ServiceResult.success(result: oauth_client_token))
allow(connection_manager).to receive(:authorization_state).and_return(:connected)
allow(connection_manager).to receive(:with_refreshed_token).and_yield
connection_manager
end
let(:root_xml_response) { create(:webdav_data) }
let(:folder1_xml_response) { create(:webdav_data_folder) }
let(:sync_service) do
sync_service = instance_double(::Storages::FileLinkSyncService)
allow(sync_service).to receive(:call) do |file_links|
ServiceResult.success(result: file_links.each { |file_link| file_link.origin_permission = :view })
end
sync_service
end
let(:wp_page) { ::Pages::FullWorkPackage.new(work_package, project) }
let(:dialog) { ::Components::FilePickerDialog.new }
before do
allow(::OAuthClients::ConnectionManager).to receive(:new).and_return(connection_manager)
allow(::Storages::FileLinkSyncService).to receive(:new).and_return(sync_service)
stub_request(:propfind, "#{storage.host}/remote.php/dav/files/#{oauth_client_token.origin_user_id}").to_return(status: 207, body: root_xml_response, headers: {})
stub_request(:propfind, "#{storage.host}/remote.php/dav/files/#{oauth_client_token.origin_user_id}/Folder1").to_return(status: 207, body: folder1_xml_response, headers: {})
project_storage
file_link
login_as current_user
wp_page.visit_tab! :files
end
describe 'with the file picker' do
it 'must enable the user to link existing files on the storage' do
expect(wp_page.all('[data-qa-selector="file-list--item"]').size).to eq 1
wp_page.find('[data-qa-selector="op-file-list--link-existing-file-button"]').click
dialog.expect_open
dialog.confirm_button_state(selection_count: 0)
dialog.select_file('Manual.pdf')
dialog.confirm_button_state(selection_count: 1)
dialog.enter_folder('Folder1')
dialog.has_list_item(text: 'jingle.ogg', checked: true, disabled: true)
dialog.select_all
dialog.confirm_button_state(selection_count: 3)
dialog.select_file('notes.txt')
dialog.confirm_button_state(selection_count: 2)
dialog.use_breadcrumb(position: 'root')
dialog.has_list_item(text: 'Manual.pdf', checked: true, disabled: false)
dialog.confirm
sleep 15
expect(wp_page.all('[data-qa-selector="file-list--item"]').size).to eq 3
end
end
end

@ -0,0 +1,104 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Components
class FilePickerDialog
include Capybara::DSL
include RSpec::Matchers
def container
'[data-qa-selector="op-files-picker-modal"]'
end
def expect_open
expect(page).to have_selector(container)
end
def confirm_button_state(selection_count:)
page.within(container) do
if selection_count > 0
expect(page).to have_button(disabled: false,
exact_text: I18n.t('js.storages.file_links.selection_any', number: selection_count))
else
expect(page).to have_button(disabled: true,
exact_text: I18n.t('js.storages.file_links.selection_none'))
end
end
end
def confirm
page.within(container) do
page.find('[data-qa-selector="op-files-picker-modal--confirm"]').click
end
end
def select_file(text)
page.within(container) do
page.find('[data-qa-selector="op-files-picker-modal--list-item"]', text: text).click
end
end
def has_list_item(text:, checked:, disabled:)
page.within(container) do
expect(page.find('[data-qa-selector="op-files-picker-modal--list-item"]', text:))
.to have_field(type: 'checkbox', checked:, disabled:)
end
end
def enter_folder(text)
page.within(container) do
page.within('[data-qa-selector="op-files-picker-modal--list-item"]', text: text) do
page.find('[data-qa-selector="op-files-picker-modal--list-item-caret"]').click
end
end
end
def select_all
page.within(container) do
page.find('[data-qa-selector="op-files-picker-modal--select-all"]').click
end
end
def use_breadcrumb(position: 'root' | 'grandparent' | 'parent')
page.within(container) do
crumbs = page.all('[data-qa-selector="op-breadcrumb"]')
case position
when 'root'
expect(crumbs.size).to be > 1
crumbs[0].click
when 'parent'
expect(crumbs.size).to be > 2
crumbs[-2].click
when 'grandparent'
expect(crumbs.size).to be > 3
crumbs[-3].click
end
end
end
end
end

@ -32,5 +32,6 @@ FactoryBot.define do
sequence(:refresh_token) { |n| "2345678901-#{n}" }
oauth_client factory: :oauth_client
user factory: :user
origin_user_id { 'admin' }
end
end

Loading…
Cancel
Save