selenium-webdriver 4.1.1 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.md +492 -553
- package/README.md +4 -4
- package/chromium.js +16 -3
- package/edge.js +2 -2
- package/lib/by.js +1 -1
- package/lib/http.js +8 -7
- package/lib/input.js +70 -4
- package/lib/webdriver.js +39 -39
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ The sample below and others are included in the `example` directory. You may
|
|
|
33
33
|
also find the tests for selenium-webdriver informative.
|
|
34
34
|
|
|
35
35
|
```javascript
|
|
36
|
-
const {Builder, By, Key, until} = require('selenium-webdriver');
|
|
36
|
+
const {Builder, Browser, By, Key, until} = require('selenium-webdriver');
|
|
37
37
|
|
|
38
38
|
(async function example() {
|
|
39
|
-
let driver = await new Builder().forBrowser(
|
|
39
|
+
let driver = await new Builder().forBrowser(Browser.FIREFOX).build();
|
|
40
40
|
try {
|
|
41
41
|
await driver.get('http://www.google.com/ncr');
|
|
42
42
|
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN);
|
|
@@ -60,7 +60,7 @@ const chrome = require('selenium-webdriver/chrome');
|
|
|
60
60
|
const firefox = require('selenium-webdriver/firefox');
|
|
61
61
|
|
|
62
62
|
let driver = new webdriver.Builder()
|
|
63
|
-
.forBrowser(
|
|
63
|
+
.forBrowser(webdriver.Browser.FIREFOX)
|
|
64
64
|
.setChromeOptions(/* ... */)
|
|
65
65
|
.setFirefoxOptions(/* ... */)
|
|
66
66
|
.build();
|
|
@@ -99,7 +99,7 @@ API:
|
|
|
99
99
|
|
|
100
100
|
```javascript
|
|
101
101
|
let driver = new webdriver.Builder()
|
|
102
|
-
.forBrowser(
|
|
102
|
+
.forBrowser(webdriver.Browser.FIREFOX)
|
|
103
103
|
.usingServer('http://localhost:4444/wd/hub')
|
|
104
104
|
.build();
|
|
105
105
|
```
|
package/chromium.js
CHANGED
|
@@ -92,6 +92,7 @@ const Command = {
|
|
|
92
92
|
LAUNCH_APP: 'launchApp',
|
|
93
93
|
GET_NETWORK_CONDITIONS: 'getNetworkConditions',
|
|
94
94
|
SET_NETWORK_CONDITIONS: 'setNetworkConditions',
|
|
95
|
+
DELETE_NETWORK_CONDITIONS: 'deleteNetworkConditions',
|
|
95
96
|
SEND_DEVTOOLS_COMMAND: 'sendDevToolsCommand',
|
|
96
97
|
SEND_AND_GET_DEVTOOLS_COMMAND: 'sendAndGetDevToolsCommand',
|
|
97
98
|
SET_PERMISSION: 'setPermission',
|
|
@@ -137,6 +138,11 @@ function configureExecutor(executor, vendorPrefix) {
|
|
|
137
138
|
'POST',
|
|
138
139
|
'/session/:sessionId/chromium/network_conditions'
|
|
139
140
|
)
|
|
141
|
+
executor.defineCommand(
|
|
142
|
+
Command.DELETE_NETWORK_CONDITIONS,
|
|
143
|
+
'DELETE',
|
|
144
|
+
'/session/:sessionId/chromium/network_conditions'
|
|
145
|
+
)
|
|
140
146
|
executor.defineCommand(
|
|
141
147
|
Command.SEND_DEVTOOLS_COMMAND,
|
|
142
148
|
'POST',
|
|
@@ -655,14 +661,12 @@ class Driver extends webdriver.WebDriver {
|
|
|
655
661
|
*/
|
|
656
662
|
static createSession(caps, opt_serviceExecutor) {
|
|
657
663
|
let executor
|
|
658
|
-
let onQuit
|
|
659
664
|
if (opt_serviceExecutor instanceof http.Executor) {
|
|
660
665
|
executor = opt_serviceExecutor
|
|
661
666
|
configureExecutor(executor, this.VENDOR_COMMAND_PREFIX)
|
|
662
667
|
} else {
|
|
663
668
|
let service = opt_serviceExecutor || this.getDefaultService()
|
|
664
669
|
executor = createExecutor(service.start(), this.VENDOR_COMMAND_PREFIX)
|
|
665
|
-
onQuit = () => service.kill()
|
|
666
670
|
}
|
|
667
671
|
|
|
668
672
|
// W3C spec requires noProxy value to be an array of strings, but Chromium
|
|
@@ -675,7 +679,7 @@ class Driver extends webdriver.WebDriver {
|
|
|
675
679
|
}
|
|
676
680
|
}
|
|
677
681
|
|
|
678
|
-
return /** @type {!Driver} */ (super.createSession(executor, caps
|
|
682
|
+
return /** @type {!Driver} */ (super.createSession(executor, caps))
|
|
679
683
|
}
|
|
680
684
|
|
|
681
685
|
/**
|
|
@@ -706,6 +710,15 @@ class Driver extends webdriver.WebDriver {
|
|
|
706
710
|
return this.execute(new command.Command(Command.GET_NETWORK_CONDITIONS))
|
|
707
711
|
}
|
|
708
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Schedules a command to delete Chromium network emulation settings.
|
|
715
|
+
* @return {!Promise} A promise that will be resolved when network
|
|
716
|
+
* emulation settings have been deleted.
|
|
717
|
+
*/
|
|
718
|
+
deleteNetworkConditions() {
|
|
719
|
+
return this.execute(new command.Command(Command.DELETE_NETWORK_CONDITIONS))
|
|
720
|
+
}
|
|
721
|
+
|
|
709
722
|
/**
|
|
710
723
|
* Schedules a command to set Chromium network emulation settings.
|
|
711
724
|
*
|
package/edge.js
CHANGED
|
@@ -94,7 +94,7 @@ let defaultService = null
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Creates {@link selenium-webdriver/remote.DriverService} instances that manage
|
|
97
|
-
* a [
|
|
97
|
+
* a [MSEdgeDriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/)
|
|
98
98
|
* server in a child process.
|
|
99
99
|
*/
|
|
100
100
|
class ServiceBuilder extends chromium.ServiceBuilder {
|
|
@@ -130,7 +130,7 @@ class Options extends chromium.Options {
|
|
|
130
130
|
* The binary path be absolute or relative to the msedgedriver server
|
|
131
131
|
* executable, but it must exist on the machine that will launch edge chromium.
|
|
132
132
|
*
|
|
133
|
-
* @param {string} path The path to the
|
|
133
|
+
* @param {string} path The path to the msedgedriver binary to use.
|
|
134
134
|
* @return {!Options} A self reference.
|
|
135
135
|
*/
|
|
136
136
|
setEdgeChromiumBinaryPath(path) {
|
package/lib/by.js
CHANGED
package/lib/http.js
CHANGED
|
@@ -63,8 +63,8 @@ function requireAtom(module, bazelTarget) {
|
|
|
63
63
|
console.log(ex2)
|
|
64
64
|
throw Error(
|
|
65
65
|
`Failed to import atoms module ${module}. If running in dev mode, you` +
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
` need to run \`bazel build ${bazelTarget}\` from the project` +
|
|
67
|
+
`root: ${ex}`
|
|
68
68
|
)
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -197,7 +197,6 @@ function toExecuteAtomCommand(command, atom, ...params) {
|
|
|
197
197
|
|
|
198
198
|
/** @const {!Map<string, (CommandSpec|CommandTransformer)>} */
|
|
199
199
|
const W3C_COMMAND_MAP = new Map([
|
|
200
|
-
|
|
201
200
|
// Session management.
|
|
202
201
|
[cmd.Name.NEW_SESSION, post('/session')],
|
|
203
202
|
[cmd.Name.QUIT, del('/session/:sessionId')],
|
|
@@ -245,7 +244,6 @@ const W3C_COMMAND_MAP = new Map([
|
|
|
245
244
|
[cmd.Name.CLEAR_ACTIONS, del('/session/:sessionId/actions')],
|
|
246
245
|
[cmd.Name.PRINT_PAGE, post('/session/:sessionId/print')],
|
|
247
246
|
|
|
248
|
-
|
|
249
247
|
// Locating elements.
|
|
250
248
|
[cmd.Name.GET_ACTIVE_ELEMENT, get('/session/:sessionId/element/active')],
|
|
251
249
|
[cmd.Name.FIND_ELEMENT, post('/session/:sessionId/element')],
|
|
@@ -266,7 +264,10 @@ const W3C_COMMAND_MAP = new Map([
|
|
|
266
264
|
],
|
|
267
265
|
// Element interaction.
|
|
268
266
|
[cmd.Name.GET_ELEMENT_TAG_NAME, get('/session/:sessionId/element/:id/name')],
|
|
269
|
-
[
|
|
267
|
+
[
|
|
268
|
+
cmd.Name.GET_DOM_ATTRIBUTE,
|
|
269
|
+
get('/session/:sessionId/element/:id/attribute/:name'),
|
|
270
|
+
],
|
|
270
271
|
[
|
|
271
272
|
cmd.Name.GET_ELEMENT_ATTRIBUTE,
|
|
272
273
|
(cmd) => {
|
|
@@ -363,7 +364,7 @@ class Client {
|
|
|
363
364
|
* @return {!Promise<Response>} A promise that will be fulfilled with the
|
|
364
365
|
* server's response.
|
|
365
366
|
*/
|
|
366
|
-
send(httpRequest) {
|
|
367
|
+
send(httpRequest) {} // eslint-disable-line
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
/**
|
|
@@ -609,5 +610,5 @@ module.exports = {
|
|
|
609
610
|
Request: Request,
|
|
610
611
|
Response: Response,
|
|
611
612
|
// Exported for testing.
|
|
612
|
-
buildPath: buildPath
|
|
613
|
+
buildPath: buildPath,
|
|
613
614
|
}
|
package/lib/input.js
CHANGED
|
@@ -200,6 +200,7 @@ Action.Type = {
|
|
|
200
200
|
POINTER_UP: 'pointerUp',
|
|
201
201
|
POINTER_MOVE: 'pointerMove',
|
|
202
202
|
POINTER_CANCEL: 'pointerCancel',
|
|
203
|
+
SCROLL: 'scroll',
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
/**
|
|
@@ -233,6 +234,7 @@ Device.Type = {
|
|
|
233
234
|
KEY: 'key',
|
|
234
235
|
NONE: 'none',
|
|
235
236
|
POINTER: 'pointer',
|
|
237
|
+
WHEEL: 'wheel',
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
/**
|
|
@@ -347,8 +349,14 @@ class Pointer extends Device {
|
|
|
347
349
|
* @return {!Action} An action to press the specified button with this device.
|
|
348
350
|
* @package
|
|
349
351
|
*/
|
|
350
|
-
press(button = Button.LEFT
|
|
351
|
-
|
|
352
|
+
press(button = Button.LEFT, width = 0, height = 0, pressure = 0,
|
|
353
|
+
tangentialPressure = 0, tiltX = 0, tiltY = 0, twist = 0,
|
|
354
|
+
altitudeAngle = 0, azimuthAngle = 0) {
|
|
355
|
+
return {
|
|
356
|
+
type: Action.Type.POINTER_DOWN, button, width, height,
|
|
357
|
+
pressure, tangentialPressure, tiltX, tiltY, twist,
|
|
358
|
+
altitudeAngle, azimuthAngle
|
|
359
|
+
}
|
|
352
360
|
}
|
|
353
361
|
|
|
354
362
|
/**
|
|
@@ -377,8 +385,16 @@ class Pointer extends Device {
|
|
|
377
385
|
* @return {!Action} The new action.
|
|
378
386
|
* @package
|
|
379
387
|
*/
|
|
380
|
-
move({ x = 0, y = 0, duration = 100, origin = Origin.VIEWPORT
|
|
381
|
-
|
|
388
|
+
move({ x = 0, y = 0, duration = 100, origin = Origin.VIEWPORT,
|
|
389
|
+
width = 0, height = 0, pressure = 0,
|
|
390
|
+
tangentialPressure = 0, tiltX = 0, tiltY = 0, twist = 0,
|
|
391
|
+
altitudeAngle = 0, azimuthAngle = 0
|
|
392
|
+
}) {
|
|
393
|
+
return {
|
|
394
|
+
type: Action.Type.POINTER_MOVE, origin, duration, x, y,
|
|
395
|
+
width, height, pressure, tangentialPressure, tiltX, tiltY, twist,
|
|
396
|
+
altitudeAngle, azimuthAngle
|
|
397
|
+
}
|
|
382
398
|
}
|
|
383
399
|
}
|
|
384
400
|
|
|
@@ -392,6 +408,34 @@ Pointer.Type = {
|
|
|
392
408
|
TOUCH: 'touch',
|
|
393
409
|
}
|
|
394
410
|
|
|
411
|
+
class Wheel extends Device {
|
|
412
|
+
/**
|
|
413
|
+
* @param {string} id the device ID.
|
|
414
|
+
* @param {Pointer.Type} type the pointer type.
|
|
415
|
+
*/
|
|
416
|
+
constructor(id, type) {
|
|
417
|
+
super(Device.Type.WHEEL, id)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Scrolls a page via the coordinates given
|
|
422
|
+
* @param {number} x starting x coordinate
|
|
423
|
+
* @param {number} y starting y coordinate
|
|
424
|
+
* @param {number} deltaX Delta X to scroll to target
|
|
425
|
+
* @param {number} deltaY Delta Y to scroll to target
|
|
426
|
+
* @param {number} duration duration ratio be the ratio of time delta and duration
|
|
427
|
+
* @returns {!Action} An action to scroll with this device.
|
|
428
|
+
*/
|
|
429
|
+
scroll(x, y, deltaX, deltaY, origin, duration) {
|
|
430
|
+
return {
|
|
431
|
+
type: Action.Type.SCROLL,
|
|
432
|
+
duration: duration, x: x, y: y,
|
|
433
|
+
deltaX: deltaX, deltaY: deltaY,
|
|
434
|
+
origin: origin
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
395
439
|
/**
|
|
396
440
|
* User facing API for generating complex user gestures. This class should not
|
|
397
441
|
* be instantiated directly. Instead, users should create new instances by
|
|
@@ -509,10 +553,14 @@ class Actions {
|
|
|
509
553
|
/** @private @const */
|
|
510
554
|
this.mouse_ = new Pointer('default mouse', Pointer.Type.MOUSE)
|
|
511
555
|
|
|
556
|
+
/** @private @const */
|
|
557
|
+
this.wheel_ = new Wheel('default wheel')
|
|
558
|
+
|
|
512
559
|
/** @private @const {!Map<!Device, !Array<!Action>>} */
|
|
513
560
|
this.sequences_ = new Map([
|
|
514
561
|
[this.keyboard_, []],
|
|
515
562
|
[this.mouse_, []],
|
|
563
|
+
[this.wheel_, []],
|
|
516
564
|
])
|
|
517
565
|
}
|
|
518
566
|
|
|
@@ -526,6 +574,11 @@ class Actions {
|
|
|
526
574
|
return this.mouse_
|
|
527
575
|
}
|
|
528
576
|
|
|
577
|
+
/** @return {!Wheel} the wheel device handle. */
|
|
578
|
+
wheel() {
|
|
579
|
+
return this.wheel_
|
|
580
|
+
}
|
|
581
|
+
|
|
529
582
|
/**
|
|
530
583
|
* @param {!Device} device
|
|
531
584
|
* @return {!Array<!Action>}
|
|
@@ -735,6 +788,19 @@ class Actions {
|
|
|
735
788
|
return this.insert(this.mouse_, this.mouse_.release(button))
|
|
736
789
|
}
|
|
737
790
|
|
|
791
|
+
/**
|
|
792
|
+
* scrolls a page via the coordinates given
|
|
793
|
+
* @param {number} x starting x coordinate
|
|
794
|
+
* @param {number} y starting y coordinate
|
|
795
|
+
* @param {number} deltax delta x to scroll to target
|
|
796
|
+
* @param {number} deltay delta y to scroll to target
|
|
797
|
+
* @param {number} duration duration ratio be the ratio of time delta and duration
|
|
798
|
+
* @returns {!Action} An action to scroll with this device.
|
|
799
|
+
*/
|
|
800
|
+
scroll(x, y, targetDeltaX, targetDeltaY, origin, duration) {
|
|
801
|
+
return this.insert(this.wheel_, this.wheel_.scroll(x, y, targetDeltaX, targetDeltaY, origin, duration))
|
|
802
|
+
}
|
|
803
|
+
|
|
738
804
|
/**
|
|
739
805
|
* Inserts an action for moving the mouse `x` and `y` pixels relative to the
|
|
740
806
|
* specified `origin`. The `origin` may be defined as the mouse's
|
package/lib/webdriver.js
CHANGED
|
@@ -1197,14 +1197,25 @@ class WebDriver {
|
|
|
1197
1197
|
* @return {!Promise<resolved>} A new CDP instance.
|
|
1198
1198
|
*/
|
|
1199
1199
|
async createCDPConnection(target) {
|
|
1200
|
+
let debuggerUrl = null
|
|
1201
|
+
|
|
1200
1202
|
const caps = await this.getCapabilities()
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1203
|
+
|
|
1204
|
+
if (process.env.SELENIUM_REMOTE_URL) {
|
|
1205
|
+
const host = new URL(process.env.SELENIUM_REMOTE_URL).host
|
|
1206
|
+
const sessionId = await this.getSession().then((session) =>
|
|
1207
|
+
session.getId()
|
|
1208
|
+
)
|
|
1209
|
+
debuggerUrl = `ws://${host}/session/${sessionId}/se/cdp`
|
|
1210
|
+
} else {
|
|
1211
|
+
const seCdp = caps['map_'].get('se:cdp')
|
|
1212
|
+
const vendorInfo =
|
|
1213
|
+
caps['map_'].get(this.VENDOR_COMMAND_PREFIX + ':chromeOptions') ||
|
|
1214
|
+
caps['map_'].get(this.VENDOR_CAPABILITY_PREFIX + ':edgeOptions') ||
|
|
1215
|
+
caps['map_'].get('moz:debuggerAddress') ||
|
|
1216
|
+
new Map()
|
|
1217
|
+
debuggerUrl = seCdp || vendorInfo['debuggerAddress'] || vendorInfo
|
|
1218
|
+
}
|
|
1208
1219
|
this._wsUrl = await this.getWsUrl(debuggerUrl, target, caps)
|
|
1209
1220
|
return new Promise((resolve, reject) => {
|
|
1210
1221
|
try {
|
|
@@ -1945,14 +1956,10 @@ class Window {
|
|
|
1945
1956
|
* @return {!Promise<{x: number, y: number, width: number, height: number}>}
|
|
1946
1957
|
* A promise that will resolve to the window rect of the current window.
|
|
1947
1958
|
*/
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
)
|
|
1953
|
-
} catch (ex) {
|
|
1954
|
-
throw ex
|
|
1955
|
-
}
|
|
1959
|
+
getRect() {
|
|
1960
|
+
return this.driver_.execute(
|
|
1961
|
+
new command.Command(command.Name.GET_WINDOW_RECT)
|
|
1962
|
+
)
|
|
1956
1963
|
}
|
|
1957
1964
|
|
|
1958
1965
|
/**
|
|
@@ -1969,19 +1976,15 @@ class Window {
|
|
|
1969
1976
|
* A promise that will resolve to the current window's updated window
|
|
1970
1977
|
* rect.
|
|
1971
1978
|
*/
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
)
|
|
1982
|
-
} catch (ex) {
|
|
1983
|
-
throw ex
|
|
1984
|
-
}
|
|
1979
|
+
setRect({ x, y, width, height }) {
|
|
1980
|
+
return this.driver_.execute(
|
|
1981
|
+
new command.Command(command.Name.SET_WINDOW_RECT).setParameters({
|
|
1982
|
+
x,
|
|
1983
|
+
y,
|
|
1984
|
+
width,
|
|
1985
|
+
height,
|
|
1986
|
+
})
|
|
1987
|
+
)
|
|
1985
1988
|
}
|
|
1986
1989
|
|
|
1987
1990
|
/**
|
|
@@ -2522,12 +2525,12 @@ class WebElement {
|
|
|
2522
2525
|
if (type === 'number') {
|
|
2523
2526
|
key = String(key)
|
|
2524
2527
|
} else if (type !== 'string') {
|
|
2525
|
-
throw TypeError('each key must be a number
|
|
2528
|
+
throw TypeError('each key must be a number or string; got ' + type)
|
|
2526
2529
|
}
|
|
2527
2530
|
|
|
2528
2531
|
// The W3C protocol requires keys to be specified as an array where
|
|
2529
2532
|
// each element is a single key.
|
|
2530
|
-
keys.push(...key
|
|
2533
|
+
keys.push(...key)
|
|
2531
2534
|
})
|
|
2532
2535
|
|
|
2533
2536
|
if (!this.driver_.fileDetector_) {
|
|
@@ -2544,7 +2547,10 @@ class WebElement {
|
|
|
2544
2547
|
keys.join('')
|
|
2545
2548
|
)
|
|
2546
2549
|
} catch (ex) {
|
|
2547
|
-
console.log(
|
|
2550
|
+
console.log(
|
|
2551
|
+
'Error trying parse string as a file with file detector; sending keys instead' +
|
|
2552
|
+
ex
|
|
2553
|
+
)
|
|
2548
2554
|
}
|
|
2549
2555
|
|
|
2550
2556
|
return this.execute_(
|
|
@@ -2715,14 +2721,8 @@ class WebElement {
|
|
|
2715
2721
|
* @return {!Promise<{width: number, height: number, x: number, y: number}>}
|
|
2716
2722
|
* A promise that will resolve with the element's rect.
|
|
2717
2723
|
*/
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
return await this.execute_(
|
|
2721
|
-
new command.Command(command.Name.GET_ELEMENT_RECT)
|
|
2722
|
-
)
|
|
2723
|
-
} catch (err) {
|
|
2724
|
-
throw err;
|
|
2725
|
-
}
|
|
2724
|
+
getRect() {
|
|
2725
|
+
return this.execute_(new command.Command(command.Name.GET_ELEMENT_RECT))
|
|
2726
2726
|
}
|
|
2727
2727
|
|
|
2728
2728
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "selenium-webdriver",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "The official WebDriver JavaScript bindings from the Selenium project",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"eslint-plugin-prettier": "^3.4.0",
|
|
36
36
|
"express": "^4.17.1",
|
|
37
37
|
"jasmine": "^3.8.0",
|
|
38
|
-
"mocha": "^
|
|
38
|
+
"mocha": "^9.2.2",
|
|
39
39
|
"multer": "^1.4.2",
|
|
40
40
|
"prettier": "^2.3.0",
|
|
41
41
|
"serve-index": "^1.9.1",
|