-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
✨Integrate E2E Testing with demo viewer and demo PWA shadow doc. #20994
Conversation
node.dataset['iAmphtmlTestId'] = value; | ||
}; | ||
const removeData = node => delete node.dataset['iAmphtmlTestId']; | ||
const getIt = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be cleaner to have an iterator that gives you what you want, and have your iteration code be straightforward:
function* createIterator(context) {
const it = document.createNodeIterator(context, NodeFilter.SHOW_ELEMENT);
let node;
let index = 0;
while(node = it.nextNode()) {
index++;
yield { node, index };
}
}
and use it as:
for (const {node, index} of createIterator(context)) {
toggleData(node, index);
}
I would consider a step further and compose:
function* createSimpleElementIterator(context) {
const it = document.createNodeIterator(context, NodeFilter.SHOW_ELEMENT);
while(node = it.nextNode()) {
yield node;
}
}
function* createIndexedInterator(iter) {
let index = 0;
for (const item of iter) {
index++;
yield {item, index};
}
}
const iter = createIndexedIterator(createSimpleElementIterator(context));
because now you will have generic composable building blocks that can ideally live outside your code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, now that we're using async
/await
it makes sense to use generators as well.
const webElement = await this.driver.findElement(byXpath); | ||
await this.maybeInstallXpath_(); | ||
|
||
const webElement = await this.driver.wait(new Condition('', async() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
label?
} | ||
this.isXpathInstalled_ = true; | ||
|
||
const script = fs.readFileSync('third_party/wgxpath/wgxpath.js', 'utf8'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure that fs.readFileAsync returns a Promise
, so would be better to do that rather than have a sync call here when everything else is async.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, TIL. I thought there were only continuation passing readFile
and the sync readFileSync
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there is no readFileAsync
, but there is an experimental feature fsPromises:
const {readFile} = require('fs').promises;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it looks like we have this in our build-system:
const BBPromise = require('bluebird');
const fs = BBPromise.promisifyAll(require('fs'));
which is what is adding the async versions. Could use that to stay consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh neat, thanks
@@ -21,32 +21,27 @@ import { | |||
} from './helpers'; | |||
|
|||
describes.endtoend('AMP carousel', { | |||
fixture: 'http://localhost:8000/test/manual/amp-carousel-0-2/basic.amp.html', | |||
experiments: ['amp-carousel-v2'], | |||
environment: 'shadow-demo', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the plan that if you want to test under multiple environments, you create multiple describes sharing the same test runner function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, or a describes.repeated
. It might be possible to pass an array and do the describes.repeated
under the hood, but I'm not sure how I'd work that right now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO one of the most important features for e2e testing is to automatically run a given test on all environments by default. Happy to chat about what this would require.
/cc @rsimha
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd imagine we can use a describes.repeated
block to iterate across the different types of Environment
s in AmpEnvironments
, just like this code goes over the variants for templateType
.
amphtml/extensions/amp-mustache/0.1/test/test-amp-mustache.js
Lines 22 to 72 in ce617b7
describes.repeated('amp-mustache 0.1', { | |
'with script[type=text/plain][template=amp-mustache]': | |
{templateType: 'script'}, | |
'with template[type=amp-mustache]': | |
{templateType: 'template'}, | |
}, (name, variant) => { | |
let sandbox; | |
let viewerCanRenderTemplates = false; | |
beforeEach(() => { | |
sandbox = sinon.sandbox; | |
const getServiceForDocStub = sandbox.stub(service, 'getServiceForDoc'); | |
getServiceForDocStub.returns({ | |
hasCapability: unused => viewerCanRenderTemplates, | |
}); | |
}); | |
afterEach(() => sandbox.restore()); | |
let innerHtmlSetup; | |
let template; | |
let templateElement; | |
let textContentSetup; | |
let isTemplateType; | |
let isTemplateTypeScript; | |
beforeEach(() => { | |
const {templateType} = variant; | |
templateElement = document.createElement(templateType); | |
if (templateType == 'script') { | |
templateElement.setAttribute('type', 'amp-mustache'); | |
} | |
template = new AmpMustache(templateElement); | |
isTemplateTypeScript = templateType == 'script'; | |
isTemplateType = templateType == 'template'; | |
textContentSetup = contents => { | |
if (isTemplateType) { | |
templateElement.content.textContent = contents; | |
} else if (isTemplateTypeScript) { | |
templateElement.textContent = contents; | |
} | |
}; | |
innerHtmlSetup = html => { | |
if (isTemplateType) { | |
templateElement./*OK*/innerHTML = html; | |
} else if (isTemplateTypeScript) { | |
templateElement.textContent = html; | |
} | |
}; | |
}); |
/cc @alabiaga
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rsimha that's what I was imagining as well. This would be the easiest implementation for now. We can refactor later to allow passing an array and have the describes.e2e handle the repetition, if that is ok with everyone
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM. I like this because it allows us to define a pattern in once place and reuse it across several tests.
/cc @estherkim
const webElement = await this.driver.wait(new Condition('', async() => { | ||
try { | ||
const root = await this.getRoot_(); | ||
const results = await this.evaluate(queryXpath, xpath, root); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about using selenium's built-in locator? https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_By.html#By.xpath
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The built-in XPath locator doesn't work for elements inside ShadowRoot
s, unfortunately. The wgxpath
library added in third_party
allows evaluating XPath queries in detached document trees as well. The comment block in query-xpath.js
has a detailed description. Let me know if I can help with any questions about that
Newbie question about |
I think you're right. The page implements some of the viewer API, but I don't think it implements all of it. As I understand it, this is meant to run the core viewer API like for loading documents in an iframe, but it doesn't intend to implement every feature like server side template rendering. /cc @alabiaga or @choumx to check my understanding |
@@ -21,32 +21,27 @@ import { | |||
} from './helpers'; | |||
|
|||
describes.endtoend('AMP carousel', { | |||
fixture: 'http://localhost:8000/test/manual/amp-carousel-0-2/basic.amp.html', | |||
experiments: ['amp-carousel-v2'], | |||
environment: 'shadow-demo', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO one of the most important features for e2e testing is to automatically run a given test on all environments by default. Happy to chat about what this would require.
/cc @rsimha
@@ -21,32 +21,27 @@ import { | |||
} from './helpers'; | |||
|
|||
describes.endtoend('AMP carousel', { | |||
fixture: 'http://localhost:8000/test/manual/amp-carousel-0-2/basic.amp.html', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the nice things about describes.integration
is that it abstracts away boilerplate markup (e.g. <head>
content) and inlines the test fixture. This also makes it easy to reuse a fixture/test tuple in different validator specs. Check out #19943.
I think we should also do that here if possible.
Server side template rendering is tested in this example viewer (capability is called viewerRenderTemplate) but note that the actual proxying of the request from the viewer's processRequest_ method to an actual REST end point doesn't happen. I just mock the rendered response expected from the server. So it isn't a true end to end test but should suffice as long email clients conform to the expected JSON format response required. But yes this viewer also doesn't fully test the other capabilities that viewers can support. |
|
||
/** @enum {string} */ | ||
const Environment = { | ||
SINGLE: 'single', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about this name. We mean single doc yes, SINGLE_DOC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, there's a precedent for single
as an ampdoc
environment.
amphtml/extensions/amp-ad/0.1/test/test-amp-ad-custom.js
Lines 141 to 145 in e194f8e
describes.realWin('with single AmpDoc', { | |
amp: { | |
ampdoc: 'single', | |
}, | |
}, env => { |
Does the precedent make you feel better about it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, but here the value single
is associated with the property ampdoc
and so it reads well. In your case you're associating single
with an environment
, which is not intuitive to me. Though the following would be clearer.
const AMP_DOC_ENVIRONMENT = {
SINGLE: 'single',
}
then it would be clearer. I don't feel strongly about this. Perhaps the fact that others have not pointed this out probably shows that I am the only one not too keen about the naming.
build-system/tasks/e2e/amp-driver.js
Outdated
/** | ||
* Navigate the browser to a URL that will display the given url in the | ||
* given environment. | ||
* @param {Environment} environment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non null !Environment.
*/ | ||
async serveMode(mode) { | ||
await this.controller_.navigateTo( | ||
`http://localhost:8000/serve_mode=${mode}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am assuming host, port and protocol here will eventually be configurable and not hardcoded here and everywhere else, as the demo server allows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be eventually, but for this PR I'd say it's out of scope. I'll add a TODO
build-system/tasks/e2e/amp-driver.js
Outdated
SHADOW_DEMO: 'shadow-demo', | ||
}; | ||
|
||
const AmpEnvironments = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick. Perhaps just Environments
as it sort of is implied that it's related to amp? and Environment constant should be EnvironmentType
?
Thanks for the review. I'm going to split this into smaller PRs |
Resolves #20575
This is a work in progress PR for integrating the various environments with end-to-end testing. This also introduces a new fixture format. It currently only runs the
test-carousel.js
tests using the new format, and breaks the others for now.Feel free to leave some feedback