diff --git a/docs/documentation/docs/controls/ListItemAttachments.md b/docs/documentation/docs/controls/ListItemAttachments.md index 4e91d49d3..90d25fe7e 100644 --- a/docs/documentation/docs/controls/ListItemAttachments.md +++ b/docs/documentation/docs/controls/ListItemAttachments.md @@ -29,6 +29,25 @@ import { ListItemAttachments } from '@pnp/spfx-controls-react/lib/ListItemAttach disabled={false} /> ``` +- If You want to use `ListItemAttachments` controls with new form You have to use React.createRef. + +Following example will add selected attachments to list item with id = 1 + +```TypeScript +let listItemAttachmentsComponentReference = React.createRef(); +... + +... +{ + //@ts-ignore + listItemAttachmentsComponentReference.current.uploadAttachments(1); + }} /> +``` + ## Implementation The `ListItemAttachments` control can be configured with the following properties: @@ -37,7 +56,7 @@ The `ListItemAttachments` control can be configured with the following propertie | Property | Type | Required | Description | | ---- | ---- | ---- | ---- | | context | BaseComponentContext | yes | SPFx web part or extention context | -| itemId | number | yes | List Item Id | +| itemId | number | no | List Item Id | | listId | string | yes | Guid of the list. | | webUrl | string | no | URL of the site. By default it uses the current site URL. | | disabled | boolean | no | Specifies if the control is disabled or not. | diff --git a/package-lock.json b/package-lock.json index cf85d809a..53e88e005 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49590,8 +49590,7 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "optional": true + "dev": true }, "moment": { "version": "2.29.1", diff --git a/package.json b/package.json index 2313a99e9..6b15d4417 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build": "gulp build", "clean": "gulp clean", "test": "gulp test", + "test:unit": "npx jest --silent --maxWorkers=4", "serve": "gulp bundle --custom-serve --max_old_space_size=4096 && fast-serve", "prepublishOnly": "gulp", "versionUpdater": "gulp versionUpdater", @@ -126,27 +127,8 @@ "@microsoft/sp-webpart-base": "identity-obj-proxy", "@microsoft/sp-core-library": "identity-obj-proxy", "@microsoft/sp-application-base": "identity-obj-proxy", - "office-ui-fabric-react/lib/FocusZone": "identity-obj-proxy", - "office-ui-fabric-react/lib/List": "identity-obj-proxy", - "office-ui-fabric-react/lib/Spinner": "identity-obj-proxy", - "office-ui-fabric-react/lib/Image": "identity-obj-proxy", - "office-ui-fabric-react/lib/Button": "identity-obj-proxy", - "office-ui-fabric-react/lib/components/Button": "identity-obj-proxy", - "office-ui-fabric-react/lib/Selection": "identity-obj-proxy", - "office-ui-fabric-react/lib/Icon": "identity-obj-proxy", - "office-ui-fabric-react/lib/Styling": "identity-obj-proxy", - "office-ui-fabric-react/lib/Check": "identity-obj-proxy", - "office-ui-fabric-react/lib/DetailsList": "identity-obj-proxy", - "office-ui-fabric-react/lib/CommandBar": "identity-obj-proxy", - "office-ui-fabric-react/lib/ContextualMenu": "identity-obj-proxy", - "office-ui-fabric-react/lib/ScrollablePane": "identity-obj-proxy", - "office-ui-fabric-react/lib/Breadcrumb": "identity-obj-proxy", - "office-ui-fabric-react/lib/Link": "identity-obj-proxy", - "office-ui-fabric-react/lib/Dialog": "identity-obj-proxy", - "office-ui-fabric-react/lib/common/DirectionalHint": "identity-obj-proxy", - "office-ui-fabric-react/lib/Persona": "identity-obj-proxy", - "office-ui-fabric-react/lib/HoverCard": "identity-obj-proxy", - "office-ui-fabric-react/lib/components/Icon": "identity-obj-proxy", + "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", + "src/common/telemetry/(.*)$":"identity-obj-proxy", "@pnp/sp": "identity-obj-proxy", "'@pnp/sp/fields": "identity-obj-proxy", "ControlStrings": "identity-obj-proxy", diff --git a/src/controls/listItemAttachments/IListItemAttachmentsProps.ts b/src/controls/listItemAttachments/IListItemAttachmentsProps.ts index 931457011..2edb2273f 100644 --- a/src/controls/listItemAttachments/IListItemAttachmentsProps.ts +++ b/src/controls/listItemAttachments/IListItemAttachmentsProps.ts @@ -2,7 +2,7 @@ import { BaseComponentContext } from '@microsoft/sp-component-base'; export interface IListItemAttachmentsProps { listId: string; - itemId: number; + itemId?: number; className?: string; webUrl?:string; disabled?: boolean; diff --git a/src/controls/listItemAttachments/IListItemAttachmentsState.ts b/src/controls/listItemAttachments/IListItemAttachmentsState.ts index 518ea0454..f6fb1eb75 100644 --- a/src/controls/listItemAttachments/IListItemAttachmentsState.ts +++ b/src/controls/listItemAttachments/IListItemAttachmentsState.ts @@ -8,4 +8,6 @@ export interface IListItemAttachmentsState { disableButton: boolean; showPlaceHolder: boolean; fireUpload: boolean; + filesToUpload?: File[]; + itemId?: number; } diff --git a/src/controls/listItemAttachments/IUploadAttachmentProps.ts b/src/controls/listItemAttachments/IUploadAttachmentProps.ts index f29a8191c..f25611dc0 100644 --- a/src/controls/listItemAttachments/IUploadAttachmentProps.ts +++ b/src/controls/listItemAttachments/IUploadAttachmentProps.ts @@ -2,11 +2,11 @@ import { BaseComponentContext } from '@microsoft/sp-component-base'; export interface IUploadAttachmentProps { listId: string; - itemId: number; + itemId?: number; className?: string; - webUrl?:string; + webUrl?: string; disabled?: boolean; context: BaseComponentContext; - fireUpload?:boolean; - onAttachmentUpload: () => void; + fireUpload?: boolean; + onAttachmentUpload: (file?: File) => void; } diff --git a/src/controls/listItemAttachments/ListItemAttachments.tsx b/src/controls/listItemAttachments/ListItemAttachments.tsx index 0c7aae69a..fc2ea7ba7 100755 --- a/src/controls/listItemAttachments/ListItemAttachments.tsx +++ b/src/controls/listItemAttachments/ListItemAttachments.tsx @@ -29,14 +29,13 @@ interface IPreviewImageCollection { export class ListItemAttachments extends React.Component { private _spservice: SPservice; - private previewImages: IPreviewImageCollection; + private previewImages: IPreviewImageCollection = {}; private _utilities: utilities; constructor(props: IListItemAttachmentsProps) { super(props); telemetry.track('ReactListItemAttachments', {}); - this.state = { file: null, hideDialog: true, @@ -45,7 +44,9 @@ export class ListItemAttachments extends React.Component { @@ -73,34 +74,69 @@ export class ListItemAttachments extends React.Component { - const filePreviewImages = files.map(file => this.loadAttachmentPreview(file)); - return Promise.all(filePreviewImages).then(filePreviews => { - this.previewImages = {}; - filePreviews.forEach(preview => { - this.previewImages[preview.name] = preview; - }); + public async uploadAttachments(itemId: number){ + if(this.state.filesToUpload){ + await Promise.all(this.state.filesToUpload.map(file=>this._spservice.addAttachment( + this.props.listId, + itemId, + file.name, + file, + this.props.webUrl))); + } + return new Promise((resolve,error)=>{ + this.setState({ + filesToUpload: [], + itemId: itemId + },()=>this.loadAttachments().then(resolve)); + }); + } + protected loadAttachmentsPreview(files: IListItemAttachmentFile[]){ + const filePreviewImages = files.map(file => this.loadAttachmentPreview(file)); + return Promise.all(filePreviewImages).then(filePreviews => { + filePreviews.forEach(preview => { + this.previewImages[preview.name] = preview; + }); - this.setState({ + this.setState({ fireUpload: false, hideDialog: true, dialogMessage: '', attachments: files, showPlaceHolder: files.length === 0 ? true : false }); + }); + } + /** + * Load Item Attachments + */ + private async loadAttachments() { + if(this.state.itemId){ + await this._spservice.getListItemAttachments(this.props.listId, this.state.itemId).then(async (files: IListItemAttachmentFile[]) => { + await this.loadAttachmentsPreview(files); + }).catch((error: Error) => { + this.setState({ + fireUpload: false, + hideDialog: false, + dialogMessage: strings.ListItemAttachmentserrorLoadAttachments.replace('{0}', error.message) + }); }); - }).catch((error: Error) => { - this.setState({ - fireUpload: false, - hideDialog: false, - dialogMessage: strings.ListItemAttachmentserrorLoadAttachments.replace('{0}', error.message) - }); + } + else if(this.state.filesToUpload && this.state.filesToUpload.length > 0){ + let files = this.state.filesToUpload.map(file=>({ + FileName: file.name, + ServerRelativeUrl: undefined + })); + await this.loadAttachmentsPreview(files); + } + else{ + this.setState({ + fireUpload: false, + hideDialog: true, + dialogMessage: '', + showPlaceHolder: true }); } + } /** * Close the dialog @@ -120,9 +156,16 @@ export class ListItemAttachments extends React.Component { + private _onAttachmentUpload = async (file: File) => { // load Attachments - this.loadAttachments(); + if(!this.state.itemId){ + let files = this.state.filesToUpload || []; + files.push(file); + this.setState({ + filesToUpload: [...files] + }); + } + await this.loadAttachments(); } /** @@ -153,8 +196,26 @@ export class ListItemAttachments extends React.Componentf.name === file.FileName); + if(fileToRemove){ + filesToUpload.splice(filesToUpload.indexOf(fileToRemove),1); + } + let attachments = this.state.attachments; + let attachmentToRemove = attachments.find(attachment => attachment.FileName === file.FileName); + if(attachmentToRemove){ + attachments.splice(attachments.indexOf(attachmentToRemove),1); + } + this.setState({ + filesToUpload: [...filesToUpload], + attachments: [...attachments] + }); + } this.setState({ fireUpload: false, hideDialog: false, @@ -183,7 +244,7 @@ export class ListItemAttachments extends React.Component { - this.setState({ - file: file - }); - - try { - await this._spservice.addAttachment(this.props.listId, this.props.itemId, file.name, file, this.props.webUrl); - - this.setState({ - isLoading: false - }); - this.props.onAttachmentUpload(); - } catch (error) { + return new Promise((resolve,errorCallback)=>{ + reader.onloadend = async () => { this.setState({ - hideDialog: false, - isLoading: false, - dialogMessage: strings.ListItemAttachmentsuploadAttachmentErrorMsg.replace('{0}', file.name).replace('{1}', error.message) + file: file }); - } - }; - reader.readAsDataURL(file); + + try { + if(this.props.itemId && this.props.itemId > 0){ + await this._spservice.addAttachment(this.props.listId, this.props.itemId, file.name, file, this.props.webUrl); + } + + this.setState({ + isLoading: false + }); + this.props.onAttachmentUpload(file); + resolve(); + } catch (error) { + this.setState({ + hideDialog: false, + isLoading: false, + dialogMessage: strings.ListItemAttachmentsuploadAttachmentErrorMsg.replace('{0}', file.name).replace('{1}', error.message) + }); + errorCallback(error); + } + }; + reader.readAsDataURL(file); + }); + } /** diff --git a/tests/controls/documentLibraryBrowser/DocumentLibraryBrowser.test.tsx b/tests/controls/documentLibraryBrowser/DocumentLibraryBrowser.test.tsx index 32a134236..c1cbf0880 100644 --- a/tests/controls/documentLibraryBrowser/DocumentLibraryBrowser.test.tsx +++ b/tests/controls/documentLibraryBrowser/DocumentLibraryBrowser.test.tsx @@ -26,7 +26,7 @@ describe("", ()=>{ }} />); - assert.equal(documentLibraryBrowser.getDOMNode().tagName, "SPINNER"); + assert.equal(documentLibraryBrowser.getDOMNode().tagName, "DIV"); await documentLibraryBrowser.instance().componentDidMount(); documentLibraryBrowser.update(); @@ -53,8 +53,8 @@ describe("", ()=>{ let libraryTitle = documentLibraryBrowser.instance()._onRenderLibraryTile(browserService.getSiteMediaLibrariesResult[0],0); let iconControl = libraryTitle.props.children.props.children.props.children[0]; let buttonControl = libraryTitle.props.children.props.children.props.children[1]; - assert.equal(iconControl.type,"Image"); - assert.equal(buttonControl.type,"DefaultButton"); + assert.equal(iconControl.type.displayName,"StyledImageBase"); + assert.equal(buttonControl.type.displayName,"CustomizedDefaultButton"); }); test("should call onOpenLibrary", async ()=>{ let asserted = false; diff --git a/tests/controls/filePicker/FileBrowser.test.tsx b/tests/controls/filePicker/FileBrowser.test.tsx new file mode 100644 index 000000000..28be9e783 --- /dev/null +++ b/tests/controls/filePicker/FileBrowser.test.tsx @@ -0,0 +1,199 @@ +/// +import * as React from "react"; +import { mount, configure } from "enzyme"; +import * as Adapter from 'enzyme-adapter-react-16'; +import { FileBrowser } from "../../../src/controls/filePicker/controls/FileBrowser/FileBrowser"; +import { MockFileBrowserService } from "../../mock/services/MockFileBrowserService"; +import { IFile } from "../../../src/services/FileBrowserService.types"; +import { assert } from "chai"; +configure({ adapter: new Adapter() }); + +describe("", ()=>{ + test("should render loading animation", async ()=>{ + //The purpose of this test is to make sure we will see the loading animation before anything else. + let mockFileBrowserService: MockFileBrowserService = new MockFileBrowserService(); + let component = mount( {}} + onOpenFolder={(folder: IFile) => {}} + />); + //as in package.json we map spinner to lib-common spinner mount will actually render the whole thing + //so let's try to find our spinner by class name + let spinner = component.getDOMNode().querySelector(".ms-Spinner-circle"); + assert.isOk(spinner); + }); + test("should load initial data", async ()=>{ + //In this test we will call componentDidMount manually + //As mounting in test context will not trigger full component lifecycle we have to do it on our own + //(It won't be a case if we were to use @testing-library/react but this is a topic for another time:) ) + let mockFileBrowserService: MockFileBrowserService = new MockFileBrowserService(); + //FileBrowser uses getListItems method on fileBrowserService. + //Our mock already provides that method, so let's assign our mock data + mockFileBrowserService.getListItemsResult = { + nextHref: undefined, + items: [{ + name: "Test file", + absoluteUrl: "https://test.sharepoint.com/sites/tea-point/Shared Documents/TestFile.docx", + serverRelativeUrl: "/sites/tea-point/Shared Documents/TestFile.docx", + isFolder: false, + modified: "", + fileIcon: "", + fileType: "docx", + // URL required to generate thumbnail preview + spItemUrl: "", + supportsThumbnail: false + },{ + name: "Another test file", + absoluteUrl: "https://test.sharepoint.com/sites/tea-point/Shared Documents/AnotherTestFile.docx", + serverRelativeUrl: "/sites/tea-point/Shared Documents/AnotherTestFile.docx", + isFolder: false, + modified: "", + fileIcon: "", + fileType: "docx", + // URL required to generate thumbnail preview + spItemUrl: "", + supportsThumbnail: false + }] + } + //As we also want to validate correct data is passed to getListItem method, let's add some assertion in our mock event + //Don't hesitate to peek the mock definition to check out how it's done. + //To avoid false positives I declare asserted variable. + //It's value will be set to true in event handler and asserted at the end of this test + let asserted = false; + mockFileBrowserService.onGetListItems = (listUrl,folderPath,acceptedExtensions,nextPageParams)=>{ + assert.equal(listUrl, "Shared Documents"); + assert.equal(folderPath,"/"); + assert.deepEqual(acceptedExtensions,["docx","xlsx"]); + //nextPageParams should be falsy in this case + assert.isNotOk(nextPageParams); + asserted = true; + } + let component = mount( {}} + onOpenFolder={(folder: IFile) => {}} + />); + //You could wonder - why not to test loading animation here instead of the previous test? + //I want to avoid situation in which broken loading animation would affect test for loading data. + //Those are two different functionalities I wish to test separately + //as promised - here we are manually call lifecycle method. Note await keyword. + await component.instance().componentDidMount(); + + //Now, let's make sure our component is rerendered + component.update(); + //And now let's find our rows. Note we are using lib-commonjs to mock details list which allows us to fully render the component + let dataRows = component.getDOMNode().querySelectorAll('[data-automationid="DetailsRowFields"]') + //We expect to have to rows. Content of each should be just the name of the document. + assert.equal(dataRows[0].textContent,"Test file"); + assert.equal(dataRows[1].textContent,"Another test file"); + //And finally let's make sure we asserted the call to getListItems + assert.isTrue(asserted); + }); + test("should handle folder change", async ()=>{ + //In this test we want to assert if changing the folder will trigger proper event + //Same as previously we will have to call lifecycle events and click handlers on our own + let mockFileBrowserService: MockFileBrowserService = new MockFileBrowserService(); + //First let define first Mock Data + let mockData = { + nextHref: undefined, + items: [{ + name: "Test Folder", + absoluteUrl: "https://test.sharepoint.com/sites/tea-point/Shared Documents/Test Folder", + serverRelativeUrl: "/sites/tea-point/Shared Documents/Test Folder", + isFolder: true, + modified: "", + fileIcon: "", + fileType: "folder", + // URL required to generate thumbnail preview + spItemUrl: "", + supportsThumbnail: false + }] + }; + mockFileBrowserService.getListItemsResult = mockData; + + //Let's mount our component... + //The key to this test is to pass onOpenFolder method that will assert validity of the event + let asserted = false; + let component = mount( {}} + onOpenFolder={(folder: IFile) => { + assert.deepEqual(folder,mockData.items[0]); + asserted = true; + }} + />); + //...and await relevant event + await component.instance().componentDidMount(); + component.update(); + + //Now we want to mock click event. There are two ways around it. One possibility is to send click event on some element. + //The other one is to call private method of our component with specific argument. In our case, that would be our folder. + //In this case I would lean toward the second option. The first one could fail if exception occur in DetailsList and we don't have to worry about it. + //However I do plan to include test sample with mocking external components (Will be more useful for functional components) + //@ts-ignore + component.instance()._handleItemInvoked(mockData.items[0]); + + assert.isTrue(asserted); + }); + test("should handle item change", async ()=>{ + //In this test we want to assert if selecting a file will trigger proper event + //Same as previously we will have to call lifecycle events and click handlers on our own + let mockFileBrowserService: MockFileBrowserService = new MockFileBrowserService(); + //First let define first Mock Data + let mockData = { + nextHref: undefined, + items: [{ + name: "Test File", + absoluteUrl: "https://test.sharepoint.com/sites/tea-point/Shared Documents/Test File.docx", + serverRelativeUrl: "/sites/tea-point/Shared Documents/Test File.docx", + isFolder: false, + modified: "", + fileIcon: "", + fileType: "docx", + // URL required to generate thumbnail preview + spItemUrl: "", + supportsThumbnail: false + }] + }; + mockFileBrowserService.getListItemsResult = mockData; + //Also let's define our expected file + const expectedFilePicked = { + fileName: "Test File", + fileNameWithoutExtension: "Test File", + fileAbsoluteUrl: "https://test.sharepoint.com/sites/tea-point/Shared Documents/Test File.docx", + spItemUrl: "", + downloadFileContent: null + } + //Let's mount our component... + //The key to this test is to pass onChange method that will assert validity of the event + let asserted = false; + let component = mount( { + assert.deepEqual(filePickerResult,expectedFilePicked); + asserted = true;}} + onOpenFolder={(folder: IFile) => {}} + />); + //...and await relevant event + await component.instance().componentDidMount(); + component.update(); + + //We can use same approach as in previous test + //@ts-ignore + component.instance()._handleItemInvoked(mockData.items[0]); + + assert.isTrue(asserted); + }); +}); \ No newline at end of file diff --git a/tests/controls/filePicker/SiteFilePickerTab.test.tsx b/tests/controls/filePicker/SiteFilePickerTab.test.tsx index eacac8797..4dadeeba3 100644 --- a/tests/controls/filePicker/SiteFilePickerTab.test.tsx +++ b/tests/controls/filePicker/SiteFilePickerTab.test.tsx @@ -8,18 +8,6 @@ import { assert } from "chai"; configure({ adapter: new Adapter() }); -jest.mock("office-ui-fabric-react/lib/Utilities",()=>({ - IRenderFunction:{ - - }, - IRectangle:{ - - }, - css: ()=>{ - - } -})) - describe("", ()=>{ test("should load initial data", async ()=>{ let browserService = new MockFileBrowserService(); diff --git a/tests/controls/listItemAttachments/ListItemAttachments.test.tsx b/tests/controls/listItemAttachments/ListItemAttachments.test.tsx new file mode 100644 index 000000000..1bcdc5a07 --- /dev/null +++ b/tests/controls/listItemAttachments/ListItemAttachments.test.tsx @@ -0,0 +1,269 @@ +/// +import * as React from "react"; +import { mount, configure } from "enzyme"; +import * as Adapter from 'enzyme-adapter-react-16'; +import { ListItemAttachments } from "../../../src/controls/listItemAttachments/ListItemAttachments"; +import { assert} from "chai"; + +configure({ adapter: new Adapter() }); + +describe("",()=>{ + test("should render item attachment", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let mockSPService = { + getListItemAttachments: (listId, itemId)=>Promise.resolve([{ + FileName : "Test file.docx", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.docx" + },{ + FileName : "Test file.xlsx", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.xlsx" + }]) + } + + let listItemAttachments = mount(); + //@ts-ignore + listItemAttachments.instance()._spservice = mockSPService; + await listItemAttachments.instance().componentDidMount(); + listItemAttachments.update(); + + let attachmentCards = listItemAttachments.getDOMNode().querySelectorAll('.ms-DocumentCard'); + let testWordCard = attachmentCards[0]; + let testExcelCard = attachmentCards[1]; + + assert.equal(testWordCard.querySelector("img").src,"https://static2.sharepointonline.com/files/fabric/assets/item-types/96/docx.png"); + assert.equal(testWordCard.textContent,"Test file.docx"); + + assert.equal(testExcelCard.querySelector("img").src,"https://static2.sharepointonline.com/files/fabric/assets/item-types/96/xlsx.png"); + assert.equal(testExcelCard.textContent,"Test file.xlsx"); + }); + test("should render placeholder if item has no attachments", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let mockSPService = { + getListItemAttachments: (listId, itemId)=>Promise.resolve([]) + } + + let listItemAttachments = mount(); + //@ts-ignore + listItemAttachments.instance()._spservice = mockSPService; + await listItemAttachments.instance().componentDidMount(); + listItemAttachments.update(); + + let placeholder = listItemAttachments.getDOMNode().querySelector('[aria-label="ListItemAttachmentslPlaceHolderButtonLabel"]'); + assert.isOk(placeholder) + }); + test("should render placeholder if no item provided", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let mockSPService = { + getListItemAttachments: (listId, itemId)=>Promise.resolve([{ + FileName : "Test file.docx", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.docx" + },{ + FileName : "Test file.xlsx", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.xlsx" + } + ]) + } + + let listItemAttachments = mount(); + //@ts-ignore + listItemAttachments.instance()._spservice = mockSPService; + await listItemAttachments.instance().componentDidMount(); + listItemAttachments.update(); + + let placeholder = listItemAttachments.getDOMNode().querySelector('[aria-label="ListItemAttachmentslPlaceHolderButtonLabel"]'); + assert.isOk(placeholder) + }); + test("should add and load attachments", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let asserted = false; + let listItemAttachmentsRef = React.createRef(); + let mockFile = new File([], "Test file.txt"); + let mockSPService = { + getListItemAttachments: (listId, itemId)=>Promise.resolve([{ + FileName : "Test file.xlsx", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.xlsx" + },{ + FileName : "Test file.txt", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.txt" + }]), + addAttachment: (listId: string, itemId: number, fileName: string, file: File, webUrl?: string)=>{ + assert.equal(listId,"test-list-id"); + assert.equal(itemId, 1); + assert.equal(file, mockFile); + asserted = true; + } + } + let listItemAttachments = mount(); + //@ts-ignore + listItemAttachments.instance()._spservice = mockSPService; + await listItemAttachments.instance().componentDidMount(); + let placeholder = listItemAttachments.getDOMNode().querySelector('[aria-label="ListItemAttachmentslPlaceHolderButtonLabel"]'); + listItemAttachments.instance().setState({ + filesToUpload:[mockFile] + }); + listItemAttachments.update(); + + assert.isOk(placeholder) + + await listItemAttachmentsRef.current.uploadAttachments(1); + + listItemAttachments.update(); + let attachmentCards = listItemAttachments.getDOMNode().querySelectorAll('.ms-DocumentCard'); + let testWordCard = attachmentCards[0]; + let testTxtCard = attachmentCards[1]; + + assert.equal(testWordCard.querySelector("img").src,"https://static2.sharepointonline.com/files/fabric/assets/item-types/96/xlsx.png"); + assert.equal(testWordCard.textContent,"Test file.xlsx"); + + assert.equal(testTxtCard.querySelector("img").src,"https://static2.sharepointonline.com/files/fabric/assets/item-types/96/txt.png"); + assert.equal(testTxtCard.textContent,"Test file.txt"); + + assert.isTrue(asserted); + }); + test("should delete without itemId provided", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let mockFile = new File([], "Test file.txt"); + let mockFileToDelete = new File([], "Test file to delete.txt"); + let listItemAttachments = mount(); + + await listItemAttachments.instance().componentDidMount(); + //@ts-ignore + await listItemAttachments.instance()._onAttachmentUpload(mockFile); + //@ts-ignore + await listItemAttachments.instance()._onAttachmentUpload(mockFileToDelete); + + listItemAttachments.update(); + + let attachmentCards = listItemAttachments.getDOMNode().querySelectorAll('.ms-DocumentCard'); + let mockFileCard = attachmentCards[0]; + let mockFileCardToDelete = attachmentCards[1]; + assert.equal(mockFileCard.textContent,"Test file.txt"); + assert.equal(mockFileCardToDelete.textContent,"Test file to delete.txt"); + + listItemAttachments.instance().setState({ + file: { + FileName: mockFileToDelete.name + } + }); + //@ts-ignore + await listItemAttachments.instance().onConfirmedDeleteAttachment(); + listItemAttachments.update(); + + attachmentCards = listItemAttachments.getDOMNode().querySelectorAll('.ms-DocumentCard'); + mockFileCard = attachmentCards[0]; + assert.equal(mockFileCard.textContent,"Test file.txt"); + assert.isNotOk(attachmentCards[1]); + }); + test("should delete with itemId provided", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let asserted = false; + let mockSPService = { + getListItemAttachments: (listId, itemId)=>Promise.resolve([]), + deleteAttachment: (fileName: string, listId: string, itemId: number)=>{ + assert.equal(fileName, "Test file.txt"); + assert.equal(listId, "test-list-id"); + assert.equal(itemId, 1); + asserted = true; + return Promise.resolve(); + } + } + jest.spyOn(mockSPService, "getListItemAttachments").mockReturnValueOnce(Promise.resolve([{ + FileName : "Test file.xlsx", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.xlsx" + },{ + FileName : "Test file.txt", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.txt" + }])); + jest.spyOn(mockSPService, "getListItemAttachments").mockReturnValueOnce(Promise.resolve([{ + FileName : "Test file.xlsx", + ServerRelativeUrl: "/sites/test-site/Shared Documents/TestFile.xlsx" + }])); + + let listItemAttachments = mount(); + + //@ts-ignore + listItemAttachments.instance()._spservice = mockSPService; + await listItemAttachments.instance().componentDidMount(); + listItemAttachments.update(); + + let attachmentCards = listItemAttachments.getDOMNode().querySelectorAll('.ms-DocumentCard'); + let mockFileCard = attachmentCards[0]; + let mockFileCardToDelete = attachmentCards[1]; + assert.equal(mockFileCard.textContent,"Test file.xlsx"); + assert.equal(mockFileCardToDelete.textContent,"Test file.txt"); + + listItemAttachments.instance().setState({ + file: { + FileName: "Test file.txt" + } + }); + //@ts-ignore + await listItemAttachments.instance().onConfirmedDeleteAttachment(); + //@ts-ignore + await listItemAttachments.instance().loadAttachments(); + listItemAttachments.update(); + + attachmentCards = listItemAttachments.getDOMNode().querySelectorAll('.ms-DocumentCard'); + mockFileCard = attachmentCards[0]; + assert.equal(mockFileCard.textContent,"Test file.xlsx"); + assert.isNotOk(attachmentCards[1]); + }) +}); \ No newline at end of file diff --git a/tests/controls/listItemAttachments/UploadAttachments.test.tsx b/tests/controls/listItemAttachments/UploadAttachments.test.tsx new file mode 100644 index 000000000..a5e95a916 --- /dev/null +++ b/tests/controls/listItemAttachments/UploadAttachments.test.tsx @@ -0,0 +1,81 @@ +/// +import * as React from "react"; +import { mount, configure } from "enzyme"; +import * as Adapter from 'enzyme-adapter-react-16'; +import { UploadAttachment } from "../../../src/controls/listItemAttachments/UploadAttachment"; +import { assert} from "chai"; + +configure({ adapter: new Adapter() }); + +describe("", ()=>{ + test("should upload document with itemId provided", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let mockFile = new File([], "Test file.txt"); + let asserted = false; + let uploadAttachmentComponent = mount({ + assert.equal(file,mockFile); + }} + />); + //@ts-ignore + uploadAttachmentComponent.instance()._spservice = { + addAttachment: (listId: string, itemId: number, fileName: string, file: File, webUrl?: string)=>{ + assert.equal(listId,"test-list-id"); + assert.equal(itemId, 1); + assert.equal(file, mockFile); + assert.equal(webUrl,"/sites/test-site"); + asserted = true; + } + } + //@ts-ignore + await uploadAttachmentComponent.instance().addAttachment({ + target:{ + files: [mockFile] + } + }); + assert.isTrue(asserted); + }); + test("should not upload document without itemId provided", async ()=>{ + let mockContext = { + pageContext:{ + web:{ + absoluteUrl: "https://test.sharepoint.com/sites/test-site" + } + } + }; + let mockFile = new File([], "Test file.txt"); + let asserted = false; + let uploadAttachmentComponent = mount({ + assert.equal(file,mockFile); + asserted = true; + }} + />); + //@ts-ignore + uploadAttachmentComponent.instance()._spservice = { + addAttachment: (listId: string, itemId: number, fileName: string, file: File, webUrl?: string)=>{ + throw Error("Upload called with no itemId!"); + } + } + //@ts-ignore + await uploadAttachmentComponent.instance().addAttachment({ + target:{ + files: [mockFile] + } + }); + assert.isTrue(asserted); + }); +}); \ No newline at end of file diff --git a/tests/mock/services/MockFileBrowserService.ts b/tests/mock/services/MockFileBrowserService.ts index 3e386751f..08f748082 100644 --- a/tests/mock/services/MockFileBrowserService.ts +++ b/tests/mock/services/MockFileBrowserService.ts @@ -3,11 +3,15 @@ import { FilesQueryResult, IFile, ILibrary } from "../../../src/services/FileBro export class MockFileBrowserService { public getListItemsResult: FilesQueryResult; public getFileThumbnailUrlResultMap: Map = new Map(); + public onGetListItems: (listUrl: string, folderPath: string, acceptedFilesExtensions?: string[], nextPageQueryStringParams?: string) => void; public getSiteMediaLibrariesResult: ILibrary[] = []; constructor() { } public getListItems = (listUrl: string, folderPath: string, acceptedFilesExtensions?: string[], nextPageQueryStringParams?: string): Promise => { + if (this.onGetListItems) { + this.onGetListItems(listUrl, folderPath, acceptedFilesExtensions, nextPageQueryStringParams); + } return Promise.resolve(this.getListItemsResult); } public getFileThumbnailUrl = (file: IFile, thumbnailWidth: number, thumbnailHeight: number): string => { diff --git a/tests/setup.js b/tests/setup.js index ed5edb141..2fb8e703e 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -18,4 +18,5 @@ jest.mock("@microsoft/decorators", () => { return control; } } -}) \ No newline at end of file +}) +DEBUG = true; \ No newline at end of file