mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-04 06:03:10 +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');
|
throw new Error('Cannot set input files to detached element');
|
||||||
const converted = await convertInputFiles(files, frame.page().context());
|
const converted = await convertInputFiles(files, frame.page().context());
|
||||||
if (converted.files) {
|
if (converted.files) {
|
||||||
|
debugLogger.log('api', 'setting input buffers');
|
||||||
await this._elementChannel.setInputFiles({ files: converted.files, ...options });
|
await this._elementChannel.setInputFiles({ files: converted.files, ...options });
|
||||||
} else {
|
} else {
|
||||||
debugLogger.log('api', 'switching to large files mode');
|
debugLogger.log('api', 'setting input file paths');
|
||||||
await this._elementChannel.setInputFilePaths({ ...converted, ...options });
|
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> {
|
export async function convertInputFiles(files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<InputFilesList> {
|
||||||
const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files];
|
const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files];
|
||||||
|
|
||||||
const sizeLimit = 50 * 1024 * 1024;
|
if (items.some(item => typeof item === 'string')) {
|
||||||
const totalBufferSizeExceedsLimit = items.reduce((size, item) => size + ((typeof item === 'object' && item.buffer) ? item.buffer.byteLength : 0), 0) > sizeLimit;
|
if (!items.every(item => typeof item === 'string'))
|
||||||
if (totalBufferSizeExceedsLimit)
|
throw new Error('File paths cannot be mixed with buffers');
|
||||||
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 (context._connection.isRemote()) {
|
if (context._connection.isRemote()) {
|
||||||
const streams: channels.WritableStreamChannel[] = await Promise.all(items.map(async item => {
|
const streams: channels.WritableStreamChannel[] = await Promise.all((items as string[]).map(async item => {
|
||||||
assert(isString(item));
|
const lastModifiedMs = (await fs.promises.stat(item)).mtimeMs;
|
||||||
const { writableStream: stream } = await context._channel.createTempFile({ name: path.basename(item) });
|
const { writableStream: stream } = await context._channel.createTempFile({ name: path.basename(item), lastModifiedMs });
|
||||||
const writable = WritableStream.from(stream);
|
const writable = WritableStream.from(stream);
|
||||||
await pipelineAsync(fs.createReadStream(item), writable.stream());
|
await pipelineAsync(fs.createReadStream(item), writable.stream());
|
||||||
return 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[] };
|
return { localPaths: items.map(f => path.resolve(f as string)) as string[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePayloads: SetInputFilesFiles = await Promise.all(items.map(async item => {
|
const payloads = items as FilePayload[];
|
||||||
if (typeof item === 'string') {
|
const sizeLimit = 50 * 1024 * 1024;
|
||||||
return {
|
const totalBufferSizeExceedsLimit = payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) > sizeLimit;
|
||||||
name: path.basename(item),
|
if (totalBufferSizeExceedsLimit)
|
||||||
buffer: await fs.promises.readFile(item)
|
throw new Error('Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.');
|
||||||
};
|
|
||||||
} else {
|
return { files: payloads };
|
||||||
return {
|
|
||||||
name: item.name,
|
|
||||||
mimeType: item.mimeType,
|
|
||||||
buffer: item.buffer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
return { files: filePayloads };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
|
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> {
|
async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions = {}): Promise<void> {
|
||||||
const converted = await convertInputFiles(files, this.page().context());
|
const converted = await convertInputFiles(files, this.page().context());
|
||||||
if (converted.files) {
|
if (converted.files) {
|
||||||
|
debugLogger.log('api', 'setting input buffers');
|
||||||
await this._channel.setInputFiles({ selector, files: converted.files, ...options });
|
await this._channel.setInputFiles({ selector, files: converted.files, ...options });
|
||||||
} else {
|
} else {
|
||||||
debugLogger.log('api', 'switching to large files mode');
|
debugLogger.log('api', 'setting input file paths');
|
||||||
await this._channel.setInputFilePaths({ selector, ...converted, ...options });
|
await this._channel.setInputFilePaths({ selector, ...converted, ...options });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -940,6 +940,7 @@ scheme.BrowserContextHarExportResult = tObject({
|
|||||||
});
|
});
|
||||||
scheme.BrowserContextCreateTempFileParams = tObject({
|
scheme.BrowserContextCreateTempFileParams = tObject({
|
||||||
name: tString,
|
name: tString,
|
||||||
|
lastModifiedMs: tOptional(tNumber),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextCreateTempFileResult = tObject({
|
scheme.BrowserContextCreateTempFileResult = tObject({
|
||||||
writableStream: tChannel(['WritableStream']),
|
writableStream: tChannel(['WritableStream']),
|
||||||
|
@ -182,7 +182,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||||||
await fs.promises.mkdir(tmpDir);
|
await fs.promises.mkdir(tmpDir);
|
||||||
this._context._tempDirs.push(tmpDir);
|
this._context._tempDirs.push(tmpDir);
|
||||||
const file = fs.createWriteStream(path.join(tmpDir, params.name));
|
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) {
|
async setDefaultNavigationTimeoutNoReply(params: channels.BrowserContextSetDefaultNavigationTimeoutNoReplyParams) {
|
||||||
|
@ -16,14 +16,17 @@
|
|||||||
|
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { Dispatcher } from './dispatcher';
|
import { Dispatcher } from './dispatcher';
|
||||||
import type * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { createGuid } from '../../utils';
|
import { createGuid } from '../../utils';
|
||||||
import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
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, stream: fs.WriteStream }, channels.WritableStreamChannel, BrowserContextDispatcher> implements channels.WritableStreamChannel {
|
||||||
_type_WritableStream = true;
|
_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', {});
|
super(scope, { guid: 'writableStream@' + createGuid(), stream }, 'WritableStream', {});
|
||||||
|
this._lastModifiedMs = lastModifiedMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async write(params: channels.WritableStreamWriteParams): Promise<channels.WritableStreamWriteResult> {
|
async write(params: channels.WritableStreamWriteParams): Promise<channels.WritableStreamWriteResult> {
|
||||||
@ -41,6 +44,8 @@ export class WritableStreamDispatcher extends Dispatcher<{ guid: string, stream:
|
|||||||
async close() {
|
async close() {
|
||||||
const stream = this._object.stream;
|
const stream = this._object.stream;
|
||||||
await new Promise<void>(fulfill => stream.end(fulfill));
|
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 {
|
path(): string {
|
||||||
|
@ -1702,9 +1702,10 @@ export type BrowserContextHarExportResult = {
|
|||||||
};
|
};
|
||||||
export type BrowserContextCreateTempFileParams = {
|
export type BrowserContextCreateTempFileParams = {
|
||||||
name: string,
|
name: string,
|
||||||
|
lastModifiedMs?: number,
|
||||||
};
|
};
|
||||||
export type BrowserContextCreateTempFileOptions = {
|
export type BrowserContextCreateTempFileOptions = {
|
||||||
|
lastModifiedMs?: number,
|
||||||
};
|
};
|
||||||
export type BrowserContextCreateTempFileResult = {
|
export type BrowserContextCreateTempFileResult = {
|
||||||
writableStream: WritableStreamChannel,
|
writableStream: WritableStreamChannel,
|
||||||
|
@ -1162,6 +1162,7 @@ BrowserContext:
|
|||||||
createTempFile:
|
createTempFile:
|
||||||
parameters:
|
parameters:
|
||||||
name: string
|
name: string
|
||||||
|
lastModifiedMs: number?
|
||||||
returns:
|
returns:
|
||||||
writableStream: WritableStream
|
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));
|
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('should connect over http', async ({ connect, startRemoteServer, mode }) => {
|
||||||
test.skip(mode !== 'default');
|
test.skip(mode !== 'default');
|
||||||
const remoteServer = await startRemoteServer(kind);
|
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'));
|
await input.setInputFiles(asset('pptr.png'));
|
||||||
expect(await input.evaluate(e => (e as HTMLInputElement).files[0].name)).toBe('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']);
|
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