Skip to content

Commit

Permalink
fix(fill): make fill work with date/time inputs (#1676)
Browse files Browse the repository at this point in the history
Date/time inputs are locale-specific, and also do not work with insertText. We just set the value on them and emulate input/change events. Note that some browsers do not support these input types just yet.
  • Loading branch information
dgozman authored Apr 7, 2020
1 parent e0c8fbf commit e683c08
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 16 deletions.
16 changes: 9 additions & 7 deletions src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
const error = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value);
if (error)
throw new Error(error);
if (value)
await this._page.keyboard.insertText(value);
else
await this._page.keyboard.press('Delete');
const errorOrNeedsInput = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value);
if (typeof errorOrNeedsInput === 'string')
throw new Error(errorOrNeedsInput);
if (errorOrNeedsInput) {
if (value)
await this._page.keyboard.insertText(value);
else
await this._page.keyboard.press('Delete');
}
}, options, true);
}

Expand Down
30 changes: 22 additions & 8 deletions src/injected/injected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,12 @@ class Injected {
return 'Element is not visible';
if (element.nodeName.toLowerCase() === 'input') {
const input = element as HTMLInputElement;
const type = input.getAttribute('type') || '';
const type = (input.getAttribute('type') || '').toLowerCase();
const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']);
const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
if (!kTextInputTypes.has(type.toLowerCase()))
if (!kTextInputTypes.has(type) && !kDateTypes.has(type))
return 'Cannot fill input of type "' + type + '".';
if (type.toLowerCase() === 'number') {
if (type === 'number') {
value = value.trim();
if (!value || isNaN(Number(value)))
return 'Cannot type text into input[type=number].';
Expand All @@ -172,9 +173,21 @@ class Injected {
return 'Cannot fill a disabled input.';
if (input.readOnly)
return 'Cannot fill a readonly input.';
if (kDateTypes.has(type)) {
value = value.trim();
input.focus();
input.value = value;
if (input.value !== value)
return `Malformed ${type} "${value}"`;
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return false; // We have already changed the value, no need to input it.
}
input.select();
input.focus();
} else if (element.nodeName.toLowerCase() === 'textarea') {
return true;
}
if (element.nodeName.toLowerCase() === 'textarea') {
const textarea = element as HTMLTextAreaElement;
if (textarea.disabled)
return 'Cannot fill a disabled textarea.';
Expand All @@ -183,7 +196,9 @@ class Injected {
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
textarea.focus();
} else if (element.isContentEditable) {
return true;
}
if (element.isContentEditable) {
const range = element.ownerDocument!.createRange();
range.selectNodeContents(element);
const selection = element.ownerDocument!.defaultView!.getSelection();
Expand All @@ -192,10 +207,9 @@ class Injected {
selection.removeAllRanges();
selection.addRange(range);
element.focus();
} else {
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
return true;
}
return false;
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
}

isCheckboxChecked(node: Node) {
Expand Down
33 changes: 32 additions & 1 deletion test/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,7 @@ module.exports.describe = function({playwright, FFOX, CHROMIUM, WEBKIT}) {
});
it('should throw on unsupported inputs', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
for (const type of ['color', 'date']) {
for (const type of ['color', 'file']) {
await page.$eval('input', (input, type) => input.setAttribute('type', type), type);
let error = null;
await page.fill('input', '').catch(e => error = e);
Expand All @@ -933,6 +933,37 @@ module.exports.describe = function({playwright, FFOX, CHROMIUM, WEBKIT}) {
expect(await page.evaluate(() => result)).toBe('text ' + type);
}
});
it('should fill date input after clicking', async({page, server}) => {
await page.setContent('<input type=date>');
await page.click('input');
await page.fill('input', '2020-03-02');
expect(await page.$eval('input', input => input.value)).toBe('2020-03-02');
});
it.skip(WEBKIT)('should throw on incorrect date', async({page, server}) => {
await page.setContent('<input type=date>');
const error = await page.fill('input', '2020-13-05').catch(e => e);
expect(error.message).toBe('Malformed date "2020-13-05"');
});
it('should fill time input', async({page, server}) => {
await page.setContent('<input type=time>');
await page.fill('input', '13:15');
expect(await page.$eval('input', input => input.value)).toBe('13:15');
});
it.skip(WEBKIT)('should throw on incorrect time', async({page, server}) => {
await page.setContent('<input type=time>');
const error = await page.fill('input', '25:05').catch(e => e);
expect(error.message).toBe('Malformed time "25:05"');
});
it('should fill datetime-local input', async({page, server}) => {
await page.setContent('<input type=datetime-local>');
await page.fill('input', '2020-03-02T05:15');
expect(await page.$eval('input', input => input.value)).toBe('2020-03-02T05:15');
});
it.skip(WEBKIT || FFOX)('should throw on incorrect datetime-local', async({page, server}) => {
await page.setContent('<input type=datetime-local>');
const error = await page.fill('input', 'abc').catch(e => e);
expect(error.message).toBe('Malformed datetime-local "abc"');
});
it('should fill contenteditable', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.fill('div[contenteditable]', 'some value');
Expand Down

0 comments on commit e683c08

Please sign in to comment.