浏览代码

Adding functional test.

dom111 2 年之前
父节点
当前提交
09fc82e1ad

+ 10 - 5
.github/workflows/build-on-push.yml

@@ -1,6 +1,12 @@
 name: Build and test application on push to remote
 name: Build and test application on push to remote
 
 
-on: [push]
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
 
 
 jobs:
 jobs:
   build:
   build:
@@ -9,7 +15,6 @@ jobs:
       - uses: actions/checkout@v1
       - uses: actions/checkout@v1
       - uses: actions/setup-node@v1
       - uses: actions/setup-node@v1
         with:
         with:
-          node-version: 12
-      - run: yarn install
-      - run: yarn build
-      - run: yarn test
+          node-version: 16
+      - run: make build
+      - run: make test

+ 6 - 13
Makefile

@@ -1,19 +1,12 @@
 SHELL = /bin/bash
 SHELL = /bin/bash
 
 
 .PHONY: build
 .PHONY: build
-build: docker-compose.override.yml
-	docker-compose run --rm web npm run build
+build: node_modules
+	npm run build
 
 
 .PHONY: test
 .PHONY: test
-test: docker-compose.override.yml
-	docker-compose run --rm test npm run test
+test: node_modules
+	docker-compose run --rm -e BASE_URL=http://webdav test npm run test
 
 
-docker-compose.override.yml:
-	@echo "version: '3'" > docker-compose.override.yml
-	@echo >> docker-compose.override.yml
-	@echo "services:" >> docker-compose.override.yml
-	@echo "  web:" >> docker-compose.override.yml
-	@echo "    user: `id -u`:`id -g`" >> docker-compose.override.yml
-
-dist/js/app.js:
-dist/js/app.js: build
+node_modules:
+	npm install

+ 1 - 2
README.md

@@ -8,7 +8,7 @@ without the need for using a third party application.
 The application has since been rewritten to not rely on jQuery and use more modern methods and provide a single runtime
 The application has since been rewritten to not rely on jQuery and use more modern methods and provide a single runtime
 file. Now that there's more separation between the interface code and the library code, I'd like to investigate using
 file. Now that there's more separation between the interface code and the library code, I'd like to investigate using
 other frontend approaches to see which I prefer (and also to weigh up the differences between the currently available
 other frontend approaches to see which I prefer (and also to weigh up the differences between the currently available
-frameworks). There's still work to do around code separation andhopefully this will be something I can continue to work
+frameworks). There's still work to do around code separation and hopefully this will be something I can continue to work
 on (as time allows) I feel it's at least as stable as the previous version.
 on (as time allows) I feel it's at least as stable as the previous version.
 
 
 ## Tested in:
 ## Tested in:
@@ -16,7 +16,6 @@ on (as time allows) I feel it's at least as stable as the previous version.
 - Chrome
 - Chrome
 - Firefox
 - Firefox
 - Edge
 - Edge
-- IE11 (I may drop support for this to reduce the package size in the future - unless anyone REALY needs it?)
 
 
 ## Implementations
 ## Implementations
 
 

+ 5 - 6
TODO.md

@@ -1,12 +1,11 @@
+- [x] Add more unit tests for the UI
+- [x] Add end-to-end UI testing
+- [x] Move to TypeScript
+- [ ] Add drag and drop tests
 - [ ] Allow uploading of directories ([#48](https://github.com/dom111/webdav-js/issues/48))
 - [ ] Allow uploading of directories ([#48](https://github.com/dom111/webdav-js/issues/48))
 - [ ] Add functionality for copying and moving files and directories
 - [ ] Add functionality for copying and moving files and directories
 - [ ] Add progress bar for file uploads
 - [ ] Add progress bar for file uploads
-- [ ] Add more unit tests for the UI
-- [ ] Add end-to-end UI testing (although it seems that drag and drop might be a problem)
 - [ ] Support keyboard navigation whilst overlay is visible
 - [ ] Support keyboard navigation whilst overlay is visible
-- [ ] Improve code in `item.js` - maybe split out the functionality into each action?
-- [ ] Look into moving to TypeScript
 - [ ] ReactJS implementation
 - [ ] ReactJS implementation
 - [ ] VueJS implementation
 - [ ] VueJS implementation
-- [ ] Native Web Components implementation
-- [ ] Angular implementation
+- [ ] Maybe a refactor...

+ 0 - 5
build/sass.sh

@@ -1,5 +0,0 @@
-> assets/css/style.css;
-for file in node_modules/{basiclightbox/src/styles/main.scss,prismjs/themes/prism.css} assets/scss/style.scss; do
-  echo '/* '$file' */' >> assets/css/style.css;
-  npm run --silent node-sass -- --output-style=expanded $file >> assets/css/style.css;
-done

+ 1 - 1
docker-compose.yml

@@ -28,4 +28,4 @@ services:
     # https://stackoverflow.com/a/53975412/3145856
     # https://stackoverflow.com/a/53975412/3145856
     # https://github.com/docker/compose/issues/5574
     # https://github.com/docker/compose/issues/5574
     security_opt:
     security_opt:
-      - seccomp:"./docker/test/chrome.json"
+      - seccomp:./docker/test/chrome.json

+ 35 - 56
index.html

@@ -44,13 +44,41 @@
         display: none;
         display: none;
       }
       }
     </style>
     </style>
+    <style>
+      .github-corner:hover .octo-arm {
+        animation: octocat-wave 560ms ease-in-out;
+      }
+      @keyframes octocat-wave {
+        0%,
+        100% {
+          transform: rotate(0);
+        }
+        20%,
+        60% {
+          transform: rotate(-25deg);
+        }
+        40%,
+        80% {
+          transform: rotate(10deg);
+        }
+      }
+      @media (max-width: 500px) {
+        .github-corner:hover .octo-arm {
+          animation: none;
+        }
+        .github-corner .octo-arm {
+          animation: octocat-wave 560ms ease-in-out;
+        }
+      }
+    </style>
   </head>
   </head>
   <body>
   <body>
     <a
     <a
       href="https://github.com/dom111/webdav-js"
       href="https://github.com/dom111/webdav-js"
       class="github-corner"
       class="github-corner"
       aria-label="View source on Github"
       aria-label="View source on Github"
-      ><svg
+    >
+      <svg
         width="80"
         width="80"
         height="80"
         height="80"
         viewBox="0 0 250 250"
         viewBox="0 0 250 250"
@@ -75,34 +103,9 @@
           d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
           d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
           fill="currentColor"
           fill="currentColor"
           class="octo-body"
           class="octo-body"
-        ></path></svg></a
-    ><style>
-      .github-corner:hover .octo-arm {
-        animation: octocat-wave 560ms ease-in-out;
-      }
-      @keyframes octocat-wave {
-        0%,
-        100% {
-          transform: rotate(0);
-        }
-        20%,
-        60% {
-          transform: rotate(-25deg);
-        }
-        40%,
-        80% {
-          transform: rotate(10deg);
-        }
-      }
-      @media (max-width: 500px) {
-        .github-corner:hover .octo-arm {
-          animation: none;
-        }
-        .github-corner .octo-arm {
-          animation: octocat-wave 560ms ease-in-out;
-        }
-      }
-    </style>
+        ></path>
+      </svg>
+    </a>
 
 
     <div class="jumbotron">
     <div class="jumbotron">
       <div class="container">
       <div class="container">
@@ -125,14 +128,14 @@
             cross-browser addition to the bookmarks of anyone that has to
             cross-browser addition to the bookmarks of anyone that has to
             interact with WebDAV. It supports previewing of many common
             interact with WebDAV. It supports previewing of many common
             filetypes (syntax highlighting for code, previews for
             filetypes (syntax highlighting for code, previews for
-            images/videos/fonts), drag and drop file uplaods and history state
+            images/videos/fonts), drag and drop file uploads and history state
             (for back button navigation).
             (for back button navigation).
           </p>
           </p>
           <p>
           <p>
             Whilst this started out as a very simple bookmarklet with some basic
             Whilst this started out as a very simple bookmarklet with some basic
             styling (and it's still not much more than that!), I'd like to
             styling (and it's still not much more than that!), I'd like to
-            continue improve it somewhat, adding in new features and using it as
-            a testbed for front-end framework experience. I'd like to
+            continue to improve it somewhat, adding in new features and using it
+            as a testbed for front-end framework experience. I'd like to
             investigate more thorough testing using it too, ideally performing
             investigate more thorough testing using it too, ideally performing
             full end-to-end testing for all the features currently implemented.
             full end-to-end testing for all the features currently implemented.
           </p>
           </p>
@@ -202,29 +205,5 @@
         event.preventDefault();
         event.preventDefault();
       });
       });
     </script>
     </script>
