Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement form verification (async form validation) #9054

Merged
merged 22 commits into from
May 12, 2017

Conversation

cvializ
Copy link
Contributor

@cvializ cvializ commented Apr 29, 2017

Closes #8369

Needs documentation, opening this PR for feedback.

@cvializ cvializ force-pushed the cv-async branch 3 times, most recently from 1b22b04 to 9b012b2 Compare May 1, 2017 17:05
@cvializ cvializ mentioned this pull request May 1, 2017
@cvializ cvializ force-pushed the cv-async branch 2 times, most recently from 6195d98 to 26dff6f Compare May 1, 2017 20:34
@@ -0,0 +1,4431 @@
RecordNumber,Zipcode,City,State
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is alllllllllllllllll of this needed for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original title of the issue was forms: zip code input validation, but it was changed. I thought reading through real California zip codes and cities would be an interesting demo. If you think it's too heavy weight we can just change it to a smaller dict.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's massive.

* @return {?Element}
*/
function getConfig_(form) {
return form.getElementsByTagName('script')[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is finds deep elements. Should this be looking for a child script instead?

});
const config = json && json[CONFIG_KEY];
if (!config.length) {
throw new Error(`The amp-form verification config should contain an array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any future uses for this configuration script? Maybe we shouldn't be throwing an error if they don't specify a verification group?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #getFormVerifier. #parseConfig_ is not called if a verification group is not specified.

throw new Error('Failed to parse amp-form config. Is it valid JSON?');
});
const config = json && json[CONFIG_KEY];
if (!config.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw for an empty JSON object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that correct behavior? If the publisher specifies {"verificationGroups": []} or {} in a form config, it seems like it is a mistake they should be made aware of through an error. Let me know if you'd prefer to allow an empty config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine for this to be an error, but it this line causes an error. As in, the test, not the error we throw on the line below.

* Clear the validity state of this group's elements.
*/
clearErrors() {
this.elements_.forEach(element => element.setCustomValidity(''));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all elements guaranteed to have #setCustomValidity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTML Constraints spec specifies that submittable elements are guaranteed to have #setCustomValidity. A publisher would have to put a non-submittable element in a verification group for this to throw, and it seems unlikely. I could check that every element in the group has a #setCustomValidity method in #parseConfig, let me know if you think that's a good idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, I would just limit them to the form input elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. See #isSubmittable_

export class FormVerifier {
/**
* @param {!HTMLFormElement} form
* @param {!Array<!VerificationGroup>} config
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: rename to groups

// Set the error message on each element that caused an error.
for (let i = 0; i < errors.length; i++) {
const error = errors[i];
const element = scopedQuerySelector(this.form_, `[name="${error.name}"]`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped query selector is unnecessary in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got an error in the presubmit on Travis telling me not to this.form_.querySelector because of strange behavior. Why is it unnecessary in this case?
If you mean use document.querySelector, multiple elements can have identical names so it could select the wrong element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scopedQuerySelector is only necessary when there is a space in the selector (really, when you're matching a descendent of a descendent, see #7260). In this case, you're only matching a single descendent, so no issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. We may want to update the message querySelectorAll produces during the travis presubmit or a comment in the jsdoc for scopedQuerySelector to make that clearer. Thanks!

// Remove the dirty flag from the successful groups that have values.
for (let i = 0; i < this.groups_.length; i++) {
const group = this.groups_[i];
if (group.isFilledOut() && group.containsNoElementOf(errorElements)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use #shouldVerify instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement and the function of #shouldVerify are not equivalent. Could you please explain why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is #verify, which means we're only verifying the elements that #shouldVerify returned true for.

* @param {!Array<!Element>} elements
* @return {boolean}
*/
containsNoElementOf(elements) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: rename to #containsNone

*/
maybeVerify_(afterVerify) {
if (this.shouldVerify_()) {
this.doXhr_().then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're gonna get into a LOT of race conditions here. Need to verify that this is a response to the last issued XHR, not any before it.

Copy link
Contributor

@aghassemi aghassemi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first round of comments for the first half.

@@ -57,6 +57,34 @@
color: #dc4e41;
}

.verification-form label {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @spacedino let's work with Abby on UI design of this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for the example page we use for development, right? It isn't baked in to amp-form. The publisher should be responsible for the spinner UX

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

less so for this example page and more so to kick of the design for AmpByExample and/or AMP Start.

},
{
"name": "fullAddress",
"elements": ["[name='addressLine2']", "[name='city']", "[name='zip']"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's restrict this to just name (considering error response has name which is used to find the target elements later)

maybe: "fields" = ['addressLine2', 'city', 'zip'].

this.validator_.onBlur(e);
}, true);
this.form_.addEventListener('change', e => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be guarded by custom-validation-reporting='as-you-go?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, "as-you-go" is sort of poorly named. It should be called something that implies "use your own error message elements instead of using Chrome's validation tooltip, AND do so as the user fills out fields". The change listener is still necessary when using verification with Chrome's default validation UI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow ok. so what is the default browser UI strategy? is it always "on-submit" and not changeable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, unless you call form.reportValidation, which by default happens on submit.

}
}

getVarSubsFields_() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc for this a few other private methods added


this.setState_(FormState_.VERIFYING);
const verifyXhr = this.doVarSubs_(this.getVarSubsFields_()).then(
() => this.doXhr_(map({verify: true})));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'verify' can collide with existing inputs named verify. Do __amp_form_verify and then check if any input has that name same way SOURCE_ORIGIN_PARAM is checked in the constructor of AMP form.

function parseConfig_(script) {
if (isJsonScriptTag(script)) {
const json = tryParseJson(script.textContent, () => {
throw new Error('Failed to parse amp-form config. Is it valid JSON?');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw user().createError() here and couple of places below.

* data in ways not possible with standard form validation, or check
* values against sets of data too large to fit in browser memory
* e.g. ensuring zip codes match with cities.
* @visibleForTesting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


this.setState_(FormState_.VERIFYING);
const verifyXhr = this.doVarSubs_(this.getVarSubsFields_()).then(
() => this.doXhr_(map({verify: true})));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the map necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not. Could you help me understand what use-cases require our map utility function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was to make for-in loops more robust. I iterate over this object using a for-in loop in #doXhr

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has an old JS meaning and a valid CS meaning.

Old JS, people used to put enumerable properties on the Object.prototype object, which would then magically appear in all for-in loops. We don't do that. Actually we did, but Ali fixed that.

The CS meaning is when the object is treated as a true Map instance with arbitrary accesses. In that case, anything in the prototype chain could be accessed, which is incorrect in 99% of cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, map is more for storing and accessing values, and we don't need it here because it's just a descriptive value and not used for storage.

const isHeadOrGet = this.method_ == 'GET' || this.method_ == 'HEAD';

const p = this.doVarSubs_(varSubsFields)
.then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace.

return;
}

this.setState_(FormState_.VERIFYING);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to do any blocking in this state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like blocking of the submit button? No, that is not planned. This state is intended to allow a spinner to show during the verify request. If the user wants to submit while the verify request is pending, they should be able to.

@cvializ cvializ force-pushed the cv-async branch 5 times, most recently from a278ebf to d2f3150 Compare May 4, 2017 15:44
Copy link
Contributor

@aghassemi aghassemi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second round.
Excellent PR overall Carlos!

this.validator_.onBlur(e);
}, true);
this.form_.addEventListener('change', e => {
const input = dev().assertElement(e.target);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's create an experiment (maybe amp-form-verifier?) and guard entry points to this. Ideally being sure when experiment is off, code paths to existing forms functionality does not change (much).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


const verifyXhr = this.doVarSubs_(this.getVarSubsFields_())
.then(() => this.doXhr_({[FORM_VERIFY_PARAM]: true}));
const reset = () => this.setState_(FormState_.INITIAL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if multiple are still running and one fails while others are still running? we don't want to lose VERIFYING state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll move this to the afterVerify callback

// Set the error message on each element that caused an error.
for (let i = 0; i < errors.length; i++) {
const error = errors[i];
const element = this.form_./*OK*/querySelector(`[name="${error.name}"]`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scopedQuerySelector(this.form_, [name="${error.name}"]) from dom.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jridgewell and I discussed this above, and he shared that scopedQuerySelector is not necessary unless there is a space in the selector. Is that your understanding as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that scopedQuerySelector(parent) can match the parent element itself but parent.querySelector can never match parent. so in that sense yeah no need to use scopedQuerySelector.

With my comment I was hoping to get rid of /*OK*/ as they should be used for exceptions not common cases, looks like querySelector is allowed without `/OK/ anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither can match the parent. The issue is #querySelector is scoped to the document, then filtered to elements inside the parent (div span can match a <div> outside parent and then a <span> inside parent). scopedQuerySelector however, is scoped to the parent only (div span can only match a <div> inside parent and a <span> inside that <div>).


#querySelector is allowed when there is a single string without any spaces (meaning, you can not have '* *'). Because this is a template string, it could do all kinds of weird things. But we know better than the linter in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jridgewell wow good to know (I probably should have known this but never too late to learn :) ). Good job preventing this with presubmit rules.

const error = errors[i];
const element = this.form_./*OK*/querySelector(`[name="${error.name}"]`);
if (element && element.checkValidity()) {
element.setCustomValidity(error.message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if multiple elements have the same name (common with radio buttons) the first one gets it which is fine, let's just document here as a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I checked how the existing validator behaves when a radio button value is required and none is checked and this matches that.
screen shot 2017-05-04 at 10 30 36 am

const status = response.status;
if (status < 200 || status >= 300) {
const retriable = isRetriable(status);
const err = user().createCustomError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not log the full response (since we store logs and response body can contain sensitive info)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing sent in the error report is the message first argument. The other information is just tacked on to the error object and not sent, so the full response won't be logged.
Also, createError doesn't report errors so this error isn't logged anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, createError don't report but whoever is catching .fetch() error may report it. (or we might be reporting all unhandledrejection error, @jridgewell do we report unhandledrejection?)

You are right that response does got get reported right now because it is just a field on FetchError, but do we need it? I like dump error objects that one can't do anything but log them instead of error objects that can be inspected and used to switch code path. (e.g. not a fan of .catch(err => if( err.response.code == 500) {...} )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the property from the error would involve slightly more refactoring effort. The code was already augmenting the error with the response and responseJson properties, just not doing so with a custom error class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#7585

We do log all unhandledrejections.

/**
* Resolves with the result of the last promise added.
*/
export class LastAddedResolver {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome!

this.count_ = 0;

if (opt_promises) {
for (let i = 0; i > opt_promises.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a test that would catch the bug in this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's a pretty goofy mistake I made. Adding the test.

inputs[i].name != SOURCE_ORIGIN_PARAM,
'Illegal input name, %s found: %s', SOURCE_ORIGIN_PARAM, inputs[i]);
const name = inputs[i].name;
user().assert(!name ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First check is unnecessary.

updatedElements.forEach(updatedElement => {
checkUserValidityAfterInteraction_(updatedElement);
});
this.validator_.onBlur(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we blurring here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function's name is not very clear. It's part of the amp-form FormValidator API and it hides any displayed messages. Originally it was always called on blur in the form, but that behavior is also needed when the verifier calls afterVerify.


const verifyXhr = this.doVarSubs_(this.getVarSubsFields_())
.then(() => this.doXhr_({[FORM_VERIFY_PARAM]: true}));
const reset = () => this.setState_(FormState_.INITIAL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to make sure form state is still VERIFYING

this.renderTemplate_(json || {});
this.maybeHandleRedirect_(response);
}, error => {
rethrowAsync('Failed to parse response JSON:', error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a user error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Can you help me understand when something should be a user error? I haven't been able to find anything in the docs/comments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Developer errors are only for state that we absolutely know to be true, and isn't. So, if we expect our BaseElement instance to have an element attached, but it doesn't, that goes to dev logs.

But anything that has to do with user input, like parsing JSON that pub provides, or a response that pub provides, is on them. We don't need to know about it.

Copy link
Contributor Author

@cvializ cvializ May 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't a user error before I made any changes, this is just split out from a larger function. A user error needs a tag, correct? What value should I put for tag given this is not in an amp element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a mistake then. This should be "FORM"

this.setState_(FormState_.SUBMIT_ERROR);
this.renderTemplate_(error.responseJson || {});
this.maybeHandleRedirect_(error.response);
rethrowAsync('Form submission failed:', error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User error.

// Set the error message on each element that caused an error.
for (let i = 0; i < errors.length; i++) {
const error = errors[i];
const element = this.form_./*OK*/querySelector(`[name="${error.name}"]`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither can match the parent. The issue is #querySelector is scoped to the document, then filtered to elements inside the parent (div span can match a <div> outside parent and then a <span> inside parent). scopedQuerySelector however, is scoped to the parent only (div span can only match a <div> inside parent and a <span> inside that <div>).


#querySelector is allowed when there is a single string without any spaces (meaning, you can not have '* *'). Because this is a template string, it could do all kinds of weird things. But we know better than the linter in this case.

}, error => {
// Match the behavior of standard functions by rejecting when an error
// occurs even if it's out of order. e.g. Promise.race and Promise.all
this.reject_(error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect, we're looking for more of a cancel behavior and not a race behavior. This should just reject if the most recently added one rejects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was on the fence about which way to go. I think it can be made to be equivalent through adding promises with a catch at the end, but I agree it's convenient to not have to do that for our common use-case.

/**
* Resolves with the result of the last promise added.
*/
export class LastAddedResolver {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ampproject/a4a will be interested in this.

* Get the result promise.
* @return {!Promise}
*/
get() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be nice if this followed the thenable spec instead (we can get rid of this method then):

then(onFulfilled, onRejected) {
  return this.promise_.then(onFulfilled, onRejected);
}

get() {
return this.promise_;
then(opt_resolve, opt_reject) {
if (!opt_resolve && !opt_reject) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let the promise implementation handle this.

@cvializ cvializ force-pushed the cv-async branch 2 times, most recently from 17ed8cc to fba0ab1 Compare May 4, 2017 22:26
if (status < 200 || status >= 300) {
const retriable = isRetriable(status);
const err = user().createCustomError(
new FetchError(`HTTP error ${status}`, response, retriable));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we creating custom Error classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to adapt the existing code to work better with the type system, since gulp check-types would complain that I was accessing properties not available on an Error instance. The previous implementation instantiated a new Error and augmented it with the response and the responseJson, in effect rejecting an error and passing data together. It was added in #3669. This felt hacky and unclear since the type was sort of lying (it says it's an Error, but it has these extra fields) so I wanted to make the augmentation more formal.

src/log.js Outdated
* @param {string} message
*/
constructor(message) {
super(message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Danger! This may or may not behave as you expect based on how compliant the browser is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran the unit test for this on our SauceLabs suite and it passed on all platforms. Does that mitigate the danger? Do those tests cover every browser/device we support?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more worried about older versions. Extending native classes has been fraught with bugs (see https://github.com/ampproject/amphtml/blob/master/src/custom-element.js#L409-L414).

@@ -361,6 +361,81 @@ The `show-all-on-submit` reporting option shows all validation errors on all inv
#### As You Go
The `as-you-go` reporting option allows your user to see validation messages as they're interacting with the input. For example, if the user types an invalid email address, the user will see the error right away. Once they correct the value, the error goes away.

## Verification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## Verification (Experimental)

also add a first sentence that points out the experimental nature and instructions on how to enable the experiment (e.g. link to https://www.ampproject.org/docs/reference/experimental )

@cvializ
Copy link
Contributor Author

cvializ commented May 11, 2017

I've tested this against some pages in the wild and our manual tests and I feel comfortable merging this now.

@aghassemi aghassemi merged commit c244689 into ampproject:master May 12, 2017
@aghassemi
Copy link
Contributor

merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants