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

✨Integrate E2E Testing with demo viewer and demo PWA shadow doc. #20994

Closed
wants to merge 13 commits into from

Conversation

cvializ
Copy link
Contributor

@cvializ cvializ commented Feb 21, 2019

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

describes.endtoend('AMP carousel', { 
  fixture: 'http://localhost:8000/test/manual/amp-carousel-0-2/basic.amp.html',
  experiments: ['amp-carousel-v2'], 
  environment: 'viewer-demo', // or 'single' or 'shadow-demo'
}, async env => {
  /* ... */
});

node.dataset['iAmphtmlTestId'] = value;
};
const removeData = node => delete node.dataset['iAmphtmlTestId'];
const getIt =

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.

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, 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() => {

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');

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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;

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.

Copy link
Contributor Author

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',

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?

Copy link
Contributor Author

@cvializ cvializ Feb 22, 2019

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

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

Copy link
Contributor

@rsimha rsimha Feb 25, 2019

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 Environments in AmpEnvironments, just like this code goes over the variants for templateType.

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

Copy link
Contributor Author

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

Copy link
Contributor

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

examples/viewer.html Outdated Show resolved Hide resolved
build-system/tasks/e2e/describes-e2e.js Show resolved Hide resolved
build-system/tasks/e2e/describes-e2e.js Show resolved Hide resolved
build-system/tasks/e2e/describes-e2e.js Outdated Show resolved Hide resolved
const webElement = await this.driver.wait(new Condition('', async() => {
try {
const root = await this.getRoot_();
const results = await this.evaluate(queryXpath, xpath, root);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@cvializ cvializ Feb 25, 2019

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 ShadowRoots, 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

examples/viewer.html Outdated Show resolved Hide resolved
@estherkim
Copy link
Collaborator

Newbie question about examples/viewer.html - is the purpose of this page to mimic the viewer in production? If so, and the viewer changes, how do we know to keep it up to date?

@cvializ
Copy link
Contributor Author

cvializ commented Feb 25, 2019

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',

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',

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.

@alabiaga
Copy link
Contributor

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.

build-system/tasks/e2e/describes-e2e.js Outdated Show resolved Hide resolved
build-system/tasks/e2e/driver/query-xpath.js Outdated Show resolved Hide resolved
build-system/tasks/e2e/selenium-webdriver-controller.js Outdated Show resolved Hide resolved

/** @enum {string} */
const Environment = {
SINGLE: 'single',
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 not sure how I feel about this name. We mean single doc yes, SINGLE_DOC?

Copy link
Contributor Author

@cvializ cvializ Mar 1, 2019

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.

describes.realWin('with single AmpDoc', {
amp: {
ampdoc: 'single',
},
}, env => {

Does the precedent make you feel better about it?

Copy link
Contributor

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.

/**
* Navigate the browser to a URL that will display the given url in the
* given environment.
* @param {Environment} environment
Copy link
Contributor

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}`);
Copy link
Contributor

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.

Copy link
Contributor Author

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

SHADOW_DEMO: 'shadow-demo',
};

const AmpEnvironments = {
Copy link
Contributor

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?

@cvializ
Copy link
Contributor Author

cvializ commented Mar 4, 2019

Thanks for the review. I'm going to split this into smaller PRs

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

Successfully merging this pull request may close these issues.

Test e2e in Viewer
7 participants