-    <script>
-      (function (i, s, o, g, r, a, m) {
-        i['GoogleAnalyticsObject'] = r;
-        (i[r] =
-          i[r] ||
-          function () {
-            (i[r].q = i[r].q || []).push(arguments);
-          }),
-          (i[r].l = 1 * new Date());
-        (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
-        a.async = 1;
-        a.src = g;
-        m.parentNode.insertBefore(a, m);
-      })(
-        window,
-        document,
-        'script',
-        'https://www.google-analytics.com/analytics.js',
-        'ga'
-      );
-
-      ga('create', 'UA-5273748-7', 'auto');
-      ga('send', 'pageview');
-    </script>
   </body>
   </body>
 </html>
 </html>

+ 1 - 1
jest.config.functional.ts → jest.config.functional.chrome.ts

@@ -1,4 +1,4 @@
 export default {
 export default {
   testMatch: ['**/tests/functional/**/*.ts'],
   testMatch: ['**/tests/functional/**/*.ts'],
-  preset: './tests/jest.ts-puppeteer.preset.ts',
+  preset: './tests/jest.ts-puppeteer.preset.chrome.ts',
 };
 };

+ 4 - 0
jest.config.functional.firefox.ts

@@ -0,0 +1,4 @@
+export default {
+  testMatch: ['**/tests/functional/**/*.ts'],
+  preset: './tests/jest.ts-puppeteer.preset.firefox.ts',
+};

文件差异内容过多而无法显示
+ 297 - 282
package-lock.json


+ 7 - 4
package.json

@@ -15,16 +15,19 @@
   "scripts": {
   "scripts": {
     "build": "npm run build:esbuild && npm run build:prettier:write && npm run build:examples",
     "build": "npm run build:esbuild && npm run build:prettier:write && npm run build:examples",
     "build:esbuild": "node ./esbuild.js",
     "build:esbuild": "node ./esbuild.js",
-    "build:esbuild:watch": "node ./esbuild.js",
-    "build:examples:branch": "bash build/examples-branch.sh",
+    "build:esbuild:watch": "node ./esbuild.js watch",
     "build:examples": "bash build/examples.sh",
     "build:examples": "bash build/examples.sh",
+    "build:examples:branch": "bash build/examples-branch.sh",
     "build:prettier:check": "prettier -c .",
     "build:prettier:check": "prettier -c .",
     "build:prettier:write": "prettier -w .",
     "build:prettier:write": "prettier -w .",
     "build:watch": "npm run build:esbuild:watch",
     "build:watch": "npm run build:esbuild:watch",
     "terser": "terser",
     "terser": "terser",
     "test": "npm run test:unit && npm run test:functional",
     "test": "npm run test:unit && npm run test:functional",
-    "test:functional": "jest -c jest.config.functional.ts",
-    "test:unit": "jest -c jest.config.unit.ts"
+    "test:functional": "npm run test:functional:chrome && npm run test:functional:firefox",
+    "test:functional:chrome": "jest -c jest.config.functional.chrome.ts",
+    "test:functional:firefox": "jest -c jest.config.functional.firefox.ts",
+    "test:unit": "jest -c jest.config.unit.ts",
+    "watch": "npm run build:watch"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@types/expect-puppeteer": "^4.4.7",
     "@types/expect-puppeteer": "^4.4.7",

+ 4 - 0
src/lib/DAV/Collection.ts

@@ -95,6 +95,10 @@ export default class Collection extends EventObject {
         return;
         return;
       }
       }
 
 
+      if (entry.directory && !destinationFullPath.endsWith('/')) {
+        destinationFullPath += '/';
+      }
+
       const newEntry = new Entry({
       const newEntry = new Entry({
         directory: entry.directory,
         directory: entry.directory,
         fullPath: destinationFullPath,
         fullPath: destinationFullPath,

+ 54 - 46
src/lib/DAV/Entry.ts

@@ -1,6 +1,6 @@
+import Collection from './Collection';
 import EventObject from '../EventObject';
 import EventObject from '../EventObject';
 import joinPath from '../joinPath';
 import joinPath from '../joinPath';
-import Collection from './Collection';
 
 
 type EntryArgs = {
 type EntryArgs = {
   directory?: boolean;
   directory?: boolean;
@@ -16,22 +16,22 @@ type EntryArgs = {
 };
 };
 
 
 export default class Entry extends EventObject {
 export default class Entry extends EventObject {
-  #del;
-  #directory;
-  #displaySize;
-  #extension;
-  #fullPath;
-  #mimeType;
-  #modified;
-  #name;
-  #path;
-  #placeholder;
-  #rename;
-  #size;
-  #title;
-  #type;
-
-  collection;
+  #del: boolean;
+  #directory: boolean;
+  #displaySize: string;
+  #extension: string;
+  #fullPath: string;
+  #mimeType: string;
+  #modified: Date;
+  #name: string;
+  #path: string;
+  #placeholder: boolean;
+  #rename: boolean;
+  #size: number;
+  #title: string;
+  #type: string;
+
+  collection: Collection | null;
 
 
   constructor({
   constructor({
     directory = false,
     directory = false,
@@ -47,9 +47,12 @@ export default class Entry extends EventObject {
   }: EntryArgs) {
   }: EntryArgs) {
     super();
     super();
 
 
+    const [path, name] = this.getFilename(fullPath);
+
+    this.#path = path;
+    this.#name = name;
     this.#directory = directory;
     this.#directory = directory;
     this.#fullPath = fullPath;
     this.#fullPath = fullPath;
-    [this.#path, this.#name] = this.getFilename();
     this.#title = title;
     this.#title = title;
     this.#modified = modified;
     this.#modified = modified;
     this.#size = size;
     this.#size = size;
@@ -60,7 +63,7 @@ export default class Entry extends EventObject {
     this.collection = collection;
     this.collection = collection;
   }
   }
 
 
-  createParentEntry() {
+  createParentEntry(): Entry {
     return this.update({
     return this.update({
       fullPath: this.path,
       fullPath: this.path,
       title: '&larr;',
       title: '&larr;',
@@ -69,14 +72,14 @@ export default class Entry extends EventObject {
     });
     });
   }
   }
 
 
-  getFilename(path = this.#fullPath) {
-    path = joinPath(path).split(/\//);
-    const file = path.pop();
+  getFilename(path: string): [string, string] {
+    const pathParts = joinPath(path).split(/\//),
+      file = pathParts.pop();
 
 
-    return [joinPath(...path), file];
+    return [joinPath(...pathParts), file];
   }
   }
 
 
-  update(properties: EntryArgs = {}) {
+  update(properties: EntryArgs = {}): Entry {
     return new Entry({
     return new Entry({
       ...{
       ...{
         directory: this.directory,
         directory: this.directory,
@@ -92,35 +95,40 @@ export default class Entry extends EventObject {
     });
     });
   }
   }
 
 
-  get del() {
+  get del(): boolean {
     return this.#del;
     return this.#del;
   }
   }
 
 
-  get directory() {
+  get directory(): boolean {
     return this.#directory;
     return this.#directory;
   }
   }
 
 
-  get displaySize() {
+  get displaySize(): string {
     if (this.directory) {
     if (this.directory) {
       return '';
       return '';
     }
     }
 
 
     if (!this.#displaySize) {
     if (!this.#displaySize) {
       this.#displaySize = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'].reduce(
       this.#displaySize = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'].reduce(
-        (size, label) =>
-          typeof size === 'string'
-            ? size
-            : size < 1024
-            ? `${size.toFixed(2 * (label === 'bytes' ? 0 : 1))} ${label}`
-            : size / 1024,
+        (size: string | number, label) => {
+          if (typeof size === 'string') {
+            return size;
+          }
+
+          if (size < 1024) {
+            return `${size.toFixed(2 * (label === 'bytes' ? 0 : 1))} ${label}`;
+          }
+
+          return size / 1024;
+        },
         this.#size
         this.#size
-      );
+      ) as string;
     }
     }
 
 
     return this.#displaySize;
     return this.#displaySize;
   }
   }
 
 
-  get extension() {
+  get extension(): string {
     if (this.directory) {
     if (this.directory) {
       return '';
       return '';
     }
     }
@@ -132,45 +140,45 @@ export default class Entry extends EventObject {
     return this.#extension;
     return this.#extension;
   }
   }
 
 
-  get fullPath() {
+  get fullPath(): string {
     return this.#fullPath;
     return this.#fullPath;
   }
   }
 
 
-  get mimeType() {
+  get mimeType(): string {
     return this.#mimeType;
     return this.#mimeType;
   }
   }
 
 
-  get modified() {
+  get modified(): Date {
     return this.#modified;
     return this.#modified;
   }
   }
 
 
-  get name() {
+  get name(): string {
     return this.#name;
     return this.#name;
   }
   }
 
 
-  get path() {
+  get path(): string {
     return this.#path;
     return this.#path;
   }
   }
 
 
-  get placeholder() {
+  get placeholder(): boolean {
     return this.#placeholder;
     return this.#placeholder;
   }
   }
 
 
-  set placeholder(value) {
+  set placeholder(value: boolean) {
     this.#placeholder = value;
     this.#placeholder = value;
 
 
     this.trigger('entry:update', this);
     this.trigger('entry:update', this);
   }
   }
 
 
-  get rename() {
+  get rename(): boolean {
     return this.#rename;
     return this.#rename;
   }
   }
 
 
-  get size() {
+  get size(): number {
     return this.#size;
     return this.#size;
   }
   }
 
 
-  get title() {
+  get title(): string {
     if (!this.#title) {
     if (!this.#title) {
       this.#title = decodeURIComponent(this.#name);
       this.#title = decodeURIComponent(this.#name);
     }
     }
@@ -178,7 +186,7 @@ export default class Entry extends EventObject {
     return this.#title;
     return this.#title;
   }
   }
 
 
-  get type() {
+  get type(): string {
     if (!this.#type) {
     if (!this.#type) {
       let type;
       let type;
 
 

+ 14 - 10
src/lib/DAV/Response.ts

@@ -5,15 +5,15 @@ export default class Response {
   #document;
   #document;
   #parser;
   #parser;
 
 
-  #getTag = (doc, tag) => doc.querySelector(tag);
+  #getTag = (doc: Element, tag: string) => doc.getElementsByTagName(tag)[0];
 
 
-  #getTagContent = (doc, tag) => {
+  #getTagContent = (doc: Element, tag) => {
     const node = this.#getTag(doc, tag);
     const node = this.#getTag(doc, tag);
 
 
     return node ? node.textContent : '';
     return node ? node.textContent : '';
   };
   };
 
 
-  constructor(rawDocument, parser = new DOMParser()) {
+  constructor(rawDocument: string, parser: DOMParser = new DOMParser()) {
     this.#parser = parser;
     this.#parser = parser;
     this.#document = parser.parseFromString(rawDocument, 'application/xml');
     this.#document = parser.parseFromString(rawDocument, 'application/xml');
   }
   }
@@ -21,7 +21,9 @@ export default class Response {
   collection({ sortDirectoriesFirst = false } = {}) {
   collection({ sortDirectoriesFirst = false } = {}) {
     if (!this.#collection) {
     if (!this.#collection) {
       this.#collection = new Collection(
       this.#collection = new Collection(
-        this.responseToPrimitives(this.#document.querySelectorAll('response')),
+        this.responseToPrimitives(
+          this.#document.getElementsByTagName('D:response')
+        ),
         {
         {
           sortDirectoriesFirst,
           sortDirectoriesFirst,
         }
         }
@@ -31,13 +33,15 @@ export default class Response {
     return this.#collection;
     return this.#collection;
   }
   }
 
 
-  responseToPrimitives(responses) {
+  responseToPrimitives(responses: HTMLCollection) {
     return Array.from(responses).map((response) => ({
     return Array.from(responses).map((response) => ({
-      directory: !!this.#getTag(response, 'collection'),
-      fullPath: this.#getTagContent(response, 'href'),
-      modified: Date.parse(this.#getTagContent(response, 'getlastmodified')),
-      size: parseInt(this.#getTagContent(response, 'getcontentlength'), 10),
-      mimeType: this.#getTagContent(response, 'getcontenttype'),
+      directory: !!this.#getTag(response, 'D:collection'),
+      fullPath: this.#getTagContent(response, 'D:href'),
+      modified: Date.parse(
+        this.#getTagContent(response, 'lp1:getlastmodified')
+      ),
+      size: parseInt(this.#getTagContent(response, 'lp1:getcontentlength'), 10),
+      mimeType: this.#getTagContent(response, 'D:getcontenttype'),
     }));
     }));
   }
   }
 }
 }

+ 5 - 2
src/lib/UI/NativeDOM.ts

@@ -114,11 +114,14 @@ export default class NativeDOM extends UI {
             }`,
             }`,
             ''
             ''
           );
           );
-      if (entry.path === destinationPath) {
+
+      if (entry.path === destinationPath || entry.directory) {
         return new Melba({
         return new Melba({
           content: `'${
           content: `'${
             entry.title
             entry.title
-          }' successfully renamed to '${decodeURIComponent(destinationFile)}'.`,
+          }' successfully renamed to '${decodeURIComponent(
+            destinationFile || destinationPath
+          )}'.`,
           type: 'success',
           type: 'success',
           hide: 5,
           hide: 5,
         });
         });

+ 4 - 6
src/lib/UI/NativeDOM/List.ts

@@ -30,8 +30,7 @@ export default class List extends Element {
     });
     });
 
 
     const arrowHandler = (event) => {
     const arrowHandler = (event) => {
-      if (![38, 40].includes(event.which)) {
-        // if (! ['ArrowUp', 'ArrowDown'].includes(event.key)) {
+      if (!['ArrowUp', 'ArrowDown'].includes(event.key)) {
         return;
         return;
       }
       }
 
 
@@ -45,11 +44,10 @@ export default class List extends Element {
           ? current.nextSibling
           ? current.nextSibling
           : this.element.querySelector('li:first-child'),
           : this.element.querySelector('li:first-child'),
         previous = current ? current.previousSibling : null;
         previous = current ? current.previousSibling : null;
-      if (event.which === 38 && previous) {
-        // if (event.key === 'ArrowUp' && previous) {
+
+      if (event.key === 'ArrowUp' && previous) {
         previous.focus();
         previous.focus();
-      } else if (event.which === 40 && next) {
-        // else if (event.key === 'ArrowDown' && next) {
+      } else if (event.key === 'ArrowDown' && next) {
         next.focus();
         next.focus();
       }
       }
     };
     };

+ 13 - 24
src/lib/UI/NativeDOM/List/Item.ts

@@ -41,7 +41,7 @@ export default class Item extends Element {
   });
   });
 
 
   constructor(entry, base64Encoder = btoa) {
   constructor(entry, base64Encoder = btoa) {
-    super(`<li tabindex="0" data-full-path=${entry.fullPath}">
+    super(`<li tabindex="0" data-full-path="${entry.fullPath}">
   <span class="title">${entry.title}</span>
   <span class="title">${entry.title}</span>
   <input type="text" name="rename" class="hidden" readonly>
   <input type="text" name="rename" class="hidden" readonly>
   <span class="size">${entry.displaySize}</span>
   <span class="size">${entry.displaySize}</span>
@@ -117,22 +117,14 @@ export default class Item extends Element {
     });
     });
 
 
     element.addEventListener('keydown', (event) => {
     element.addEventListener('keydown', (event) => {
-      if ([113, 46, 13].includes(event.which)) {
-        // if (['F2', 'Delete', 'Enter'].includes(event.key)) {
+      if (['F2', 'Delete', 'Enter'].includes(event.key)) {
         event.preventDefault();
         event.preventDefault();
 
 
-        if (event.which === 113) {
-          // if (event.key === 'F2') {
-          if (this.#entry.rename) {
-            this.rename();
-          }
-        } else if (event.which === 46) {
-          // else if (event.key === 'Delete') {
-          if (this.#entry.del) {
-            this.del();
-          }
-        } else if (event.which === 13 && !this.#entry.directory) {
-          // else if (event.key === 'Enter' && ! this.#entry.directory) {
+        if (event.key === 'F2' && this.#entry.rename) {
+          this.rename();
+        } else if (event.key === 'Delete' && this.#entry.del) {
+          this.del();
+        } else if (event.key === 'Enter' && !this.#entry.directory) {
           if (event.shiftKey) {
           if (event.shiftKey) {
             return this.download();
             return this.download();
           }
           }
@@ -182,15 +174,14 @@ export default class Item extends Element {
     this.loading();
     this.loading();
 
 
     if (entry.directory) {
     if (entry.directory) {
-      return this.trigger('go', entry.fullPath, {
-        failure: () => this.loading(false),
-      });
+      return this.trigger('go', entry.fullPath, false, () =>
+        this.loading(false)
+      );
     }
     }
 
 
     const launchLightbox = (lightboxContent, onShow = null) => {
     const launchLightbox = (lightboxContent, onShow = null) => {
       const escapeListener = (event) => {
       const escapeListener = (event) => {
-          if (event.which === 27) {
-            // if (event.key === 'Escape') {
+          if (event.key === 'Escape') {
             lightbox.close();
             lightbox.close();
           }
           }
         },
         },
@@ -289,14 +280,12 @@ export default class Item extends Element {
         save();
         save();
       },
       },
       keyDownListener = (event) => {
       keyDownListener = (event) => {
-        if (event.which === 13) {
-          // if (event.key === 'Enter') {
+        if (event.key === 'Enter') {
           event.stopPropagation();
           event.stopPropagation();
           event.preventDefault();
           event.preventDefault();
 
 
           save();
           save();
-        } else if (event.which === 27) {
-          // else if (event.key === 'Escape') {
+        } else if (event.key === 'Escape') {
           revert();
           revert();
         }
         }
       },
       },

+ 1 - 1
src/lib/UI/UI.ts

@@ -38,7 +38,7 @@ export default class UI extends EventObject {
     };
     };
   }
   }
 
 
-  get dav() {
+  get dav(): DAV {
     return this.#dav;
     return this.#dav;
   }
   }
 
 

+ 3 - 4
src/lib/joinPath.ts

@@ -1,10 +1,9 @@
-export const trimSlashes = (piece) => piece.replace(/^\/+|\/+$/g, '');
+export const trimSlashes = (piece: string) => piece.replace(/^\/+|\/+$/g, '');
 
 
-export const joinPath = (...pieces) => {
-  return `/${pieces
+export const joinPath = (...pieces: string[]) =>
+  `/${pieces
     .map(trimSlashes)
     .map(trimSlashes)
     .filter((piece) => piece)
     .filter((piece) => piece)
     .join('/')}`;
     .join('/')}`;
-};
 
 
 export default joinPath;
 export default joinPath;

文件差异内容过多而无法显示
+ 0 - 0
src/webdav-min.js


+ 252 - 0
tests/functional/List.test.ts

@@ -0,0 +1,252 @@
+import {
+  isLightboxClosed,
+  isPageReady,
+  isLightboxShown,
+  expectToastShown,
+  isElementGone,
+  isElementThere,
+} from '../lib/isReady';
+import { ElementHandle } from 'puppeteer';
+import * as fs from 'fs';
+
+const BASE_URL = process.env.BASE_URL ?? 'http://localhost:8080/',
+  DESTINATION_FONT_FILE = '/tmp/BlackAndWhitePicture-Regular.ttf';
+
+describe('WebDAV.js', () => {
+  describe('List', () => {
+    it('should be possible to preview items', async () => {
+      // Wait for page JS to replace page contents
+      await isPageReady(page, BASE_URL);
+
+      await (
+        [
+          [
+            '[data-full-path="/0.jpg"]',
+            async (lightbox: ElementHandle) => {
+              await expect(await lightbox.$('img')).toBeTruthy();
+            },
+          ],
+          [
+            '[data-full-path="/BlackAndWhitePicture-Regular.ttf"]',
+            async (lightbox: ElementHandle) => {
+              await expect(await lightbox.$('img')).toBeFalsy();
+              await expect(await lightbox.$$('style')).toHaveLength(1);
+              await expect(await lightbox.$$('h1')).toHaveLength(1);
+              await expect(await lightbox.$$('p')).toHaveLength(4);
+            },
+          ],
+          [
+            '[data-full-path="/dummy.pdf"]',
+            async (lightbox: ElementHandle) => {
+              await expect(await lightbox.$('iframe')).toBeTruthy();
+            },
+          ],
+          [
+            '[data-full-path="/style.css"]',
+            async (lightbox: ElementHandle) => {
+              await expect(await lightbox.$('pre.language-css')).toBeTruthy();
+            },
+          ],
+          [
+            '[data-full-path="/video.mp4"]',
+            async (lightbox: ElementHandle) => {
+              await expect(await lightbox.$('video[autoplay]')).toBeTruthy();
+            },
+          ],
+        ] as [string, (lightbox: ElementHandle) => Promise<void>][]
+      ).reduce(
+        async (
+          previous: Promise<void>,
+          [selector, expectation]
+        ): Promise<void> => {
+          await previous;
+
+          await page.click(selector);
+
+          await expectation(await isLightboxShown(page));
+
+          await isLightboxClosed(page);
+
+          await page.waitForTimeout(500);
+        },
+        Promise.resolve(null)
+      );
+    });
+
+    it('should be possible to navigate directories', async () => {
+      await isPageReady(page, BASE_URL);
+
+      await expect(await page.$$('main ul li')).toHaveLength(23);
+
+      await page.click('[data-full-path="/source.js/"]');
+
+      await page.waitForTimeout(200);
+
+      await expect(await page.$$('main ul li')).toHaveLength(1);
+
+      await expect(await page.$('[data-full-path="/"]')).toBeTruthy();
+
+      await page.click('[data-full-path="/"]');
+
+      await page.waitForTimeout(200);
+
+      await expect(await page.$$('main ul li')).toHaveLength(23);
+    });
+
+    it('should be possible to create a new navigable directory, rename it and delete it', async () => {
+      await isPageReady(page, BASE_URL);
+
+      page.once(
+        'dialog',
+        async (dialog) => await dialog.accept('new-directory')
+      );
+
+      await page.click('.create-directory');
+
+      await isElementThere(page, '[data-full-path="/new-directory/"]');
+
+      await expectToastShown(
+        page,
+        `'new-directory' has been created.`,
+        'success'
+      );
+
+      await page.click('[data-full-path="/new-directory/"] .rename');
+
+      await page.waitForFunction(() =>
+        document.activeElement.matches(
+          '[data-full-path="/new-directory/"] input[type="text"]'
+        )
+      );
+
+      await page.keyboard.down('Control');
+      await page.keyboard.press('Backspace');
+      await page.keyboard.up('Control');
+
+      await page.type(
+        '[data-full-path="/new-directory/"] input[type="text"]',
+        'folder'
+      );
+
+      await page.keyboard.press('Enter');
+
+      await expectToastShown(
+        page,
+        `'new-directory' successfully renamed to 'new-folder'.`,
+        'success'
+      );
+
+      await isElementThere(page, '[data-full-path="/new-folder/"]');
+
+      page.once('dialog', async (dialog) => await dialog.accept());
+
+      await page.click('[data-full-path="/new-folder/"] .delete');
+
+      await isElementGone(page, '[data-full-path="/new-folder/"]');
+
+      await expectToastShown(page, `'new-folder' has been deleted.`, 'success');
+    });
+
+    it('should show expected errors', async () => {
+      await isPageReady(page, BASE_URL);
+
+      await page.click('[data-full-path="/inaccessible-dir/"]');
+
+      await expectToastShown(
+        page,
+        'HEAD /inaccessible-dir/ failed: Forbidden (403)',
+        'error'
+      );
+
+      await page.click('[data-full-path="/inaccessible-file"]');
+
+      await expectToastShown(
+        page,
+        'GET /inaccessible-file failed: Forbidden (403)',
+        'error'
+      );
+
+      await page.click('[data-full-path="/inaccessible-image.jpg"]');
+
+      await expectToastShown(
+        page,
+        'HEAD /inaccessible-image.jpg failed: Forbidden (403)',
+        'error'
+      );
+
+      await page.click('[data-full-path="/inaccessible-text-file.txt"]');
+
+      await expectToastShown(
+        page,
+        'GET /inaccessible-text-file.txt failed: Forbidden (403)',
+        'error'
+      );
+    });
+
+    it('should be possible to download a file', async () => {
+      await isPageReady(page, BASE_URL);
+
+      await expect(() => fs.accessSync(DESTINATION_FONT_FILE)).toThrow();
+
+      await page
+        .target()
+        .createCDPSession()
+        .then((client) =>
+          client.send('Page.setDownloadBehavior', {
+            behavior: 'allow',
+            downloadPath: '/tmp',
+          })
+        );
+
+      await page.click(
+        '[data-full-path="/BlackAndWhitePicture-Regular.ttf"] [download]'
+      );
+
+      // wait for the file to download
+      await page.waitForTimeout(400);
+
+      await expect(() => fs.accessSync(DESTINATION_FONT_FILE)).not.toThrow();
+
+      fs.rmSync(DESTINATION_FONT_FILE);
+    });
+
+    it('should be possible to upload a file', async () => {
+      await isPageReady(page, BASE_URL);
+
+      const elementHandle = (await page.$(
+        'input[type=file]'
+      )) as ElementHandle<HTMLInputElement>;
+
+      await elementHandle.uploadFile('./package.json');
+
+      await expectToastShown(
+        page,
+        `'package.json' has been successfully uploaded.`,
+        'success'
+      );
+
+      await page.click('[data-full-path="/package.json"]');
+
+      await page.once('dialog', async (dialog) => await dialog.accept());
+
+      await page.click('[data-full-path="/package.json"] .delete');
+
+      await isElementGone(page, '[data-full-path="/package.json"]');
+
+      await expectToastShown(
+        page,
+        `'package.json' has been deleted.`,
+        'success'
+      );
+    });
+  });
+
+  beforeAll(async () => {
+    try {
+      fs.accessSync(DESTINATION_FONT_FILE);
+      fs.rmSync(DESTINATION_FONT_FILE);
+    } catch (e) {
+      // we don't need to do anything here
+    }
+  });
+});

+ 1 - 0
tests/jest.ts-puppeteer.preset.chrome.ts

@@ -0,0 +1 @@
+module.exports = require('./jest.ts-puppeteer.preset')('chrome');

+ 1 - 0
tests/jest.ts-puppeteer.preset.firefox.ts

@@ -0,0 +1 @@
+module.exports = require('./jest.ts-puppeteer.preset')('firefox');

+ 15 - 0
tests/jest.ts-puppeteer.preset.ts

@@ -0,0 +1,15 @@
+const { jsWithTs } = require('ts-jest/presets');
+const jestPuppeteerPreset = require('jest-puppeteer/jest-preset.js');
+
+const combined = {
+  ...jsWithTs,
+  ...jestPuppeteerPreset,
+};
+
+module.exports = (product) => ({
+  ...combined,
+  launch: {
+    ...(combined.launch ?? {}),
+    product,
+  },
+});

+ 38 - 0
tests/lib/getProperties.ts

@@ -0,0 +1,38 @@
+import { ElementHandle } from 'puppeteer';
+
+export const getRawProperty = async <
+  T extends HTMLElement = HTMLElement,
+  K extends keyof T = keyof T
+>(
+  element: Promise<ElementHandle<T>> | ElementHandle<T>,
+  property: K
+): Promise<T[K]> =>
+  await (await (await element)?.getProperty(property as string))?.jsonValue();
+
+export const getRawProperties = async <
+  T extends HTMLElement = HTMLElement,
+  K extends keyof T = keyof T
+>(
+  element: Promise<ElementHandle<T>> | ElementHandle<T>,
+  ...properties: K[]
+) =>
+  Promise.all(properties.map((property) => getRawProperty(element, property)));
+
+export const getRawPropertyFromMany = async <
+  T extends HTMLElement = HTMLElement,
+  K extends keyof T = keyof T
+>(
+  elements: Promise<ElementHandle<T>[]> | ElementHandle<T>[],
+  property: K
+): Promise<T[K][]> =>
+  Promise.all(
+    (await elements).map(
+      async (element): Promise<any> =>
+        (['innerText', 'innerHTML'] as K[]).includes(property)
+          ? await element?.evaluate<[string]>(
+              (element: T, property: string) => element[property],
+              property as string
+            )
+          : getRawProperty<T, K>(element, property)
+    )
+  );

+ 57 - 0
tests/lib/grantClipboardPermissions.ts

@@ -0,0 +1,57 @@
+import { Page } from 'puppeteer';
+
+export const grantRawPermissions = async (
+  page: Page,
+  permissions: string[]
+) => {
+  const context = await page.browserContext(),
+    url = new URL(page.url());
+
+  // @ts-ignore
+  await context._connection.send('Browser.grantPermissions', {
+    origin: url.origin,
+    // @ts-ignore
+    browserContextId: context._id,
+    permissions: permissions,
+  });
+
+  await page.reload();
+};
+
+// https://github.com/puppeteer/puppeteer/issues/3241#issuecomment-751489962:
+// > When I try to grant clipboard-write permissions in headless mode it changes it to 'denied' instead (if I don't call it is it 'prompt'):
+// >
+// >   context.overridePermissions(url.origin, ['clipboard-write'])
+// >
+// >   const page = await context.newPage()
+// >   await page.goto(url.origin)
+// >   const state = await page.evaluate(async () => {
+// >     return (await navigator.permissions.query({name: 'clipboard-write'})).state;
+// >   });
+// >   console.log(state) // denied
+// >
+// > macOS: 10.15.7
+// > puppeteer: 5.4.1
+//
+// I'm not an expert in this regard, but at least when limiting the scope of this discussion to clipboard-write and clipboard-read, it seems to me that there is a bug/mistake in the overridePermissions() method. As per its documentation:
+//
+// > An array of permissions to grant. All permissions that are not listed here will be automatically denied.
+//
+// So, if you specify either clipboard-write and clipboard-read as permissions then both will be mapped to clipboardReadWrite which is the only permission that will be granted. By looking at the Chrome DevTools Protocol (which overridePermissions() uses to override permissions) I found that there are actually two permissions related to the clipboard: clipboardSanitizedWrite and clipboardReadWrite. When I mimicked overridePermissions()'s CDP call manually using 'clipboardSanitizedWrite' as permissions I was able to use navigator.clipboard.writeText and your snipped returned "granted" (instead of "denied"). More specifically, the following snippet should work:
+//
+//   await context._connection.send('Browser.grantPermissions', {
+//     origin: url.origin,
+//     browserContextId: this._id || undefined,
+//     permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
+//   });
+//
+//   const page = await context.newPage()
+//   await page.goto(url.origin)
+//   const state = await page.evaluate(async () => {
+//     return (await navigator.permissions.query({name: 'clipboard-write'})).state;
+//   });
+//   console.log(state) // granted
+//
+// It's not pretty and should probably be fixed in overridePermissions(), but it gets the job done.
+export const grantClipboardPermissions = (page: Page) =>
+  grantRawPermissions(page, ['clipboardReadWrite', 'clipboardSanitizedWrite']);

+ 64 - 0
tests/lib/isReady.ts

@@ -0,0 +1,64 @@
+import { Page } from 'puppeteer';
+
+export const isPageReady = async (page: Page, url: string) => {
+  await page.goto(url);
+
+  await isElementThere(page, 'main ul li');
+};
+
+export const isElementThere = async (page: Page, selector: string) =>
+  await page.waitForFunction(
+    (selector) => !!document.querySelector(selector),
+    {},
+    selector
+  );
+
+export const isElementGone = async (page: Page, selector: string) =>
+  await page.waitForFunction(
+    (selector) => !document.querySelector(selector),
+    {},
+    selector
+  );
+
+export const isLightboxShown = async (page: Page) => {
+  await isElementThere(page, '.basicLightbox--visible');
+
+  return page.$('.basicLightbox--visible');
+};
+
+export const isLightboxClosed = async (page: Page) => {
+  const lightbox = await isLightboxShown(page);
+
+  // Click on the overlay to close the lightbox
+  await lightbox.click({
+    offset: {
+      x: 10,
+      y: 10,
+    },
+  });
+
+  await isElementGone(page, '.basicLightbox--visible');
+};
+
+export const expectToastShown = async (
+  page: Page,
+  text: string,
+  type: string
+) => {
+  await page.waitForTimeout(100);
+
+  const toast = await page.$('.toast__container .toast');
+
+  await expect(
+    await toast.evaluate((toast) => toast.childNodes[1].textContent)
+  ).toEqual(text);
+
+  await expect(
+    await toast.evaluate(
+      (toast, type: string) => toast.classList.contains(`toast--${type}`),
+      type
+    )
+  ).toBeTruthy();
+
+  await toast.evaluate((toast) => toast.remove());
+};

+ 67 - 39
tests/unit/DAV.test.ts

@@ -1,42 +1,60 @@
 import Collection from '../../src/lib/DAV/Collection';
 import Collection from '../../src/lib/DAV/Collection';
 import DAV from '../../src/lib/DAV';
 import DAV from '../../src/lib/DAV';
+import { DOMParser } from '@xmldom/xmldom';
 import HTTP from '../../src/lib/HTTP';
 import HTTP from '../../src/lib/HTTP';
 
 
-// @ts-ignore
-const MockHttp = jest.createMockFromModule('../../src/lib/HTTP').default;
-
 describe('DAV', () => {
 describe('DAV', () => {
-  // @ts-ignore
-  console.log((MockHttp as HTTP).GET('', {}));
-
-  const getSpies = (SpyHTTPReturns = {}, SpyCacheReturns = {}) => {
-    const SpyHTTP = jasmine.createSpyObj('HTTP', [
-        'GET',
-        'HEAD',
-        'PUT',
-        'PROPFIND',
-        'DELETE',
-        'MKCOL',
-        'COPY',
-        'MOVE',
-      ]),
-      SpyCache = jasmine.createSpyObj('Cache', ['delete', 'get', 'has', 'set']);
+  const getSpies = (
+    SpyHTTPReturns = {},
+    SpyCacheReturns = {}
+  ): [HTTP, Map<string, Collection>] => {
+    const SpyHTTP = new HTTP(),
+      SpyCache = new Map();
+
     [
     [
-      [SpyHTTP, SpyHTTPReturns],
-      [SpyCache, SpyCacheReturns],
-    ].forEach(([object, returnValues]) =>
-      Object.entries(returnValues).forEach(
-        ([method, returnValue]) =>
-          (object[method] = object[method].and.returnValue(returnValue))
-      )
+      'GET',
+      'HEAD',
+      'PUT',
+      'PROPFIND',
+      'DELETE',
+      'MKCOL',
+      'COPY',
+      'MOVE',
+    ].forEach(
+      (methodName) =>
+        (SpyHTTP[methodName] = jest.fn(
+          () =>
+            new Promise((resolve) =>
+              resolve(SpyHTTPReturns[methodName] ?? null)
+            )
+        ))
+    );
+
+    ['delete', 'get', 'has', 'set'].forEach(
+      (methodName) =>
+        (SpyCache[methodName] = jest.fn(
+          () => SpyCacheReturns[methodName] ?? null
+        ))
     );
     );
 
 
     return [SpyHTTP, SpyCache];
     return [SpyHTTP, SpyCache];
   };
   };
 
 
+  if (typeof window === 'undefined') {
+    global.location = {
+      ...global.location,
+      protocol: 'http:',
+      host: 'localhost',
+      hostname: 'localhost',
+    };
+
+    global.DOMParser = DOMParser;
+  }
+
   it('should fire a HEAD request on check', () => {
   it('should fire a HEAD request on check', () => {
     const [SpyHTTP, SpyCache] = getSpies(),
     const [SpyHTTP, SpyCache] = getSpies(),
       dav = new DAV({}, SpyCache, SpyHTTP);
       dav = new DAV({}, SpyCache, SpyHTTP);
+
     dav.check('/checkHeadRequest');
     dav.check('/checkHeadRequest');
     expect(SpyHTTP.HEAD).toHaveBeenCalledWith('/checkHeadRequest');
     expect(SpyHTTP.HEAD).toHaveBeenCalledWith('/checkHeadRequest');
   });
   });
@@ -44,6 +62,7 @@ describe('DAV', () => {
   it('should fire a COPY request on copy', () => {
   it('should fire a COPY request on copy', () => {
     const [SpyHTTP, SpyCache] = getSpies(),
     const [SpyHTTP, SpyCache] = getSpies(),
       dav = new DAV({}, SpyCache, SpyHTTP);
       dav = new DAV({}, SpyCache, SpyHTTP);
+
     dav.copy('/copySource', '/copyDestination');
     dav.copy('/copySource', '/copyDestination');
     expect(SpyHTTP.COPY).toHaveBeenCalledWith('/copySource', {
     expect(SpyHTTP.COPY).toHaveBeenCalledWith('/copySource', {
       headers: {
       headers: {
@@ -57,6 +76,7 @@ describe('DAV', () => {
   it('should fire a DELETE request on del', () => {
   it('should fire a DELETE request on del', () => {
     const [SpyHTTP, SpyCache] = getSpies(),
     const [SpyHTTP, SpyCache] = getSpies(),
       dav = new DAV({}, SpyCache, SpyHTTP);
       dav = new DAV({}, SpyCache, SpyHTTP);
+
     dav.del('/checkDeleteRequest');
     dav.del('/checkDeleteRequest');
     expect(SpyHTTP.DELETE).toHaveBeenCalledWith('/checkDeleteRequest');
     expect(SpyHTTP.DELETE).toHaveBeenCalledWith('/checkDeleteRequest');
   });
   });
@@ -64,6 +84,7 @@ describe('DAV', () => {
   it('should fire a GET request on get', () => {
   it('should fire a GET request on get', () => {
     const [SpyHTTP, SpyCache] = getSpies(),
     const [SpyHTTP, SpyCache] = getSpies(),
       dav = new DAV({}, SpyCache, SpyHTTP);
       dav = new DAV({}, SpyCache, SpyHTTP);
+
     dav.get('/checkGetRequest');
     dav.get('/checkGetRequest');
     expect(SpyHTTP.GET).toHaveBeenCalledWith('/checkGetRequest');
     expect(SpyHTTP.GET).toHaveBeenCalledWith('/checkGetRequest');
   });
   });
@@ -85,6 +106,7 @@ describe('DAV', () => {
       ),
       ),
       dav = new DAV({}, SpyCache, SpyHTTP),
       dav = new DAV({}, SpyCache, SpyHTTP),
       collection = await dav.list('/checkPropfindRequest');
       collection = await dav.list('/checkPropfindRequest');
+
     expect(SpyCache.get).toHaveBeenCalledWith('/checkPropfindRequest/');
     expect(SpyCache.get).toHaveBeenCalledWith('/checkPropfindRequest/');
     expect(SpyHTTP.HEAD).toHaveBeenCalledWith('/checkPropfindRequest/');
     expect(SpyHTTP.HEAD).toHaveBeenCalledWith('/checkPropfindRequest/');
     expect(SpyHTTP.PROPFIND).toHaveBeenCalledWith('/checkPropfindRequest/');
     expect(SpyHTTP.PROPFIND).toHaveBeenCalledWith('/checkPropfindRequest/');
@@ -98,6 +120,8 @@ describe('DAV', () => {
   it('should fire an MKCOL request on mkcol', () => {
   it('should fire an MKCOL request on mkcol', () => {
     const [SpyHTTP, SpyCache] = getSpies(),
     const [SpyHTTP, SpyCache] = getSpies(),
       dav = new DAV({}, SpyCache, SpyHTTP);
       dav = new DAV({}, SpyCache, SpyHTTP);
+    0;
+
     dav.mkcol('/checkMkcolRequest');
     dav.mkcol('/checkMkcolRequest');
     expect(SpyHTTP.MKCOL).toHaveBeenCalledWith('/checkMkcolRequest');
     expect(SpyHTTP.MKCOL).toHaveBeenCalledWith('/checkMkcolRequest');
   });
   });
@@ -105,6 +129,7 @@ describe('DAV', () => {
   it('should fire a MOVE request on move', () => {
   it('should fire a MOVE request on move', () => {
     const [SpyHTTP, SpyCache] = getSpies(),
     const [SpyHTTP, SpyCache] = getSpies(),
       dav = new DAV({}, SpyCache, SpyHTTP);
       dav = new DAV({}, SpyCache, SpyHTTP);
+
     dav.move('/moveSource', '/moveDestination');
     dav.move('/moveSource', '/moveDestination');
 
 
     expect(SpyHTTP.MOVE).toHaveBeenCalledWith('/moveSource', {
     expect(SpyHTTP.MOVE).toHaveBeenCalledWith('/moveSource', {
@@ -116,20 +141,22 @@ describe('DAV', () => {
     });
     });
   });
   });
 
 
-  it('should fire a PUT request on upload', () => {
-    const [SpyHTTP, SpyCache] = getSpies(),
-      dav = new DAV({}, SpyCache, SpyHTTP),
-      file = new File([''], 'uploadTest', {
-        type: 'text/plain',
-      });
-    dav.upload('/path/', file);
-    expect(SpyHTTP.PUT).toHaveBeenCalledWith('/path/uploadTest', {
-      headers: {
-        'Content-Type': file.type,
-      },
-      body: file,
-    });
-  });
+  test.todo('should fire a PUT request on upload - functional only');
+  // it('should fire a PUT request on upload', () => {
+  //   const [SpyHTTP, SpyCache] = getSpies(),
+  //     dav = new DAV({}, SpyCache, SpyHTTP),
+  //     file = new File([''], 'uploadTest', {
+  //       type: 'text/plain',
+  //     });
+  //
+  //   dav.upload('/path/', file);
+  //   expect(SpyHTTP.PUT).toHaveBeenCalledWith('/path/uploadTest', {
+  //     headers: {
+  //       'Content-Type': file.type,
+  //     },
+  //     body: file,
+  //   });
+  // });
 
 
   it('should not fire a HEAD request on list when `bypassCheck` is set', async () => {
   it('should not fire a HEAD request on list when `bypassCheck` is set', async () => {
     const [SpyHTTP, SpyCache] = getSpies(
     const [SpyHTTP, SpyCache] = getSpies(
@@ -153,6 +180,7 @@ describe('DAV', () => {
         SpyCache,
         SpyCache,
         SpyHTTP
         SpyHTTP
       );
       );
+
     await dav.list('/checkPropfindRequest');
     await dav.list('/checkPropfindRequest');
 
 
     expect(SpyHTTP.HEAD).not.toHaveBeenCalledWith('/checkPropfindRequest/');
     expect(SpyHTTP.HEAD).not.toHaveBeenCalledWith('/checkPropfindRequest/');

+ 4 - 2
tests/unit/DAV/Entry.test.ts

@@ -7,14 +7,12 @@ describe('Entry', () => {
       modified: new Date(),
       modified: new Date(),
     }),
     }),
     file = new Entry({
     file = new Entry({
-      directory: false,
       fullPath: '/path/to/file.txt',
       fullPath: '/path/to/file.txt',
       modified: new Date(),
       modified: new Date(),
       size: 54321,
       size: 54321,
       mimeType: 'text/plain',
       mimeType: 'text/plain',
     }),
     }),
     atFile = new Entry({
     atFile = new Entry({
-      directory: false,
       fullPath: '/%40',
       fullPath: '/%40',
       modified: new Date(),
       modified: new Date(),
       size: 54321,
       size: 54321,
@@ -26,6 +24,10 @@ describe('Entry', () => {
     expect(directory.name).toBe('to');
     expect(directory.name).toBe('to');
   });
   });
 
 
+  it('should return an empty size for directories', () => {
+    expect(directory.directory).toBe(true);
+  });
+
   it('should return an empty size for directories', () => {
   it('should return an empty size for directories', () => {
     expect(directory.displaySize).toBe('');
     expect(directory.displaySize).toBe('');
   });
   });

文件差异内容过多而无法显示
+ 0 - 0
tests/unit/DAV/Response.test.ts


部分文件因为文件数量过多而无法显示