mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-03 07:51:12 +03:00
fix: preserve lastModified timestamp in setInputFiles (#27671)
Fixes #27452
This commit is contained in:
parent
7cd390b708
commit
bd58c0d5d2
@ -152,9 +152,10 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
||||
throw new Error('Cannot set input files to detached element');
|
||||
const converted = await convertInputFiles(files, frame.page().context());
|
||||
if (converted.files) {
|
||||
debugLogger.log('api', 'setting input buffers');
|
||||
await this._elementChannel.setInputFiles({ files: converted.files, ...options });
|
||||
} else {
|
||||
debugLogger.log('api', 'switching to large files mode');
|
||||
debugLogger.log('api', 'setting input file paths');
|
||||
await this._elementChannel.setInputFilePaths({ ...converted, ...options });
|
||||
}
|
||||
}
|
||||
@ -265,18 +266,13 @@ type InputFilesList = {
|
||||
export async function convertInputFiles(files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<InputFilesList> {
|
||||
const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files];
|
||||
|
||||
const sizeLimit = 50 * 1024 * 1024;
|
||||
const totalBufferSizeExceedsLimit = items.reduce((size, item) => size + ((typeof item === 'object' && item.buffer) ? item.buffer.byteLength : 0), 0) > sizeLimit;
|
||||
if (totalBufferSizeExceedsLimit)
|
||||
throw new Error('Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.');
|
||||
|
||||
const stats = await Promise.all(items.filter(isString).map(item => fs.promises.stat(item as string)));
|
||||
const totalFileSizeExceedsLimit = stats.reduce((acc, stat) => acc + stat.size, 0) > sizeLimit;
|
||||
if (totalFileSizeExceedsLimit) {
|
||||
if (items.some(item => typeof item === 'string')) {
|
||||
if (!items.every(item => typeof item === 'string'))
|
||||
throw new Error('File paths cannot be mixed with buffers');
|
||||
if (context._connection.isRemote()) {
|
||||
const streams: channels.WritableStreamChannel[] = await Promise.all(items.map(async item => {
|
||||
assert(isString(item));
|
||||
const { writableStream: stream } = await context._channel.createTempFile({ name: path.basename(item) });
|
||||
const streams: channels.WritableStreamChannel[] = await Promise.all((items as string[]).map(async item => {
|
||||
const lastModifiedMs = (await fs.promises.stat(item)).mtimeMs;
|
||||
const { writableStream: stream } = await context._channel.createTempFile({ name: path.basename(item), lastModifiedMs });
|
||||
const writable = WritableStream.from(stream);
|
||||
await pipelineAsync(fs.createReadStream(item), writable.stream());
|
||||
return stream;
|
||||
@ -286,21 +282,13 @@ export async function convertInputFiles(files: string | FilePayload | string[] |
|
||||
return { localPaths: items.map(f => path.resolve(f as string)) as string[] };
|
||||
}
|
||||
|
||||
const filePayloads: SetInputFilesFiles = await Promise.all(items.map(async item => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
name: path.basename(item),
|
||||
buffer: await fs.promises.readFile(item)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: item.name,
|
||||
mimeType: item.mimeType,
|
||||
buffer: item.buffer,
|
||||
};
|
||||
}
|
||||
}));
|
||||
return { files: filePayloads };
|
||||
const payloads = items as FilePayload[];
|
||||
const sizeLimit = 50 * 1024 * 1024;
|
||||
const totalBufferSizeExceedsLimit = payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) > sizeLimit;
|
||||
if (totalBufferSizeExceedsLimit)
|
||||
throw new Error('Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.');
|
||||
|
||||
return { files: payloads };
|
||||
}
|
||||
|
||||
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
|
||||
|
@ -402,9 +402,10 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
||||
async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions = {}): Promise<void> {
|
||||
const converted = await convertInputFiles(files, this.page().context());
|
||||
if (converted.files) {
|
||||
debugLogger.log('api', 'setting input buffers');
|
||||
await this._channel.setInputFiles({ selector, files: converted.files, ...options });
|
||||
} else {
|
||||
debugLogger.log('api', 'switching to large files mode');
|
||||
debugLogger.log('api', 'setting input file paths');
|
||||
await this._channel.setInputFilePaths({ selector, ...converted, ...options });
|
||||
}
|
||||
}
|
||||
|
@ -940,6 +940,7 @@ scheme.BrowserContextHarExportResult = tObject({
|
||||
});
|
||||
scheme.BrowserContextCreateTempFileParams = tObject({
|
||||
name: tString,
|
||||
lastModifiedMs: tOptional(tNumber),
|
||||
});
|
||||
scheme.BrowserContextCreateTempFileResult = tObject({
|
||||
writableStream: tChannel(['WritableStream']),
|
||||
|
@ -182,7 +182,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
await fs.promises.mkdir(tmpDir);
|
||||
this._context._tempDirs.push(tmpDir);
|
||||
const file = fs.createWriteStream(path.join(tmpDir, params.name));
|
||||
return { writableStream: new WritableStreamDispatcher(this, file) };
|
||||
return { writableStream: new WritableStreamDispatcher(this, file, params.lastModifiedMs) };
|
||||
}
|
||||
|
||||
async setDefaultNavigationTimeoutNoReply(params: channels.BrowserContextSetDefaultNavigationTimeoutNoReplyParams) {
|
||||
|
@ -16,14 +16,17 @@
|
||||
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import type * as fs from 'fs';
|
||||
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 {
|
||||
_type_WritableStream = true;
|
||||
constructor(scope: BrowserContextDispatcher, stream: fs.WriteStream) {
|
||||
private _lastModifiedMs: number | undefined;
|
||||
|
||||
constructor(scope: BrowserContextDispatcher, stream: fs.WriteStream, lastModifiedMs?: number) {
|
||||
super(scope, { guid: 'writableStream@' + createGuid(), stream }, 'WritableStream', {});
|
||||
this._lastModifiedMs = lastModifiedMs;
|
||||
}
|
||||
|
||||
async write(params: channels.WritableStreamWriteParams): Promise<channels.WritableStreamWriteResult> {
|
||||
@ -41,6 +44,8 @@ export class WritableStreamDispatcher extends Dispatcher<{ guid: string, stream:
|
||||
async close() {
|
||||
const stream = this._object.stream;
|
||||
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 {
|
||||
|
@ -1702,9 +1702,10 @@ export type BrowserContextHarExportResult = {
|
||||
};
|
||||
export type BrowserContextCreateTempFileParams = {
|
||||
name: string,
|
||||
lastModifiedMs?: number,
|
||||
};
|
||||
export type BrowserContextCreateTempFileOptions = {
|
||||
|
||||
lastModifiedMs?: number,
|
||||
};
|
||||
export type BrowserContextCreateTempFileResult = {
|
||||
writableStream: WritableStreamChannel,
|
||||
|
@ -1162,6 +1162,7 @@ BrowserContext:
|
||||
createTempFile:
|
||||
parameters:
|
||||
name: string
|
||||
lastModifiedMs: number?
|
||||
returns:
|
||||
writableStream: WritableStream
|
||||
|
||||
|
@ -698,6 +698,26 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
await Promise.all([uploadFile, file1.filepath].map(fs.promises.unlink));
|
||||
});
|
||||
|
||||
test('setInputFiles should preserve lastModified timestamp', async ({ connect, startRemoteServer, asset }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27452' });
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint());
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.setContent(`<input type=file multiple=true/>`);
|
||||
const input = page.locator('input');
|
||||
const files = ['file-to-upload.txt', 'file-to-upload-2.txt'];
|
||||
await input.setInputFiles(files.map(f => asset(f)));
|
||||
expect(await input.evaluate(e => [...(e as HTMLInputElement).files].map(f => f.name))).toEqual(files);
|
||||
const timestamps = await input.evaluate(e => [...(e as HTMLInputElement).files].map(f => f.lastModified));
|
||||
const expectedTimestamps = files.map(file => Math.round(fs.statSync(asset(file)).mtimeMs));
|
||||
// On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even
|
||||
// rounds it to seconds in WebKit: 1696272058110 -> 1696272058000.
|
||||
for (let i = 0; i < timestamps.length; i++)
|
||||
expect(Math.abs(timestamps[i] - expectedTimestamps[i]), `expected: ${expectedTimestamps}; actual: ${timestamps}`).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('should connect over http', async ({ connect, startRemoteServer, mode }) => {
|
||||
test.skip(mode !== 'default');
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
|
@ -613,4 +613,19 @@ it('input should trigger events when files changed second time', async ({ page,
|
||||
await input.setInputFiles(asset('pptr.png'));
|
||||
expect(await input.evaluate(e => (e as HTMLInputElement).files[0].name)).toBe('pptr.png');
|
||||
expect(await events.evaluate(e => e)).toEqual(['input', 'change']);
|
||||
});
|
||||
|
||||
it('should preserve lastModified timestamp', async ({ page, asset }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27452' });
|
||||
await page.setContent(`<input type=file multiple=true/>`);
|
||||
const input = page.locator('input');
|
||||
const files = ['file-to-upload.txt', 'file-to-upload-2.txt'];
|
||||
await input.setInputFiles(files.map(f => asset(f)));
|
||||
expect(await input.evaluate(e => [...(e as HTMLInputElement).files].map(f => f.name))).toEqual(files);
|
||||
const timestamps = await input.evaluate(e => [...(e as HTMLInputElement).files].map(f => f.lastModified));
|
||||
const expectedTimestamps = files.map(file => Math.round(fs.statSync(asset(file)).mtimeMs));
|
||||
// On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even
|
||||
// rounds it to seconds in WebKit: 1696272058110 -> 1696272058000.
|
||||
for (let i = 0; i < timestamps.length; i++)
|
||||
expect(Math.abs(timestamps[i] - expectedTimestamps[i]), `expected: ${expectedTimestamps}; actual: ${timestamps}`).toBeLessThan(1000);
|
||||
});
|
Loading…
Reference in New Issue
Block a user