Skip to content

Commit

Permalink
feedback 4
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt committed Jun 10, 2024
1 parent 3983cf9 commit fdf130a
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 43 deletions.
30 changes: 17 additions & 13 deletions packages/playwright-core/src/client/elementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export function convertSelectOptionValues(values: string | api.ElementHandle | S
return { options: values as SelectOption[] };
}

type SetInputFilesFiles = Pick<channels.ElementHandleSetInputFilesParams, 'payloads' | 'localPaths' | 'streams'>;
type SetInputFilesFiles = Pick<channels.ElementHandleSetInputFilesParams, 'payloads' | 'localPaths'| 'localDirectory' | 'streams'>;

function filePayloadExceedsSizeLimit(payloads: FilePayload[]) {
return payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) >= fileUploadSizeLimit;
Expand All @@ -262,33 +262,37 @@ export async function convertInputFiles(files: string | FilePayload | string[] |
if (items.some(item => typeof item === 'string')) {
if (!items.every(item => typeof item === 'string'))
throw new Error('File paths cannot be mixed with buffers');
const itemFileTypes = (await Promise.all(items.map(async item => (await fs.promises.stat(item as string)).isDirectory() ? 'directory' : 'file')));
const itemFileTypes = (await Promise.all((items as string[]).map(async item => (await fs.promises.stat(item)).isDirectory() ? 'directory' : 'file')));
if (new Set(itemFileTypes).size > 1 || itemFileTypes.filter(type => type === 'directory').length > 1)
throw new Error('File paths must be all files or a single directory');

if (context._connection.isRemote()) {
const streams = (await Promise.all((items as string[]).map(async item => {
const isDirectory = (await fs.promises.stat(item)).isDirectory();
const files = isDirectory ? (await fs.promises.readdir(item, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => path.join(f.path, f.name)) : [item];
const { writableStreams } = await context._wrapApiCall(async () => context._channel.createTempFiles({
rootDirName: isDirectory ? path.basename(item) : undefined,
items: await Promise.all(files.map(async file => {
const lastModifiedMs = (await fs.promises.stat(file)).mtimeMs;
const streams = (await Promise.all((items).map(async item => {
const isDirectory = (await fs.promises.stat(item as string)).isDirectory();
const files = isDirectory ? (await fs.promises.readdir(item as string, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => path.join(f.path, f.name)) : [item];
const { writableStreams, rootDir } = await context._wrapApiCall(async () => context._channel.createTempFiles({
rootDirName: isDirectory ? path.basename(item as string) : undefined,
items: await Promise.all((files as string[]).map(async file => {
const lastModifiedMs = (await fs.promises.stat(item as string)).mtimeMs;
return {
name: isDirectory ? path.relative(item, file) : path.basename(file),
name: isDirectory ? path.relative(item as string, file) : path.basename(file),
lastModifiedMs
};
})),
}), true);
for (let i = 0; i < files.length; i++) {
const writable = WritableStream.from(writableStreams[i]);
await pipelineAsync(fs.createReadStream(files[i]), writable.stream());
await pipelineAsync(fs.createReadStream((files as string[])[i]), writable.stream());
}
return writableStreams;
return rootDir ?? writableStreams;
}))).flat();
return { streams };
}
return { localPaths: items.map(f => path.resolve(f as string)) as string[] };
const resolvedItems = (items as string[]).map(f => path.resolve(f));
return {
localPaths: resolvedItems.filter((_, i) => itemFileTypes[i] === 'file'),
localDirectory: resolvedItems.find((_, i) => itemFileTypes[i] === 'directory'),
};
}

const payloads = items as FilePayload[];
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ scheme.BrowserContextCreateTempFilesParams = tObject({
})),
});
scheme.BrowserContextCreateTempFilesResult = tObject({
rootDir: tOptional(tChannel(['WritableStream'])),
writableStreams: tArray(tChannel(['WritableStream'])),
});
scheme.BrowserContextUpdateSubscriptionParams = tObject({
Expand Down Expand Up @@ -1629,6 +1630,7 @@ scheme.FrameSetInputFilesParams = tObject({
mimeType: tOptional(tString),
buffer: tBinary,
}))),
localDirectory: tOptional(tString),
localPaths: tOptional(tArray(tString)),
streams: tOptional(tArray(tChannel(['WritableStream']))),
timeout: tOptional(tNumber),
Expand Down Expand Up @@ -1996,6 +1998,7 @@ scheme.ElementHandleSetInputFilesParams = tObject({
mimeType: tOptional(tString),
buffer: tBinary,
}))),
localDirectory: tOptional(tString),
localPaths: tOptional(tArray(tString)),
streams: tOptional(tArray(tChannel(['WritableStream']))),
timeout: tOptional(tNumber),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,11 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await fs.promises.mkdir(tempDirWithRootName, { recursive: true });
this._context._tempDirs.push(tmpDir);
return {
rootDir: params.rootDirName ? new WritableStreamDispatcher(this, tempDirWithRootName) : undefined,
writableStreams: await Promise.all(params.items.map(async item => {
await fs.promises.mkdir(path.dirname(path.join(tempDirWithRootName, item.name)), { recursive: true });
const file = fs.createWriteStream(path.join(tempDirWithRootName, item.name));
return new WritableStreamDispatcher(this, file, item.lastModifiedMs, params.rootDirName ? tempDirWithRootName : undefined);
return new WritableStreamDispatcher(this, file, item.lastModifiedMs);
}))
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ import * as fs from 'fs';
import { createGuid } from '../../utils';
import type { BrowserContextDispatcher } from './browserContextDispatcher';

export class WritableStreamDispatcher extends Dispatcher<{ guid: string, stream: fs.WriteStream }, channels.WritableStreamChannel, BrowserContextDispatcher> implements channels.WritableStreamChannel {
export class WritableStreamDispatcher extends Dispatcher<{ guid: string, streamOrDirectory: fs.WriteStream | string }, channels.WritableStreamChannel, BrowserContextDispatcher> implements channels.WritableStreamChannel {
_type_WritableStream = true;
private _lastModifiedMs: number | undefined;
private _rootDir: string | undefined;

constructor(scope: BrowserContextDispatcher, stream: fs.WriteStream, lastModifiedMs?: number, rootDir?: string) {
super(scope, { guid: 'writableStream@' + createGuid(), stream }, 'WritableStream', {});
constructor(scope: BrowserContextDispatcher, streamOrDirectory: fs.WriteStream | string, lastModifiedMs?: number) {
super(scope, { guid: 'writableStream@' + createGuid(), streamOrDirectory }, 'WritableStream', {});
this._lastModifiedMs = lastModifiedMs;
this._rootDir = rootDir;
}

async write(params: channels.WritableStreamWriteParams): Promise<channels.WritableStreamWriteResult> {
const stream = this._object.stream;
if (typeof this._object.streamOrDirectory === 'string')
throw new Error('Cannot write to a directory');
const stream = this._object.streamOrDirectory;
await new Promise<void>((fulfill, reject) => {
stream.write(params.binary, error => {
if (error)
Expand All @@ -44,17 +44,21 @@ export class WritableStreamDispatcher extends Dispatcher<{ guid: string, stream:
}

async close() {
const stream = this._object.stream;
if (typeof this._object.streamOrDirectory === 'string')
throw new Error('Cannot close a directory');
const stream = this._object.streamOrDirectory;
await new Promise<void>(fulfill => stream.end(fulfill));
if (this._lastModifiedMs)
await fs.promises.utimes(this.path(), new Date(this._lastModifiedMs), new Date(this._lastModifiedMs));
}

path(): string {
return this._object.stream.path as string;
if (typeof this._object.streamOrDirectory === 'string')
return this._object.streamOrDirectory;
return this._object.streamOrDirectory.path as string;
}

rootDir(): string | undefined {
return this._rootDir;
isDirectory(): boolean {
return typeof this._object.streamOrDirectory === 'string';
}
}
22 changes: 10 additions & 12 deletions packages/playwright-core/src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { prepareFilesForUpload } from './fileUploadUtils';
export type InputFilesItems = {
filePayloads?: types.FilePayload[],
localPaths?: string[]
localDirectory?: string
};

type ActionName = 'click' | 'hover' | 'dblclick' | 'tap' | 'move and up' | 'move and down';
Expand Down Expand Up @@ -625,13 +626,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

async _setInputFiles(progress: Progress, items: InputFilesItems, options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
const { filePayloads, localPaths } = items;
const localPathsFileTypes = (await Promise.all((localPaths ?? []).map(async item => (await fs.promises.stat(item as string)).isDirectory() ? 'directory' : 'file')));
if (new Set(localPathsFileTypes).size > 1 || localPathsFileTypes.filter(type => type === 'directory').length > 1)
throw new Error('File paths must be all files or a single directory');
const doesLocalPathsIncludeDirectory = localPathsFileTypes.includes('directory');
const { filePayloads, localPaths, localDirectory } = items;
const multiple = filePayloads && filePayloads.length > 1 || localPaths && localPaths.length > 1;
const result = await this.evaluateHandleInUtility(([injected, node, { multiple, doesLocalPathsIncludeDirectory }]): Element | undefined => {
const result = await this.evaluateHandleInUtility(([injected, node, { multiple, directoryUpload }]): Element | undefined => {
const element = injected.retarget(node, 'follow-label');
if (!element)
return;
Expand All @@ -640,24 +637,25 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const inputElement = element as HTMLInputElement;
if (multiple && !inputElement.multiple && !inputElement.webkitdirectory)
throw injected.createStacklessError('Non-multiple file input can only accept single file');
if (doesLocalPathsIncludeDirectory && !inputElement.webkitdirectory)
if (directoryUpload && !inputElement.webkitdirectory)
throw injected.createStacklessError('File input does not support directories, pass individual files instead');
return inputElement;
}, { multiple, doesLocalPathsIncludeDirectory });
}, { multiple, directoryUpload: !!localDirectory });
if (result === 'error:notconnected' || !result.asElement())
return 'error:notconnected';
const retargeted = result.asElement() as ElementHandle<HTMLInputElement>;
await progress.beforeInputAction(this);
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects.
if (localPaths) {
await Promise.all(localPaths.map(localPath => (
if (localPaths || localDirectory) {
const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!;
await Promise.all((localPathsOrDirectory).map(localPath => (
fs.promises.access(localPath, fs.constants.F_OK)
)));
const waitForInputEvent = doesLocalPathsIncludeDirectory ? this.evaluate(node => new Promise<any>(fulfill => {
const waitForInputEvent = localDirectory ? this.evaluate(node => new Promise<any>(fulfill => {
node.addEventListener('input', fulfill, { once: true });
})).catch(() => {}) : Promise.resolve();
await this._page._delegate.setInputFilePaths(retargeted, localPaths);
await this._page._delegate.setInputFilePaths(retargeted, localPathsOrDirectory);
await waitForInputEvent;
} else {
await this._page._delegate.setInputFiles(retargeted, filePayloads!);
Expand Down
11 changes: 4 additions & 7 deletions packages/playwright-core/src/server/fileUploadUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,14 @@ async function filesExceedUploadLimit(files: string[]) {

export async function prepareFilesForUpload(frame: Frame, params: channels.ElementHandleSetInputFilesParams): Promise<InputFilesItems> {
const { payloads, streams } = params;
let { localPaths } = params;
let { localPaths, localDirectory } = params;

if ([payloads, localPaths, streams].filter(Boolean).length !== 1)
throw new Error('Exactly one of payloads, localPaths and streams must be provided');

if (streams) {
const directoryMode = streams.every(c => (c as WritableStreamDispatcher).rootDir());
if (directoryMode)
localPaths = Array.from(new Set(streams.map(c => (c as WritableStreamDispatcher).rootDir()!)));
else
localPaths = streams.map(c => (c as WritableStreamDispatcher).path());
localPaths = streams.map(c => (c as WritableStreamDispatcher)).filter(s => !s.isDirectory()).map(s => s.path());
localDirectory = streams.map(c => (c as WritableStreamDispatcher)).filter(s => s.isDirectory()).map(s => s.path())[0];
}
if (localPaths) {
for (const p of localPaths)
Expand Down Expand Up @@ -78,5 +75,5 @@ export async function prepareFilesForUpload(frame: Frame, params: channels.Eleme
lastModifiedMs: payload.lastModifiedMs
}));

return { localPaths, filePayloads };
return { localPaths, localDirectory, filePayloads };
}
5 changes: 5 additions & 0 deletions packages/protocol/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1748,6 +1748,7 @@ export type BrowserContextCreateTempFilesOptions = {
rootDirName?: string,
};
export type BrowserContextCreateTempFilesResult = {
rootDir?: WritableStreamChannel,
writableStreams: WritableStreamChannel[],
};
export type BrowserContextUpdateSubscriptionParams = {
Expand Down Expand Up @@ -2916,6 +2917,7 @@ export type FrameSetInputFilesParams = {
mimeType?: string,
buffer: Binary,
}[],
localDirectory?: string,
localPaths?: string[],
streams?: WritableStreamChannel[],
timeout?: number,
Expand All @@ -2928,6 +2930,7 @@ export type FrameSetInputFilesOptions = {
mimeType?: string,
buffer: Binary,
}[],
localDirectory?: string,
localPaths?: string[],
streams?: WritableStreamChannel[],
timeout?: number,
Expand Down Expand Up @@ -3540,6 +3543,7 @@ export type ElementHandleSetInputFilesParams = {
mimeType?: string,
buffer: Binary,
}[],
localDirectory?: string,
localPaths?: string[],
streams?: WritableStreamChannel[],
timeout?: number,
Expand All @@ -3551,6 +3555,7 @@ export type ElementHandleSetInputFilesOptions = {
mimeType?: string,
buffer: Binary,
}[],
localDirectory?: string,
localPaths?: string[],
streams?: WritableStreamChannel[],
timeout?: number,
Expand Down
3 changes: 3 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,7 @@ BrowserContext:
name: string
lastModifiedMs: number?
returns:
rootDir: WritableStream?
writableStreams:
type: array
items: WritableStream
Expand Down Expand Up @@ -2195,6 +2196,7 @@ Frame:
name: string
mimeType: string?
buffer: binary
localDirectory: string?
localPaths:
type: array?
items: string
Expand Down Expand Up @@ -2755,6 +2757,7 @@ ElementHandle:
name: string
mimeType: string?
buffer: binary
localDirectory: string?
localPaths:
type: array?
items: string
Expand Down

0 comments on commit fdf130a

Please sign in to comment.