Skip to content

virtual-scroll: add e2e tests for autosize scroll strategy #11345

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

Merged
merged 4 commits into from
May 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
/e2e/components/stepper-e2e.spec.ts @mmalerba
/e2e/components/tabs-e2e.spec.ts @andrewseguin
/e2e/components/toolbar-e2e.spec.ts @devversion
/e2e/components/virtual-scroll-e2e.spec.ts @mmalerba
/e2e/util/** @jelbourn
/src/e2e-app/* @jelbourn
/src/e2e-app/block-scroll-strategy/** @andrewseguin @crisbeto
Expand All @@ -185,6 +186,7 @@
/src/e2e-app/sidenav/** @mmalerba
/src/e2e-app/slide-toggle/** @devversion
/src/e2e-app/tabs/** @andrewseguin
/src/e2e-app/virtual-scroll/** @mmalerba

# Universal app
/src/universal-app/** @jelbourn
Expand Down
117 changes: 117 additions & 0 deletions e2e/components/virtual-scroll-e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {browser, by, element, ElementFinder} from 'protractor';
import {ILocation, ISize} from 'selenium-webdriver';

declare var window: any;


describe('autosize cdk-virtual-scroll', () => {
let viewport: ElementFinder;

describe('with uniform items', () => {
beforeEach(() => {
browser.get('/virtual-scroll');
viewport = element(by.css('.demo-virtual-scroll-uniform-size cdk-virtual-scroll-viewport'));
});

it('should scroll down slowly', async () => {
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 2000);
const offScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="39"]'));
const onScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="40"]'));
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);
});

it('should jump scroll position down and slowly scroll back up', async () => {
// The estimate of the total content size is exactly correct, so we wind up scrolled to the
// same place as if we slowly scrolled down.
await browser.executeAsyncScript(scrollViewportTo, viewport, 2000);
const offScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="39"]'));
const onScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="40"]'));
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);

// As we slowly scroll back up we should wind up back at the start of the content.
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 0);
const first = element(by.css('.demo-virtual-scroll-uniform-size [data-index="0"]'));
expect(await isVisibleInViewport(first, viewport)).toBe(true);
});
});

describe('with variable size', () => {
beforeEach(() => {
browser.get('/virtual-scroll');
viewport = element(by.css('.demo-virtual-scroll-variable-size cdk-virtual-scroll-viewport'));
});

it('should scroll down slowly', async () => {
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 2000);
const offScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="19"]'));
const onScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="20"]'));
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);
});

it('should jump scroll position down and slowly scroll back up', async () => {
// The estimate of the total content size is slightly different than the actual, so we don't
// wind up in the same spot as if we scrolled slowly down.
await browser.executeAsyncScript(scrollViewportTo, viewport, 2000);
const offScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="18"]'));
const onScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="19"]'));
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);

// As we slowly scroll back up we should wind up back at the start of the content. As we
// scroll the error from when we jumped the scroll position should be slowly corrected.
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 0);
const first = element(by.css('.demo-virtual-scroll-variable-size [data-index="0"]'));
expect(await isVisibleInViewport(first, viewport)).toBe(true);
});
});
});


/** Checks if the given element is visible in the given viewport. */
async function isVisibleInViewport(el: ElementFinder, viewport: ElementFinder): Promise<boolean> {
Copy link
Member

Choose a reason for hiding this comment

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

nit: consider naming this something along the lines of isCompletelyInViewport? isVisibileInViewport could apply to something that's partially in the viewport.

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 does apply to things that are partially in the viewport, that is intentional.

if (!await el.isPresent() || !await el.isDisplayed() || !await viewport.isPresent() ||
!await viewport.isDisplayed()) {
return false;
}
const viewportRect = getRect(await viewport.getLocation(), await viewport.getSize());
const elRect = getRect(await el.getLocation(), await el.getSize());
return elRect.left < viewportRect.right && elRect.right > viewportRect.left &&
elRect.top < viewportRect.bottom && elRect.bottom > viewportRect.top;
}


/** Gets the rect for an element given its location ans size. */
function getRect(location: ILocation, size: ISize):
{top: number, left: number, bottom: number, right: number} {
return {
top: location.y,
left: location.x,
bottom: location.y + size.height,
right: location.x + size.width
};
}


/** Immediately scrolls the viewport to the given offset. */
function scrollViewportTo(viewportEl: any, offset: number, done: () => void) {
viewportEl.scrollTop = offset;
window.requestAnimationFrame(() => done());
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure whether this wouldn't introduce flakes in the CI. Most browsers will pause or throttle the requestAnimationFrame calls if the browser somehow ends up being in the background.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting, I need to pass some time since the scroll events are sampled every animation frame. If this turns out to be flaky I can switch to browser.sleep(16). Let's just see what happens with this first though

}


/** Smoothly scrolls the viewport to the given offset, 25px at a time. */
function smoothScrollViewportTo(viewportEl: any, offset: number, done: () => void) {
let promise = Promise.resolve();
let curOffset = viewportEl.scrollTop;
do {
const co = curOffset += Math.min(25, Math.max(-25, offset - curOffset));
promise = promise.then(() => new Promise<void>(resolve => {
viewportEl.scrollTop = co;
window.requestAnimationFrame(() => resolve());
}));
} while (curOffset != offset);
promise.then(() => done());
}
4 changes: 2 additions & 2 deletions src/cdk-experimental/scrolling/virtual-scroll-viewport.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ cdk-virtual-scroll-viewport {
will-change: contents, transform;
}

.cdk-virtual-scroll-orientation-horizontal {
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
bottom: 0;
}

.cdk-virtual-scroll-orientation-vertical {
.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
right: 0;
}

Expand Down
50 changes: 27 additions & 23 deletions src/e2e-app/e2e-app-module.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import {ScrollingModule} from '@angular/cdk-experimental';
import {FullscreenOverlayContainer, OverlayContainer} from '@angular/cdk/overlay';
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {RouterModule} from '@angular/router';
import {SimpleCheckboxes} from './checkbox/checkbox-e2e';
import {E2EApp, Home} from './e2e-app/e2e-app';
import {IconE2E} from './icon/icon-e2e';
import {ButtonE2E} from './button/button-e2e';
import {MenuE2E} from './menu/menu-e2e';
import {SimpleRadioButtons} from './radio/radio-e2e';
import {BasicTabs} from './tabs/tabs-e2e';
import {DialogE2E, TestDialog} from './dialog/dialog-e2e';
import {GridListE2E} from './grid-list/grid-list-e2e';
import {ProgressBarE2E} from './progress-bar/progress-bar-e2e';
import {ProgressSpinnerE2E} from './progress-spinner/progress-spinner-e2e';
import {FullscreenE2E, TestDialogFullScreen} from './fullscreen/fullscreen-e2e';
import {E2E_APP_ROUTES} from './e2e-app/routes';
import {SlideToggleE2E} from './slide-toggle/slide-toggle-e2e';
import {InputE2E} from './input/input-e2e';
import {SidenavE2E} from './sidenav/sidenav-e2e';
import {BlockScrollStrategyE2E} from './block-scroll-strategy/block-scroll-strategy-e2e';
import {ReactiveFormsModule} from '@angular/forms';
import {
MatButtonModule,
MatCheckboxModule,
Expand All @@ -39,9 +22,28 @@ import {
MatStepperModule,
MatTabsModule,
} from '@angular/material';
import {FullscreenOverlayContainer, OverlayContainer} from '@angular/cdk/overlay';
import {ExampleModule} from '@angular/material-examples';
import {ReactiveFormsModule} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {RouterModule} from '@angular/router';
import {BlockScrollStrategyE2E} from './block-scroll-strategy/block-scroll-strategy-e2e';
import {ButtonE2E} from './button/button-e2e';
import {SimpleCheckboxes} from './checkbox/checkbox-e2e';
import {DialogE2E, TestDialog} from './dialog/dialog-e2e';
import {E2EApp, Home} from './e2e-app/e2e-app';
import {E2E_APP_ROUTES} from './e2e-app/routes';
import {FullscreenE2E, TestDialogFullScreen} from './fullscreen/fullscreen-e2e';
import {GridListE2E} from './grid-list/grid-list-e2e';
import {IconE2E} from './icon/icon-e2e';
import {InputE2E} from './input/input-e2e';
import {MenuE2E} from './menu/menu-e2e';
import {ProgressBarE2E} from './progress-bar/progress-bar-e2e';
import {ProgressSpinnerE2E} from './progress-spinner/progress-spinner-e2e';
import {SimpleRadioButtons} from './radio/radio-e2e';
import {SidenavE2E} from './sidenav/sidenav-e2e';
import {SlideToggleE2E} from './slide-toggle/slide-toggle-e2e';
import {BasicTabs} from './tabs/tabs-e2e';
import {VirtualScrollE2E} from './virtual-scroll/virtual-scroll-e2e';

/**
* NgModule that contains all Material modules that are required to serve the e2e-app.
Expand All @@ -66,6 +68,7 @@ import {ReactiveFormsModule} from '@angular/forms';
MatStepperModule,
MatTabsModule,
MatNativeDateModule,
ScrollingModule,
]
})
export class E2eMaterialModule {}
Expand Down Expand Up @@ -98,7 +101,8 @@ export class E2eMaterialModule {}
SlideToggleE2E,
TestDialog,
TestDialogFullScreen,
BlockScrollStrategyE2E
BlockScrollStrategyE2E,
VirtualScrollE2E,
],
bootstrap: [E2EApp],
providers: [
Expand Down
1 change: 1 addition & 0 deletions src/e2e-app/e2e-app/e2e-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<a mat-list-item [routerLink]="['tabs']">Tabs</a>
<a mat-list-item [routerLink]="['cards']">Cards</a>
<a mat-list-item [routerLink]="['toolbar']">Toolbar</a>
<a mat-list-item [routerLink]="['virtual-scroll']">Virtual Scroll</a>
</mat-nav-list>

<main>
Expand Down
2 changes: 2 additions & 0 deletions src/e2e-app/e2e-app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Routes} from '@angular/router';
import {VirtualScrollE2E} from '../virtual-scroll/virtual-scroll-e2e';
import {Home} from './e2e-app';
import {ButtonE2E} from '../button/button-e2e';
import {BasicTabs} from '../tabs/tabs-e2e';
Expand Down Expand Up @@ -47,4 +48,5 @@ export const E2E_APP_ROUTES: Routes = [
{path: 'tabs', component: BasicTabs},
{path: 'cards', component: CardFancyExample},
{path: 'toolbar', component: ToolbarMultirowExample},
{path: 'virtual-scroll', component: VirtualScrollE2E},
];
4 changes: 4 additions & 0 deletions src/e2e-app/tsconfig-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"@angular/cdk/*": ["./cdk/*"],
"@angular/material": ["./material"],
"@angular/material/*": ["./material/*"],
"@angular/material-experimental/*": ["./material-experimental/*"],
"@angular/material-experimental": ["./material-experimental/"],
"@angular/cdk-experimental/*": ["./cdk-experimental/*"],
"@angular/cdk-experimental": ["./cdk-experimental/"],
"@angular/material-moment-adapter": ["./material-moment-adapter"],
"@angular/material-examples": ["./material-examples"]
}
Expand Down
13 changes: 13 additions & 0 deletions src/e2e-app/virtual-scroll/virtual-scroll-e2e.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.demo-viewport {
height: 300px;
width: 300px;
box-shadow: 0 0 0 1px black;
}

.demo-item {
background: magenta;
}

.demo-odd {
background: cyan;
}
19 changes: 19 additions & 0 deletions src/e2e-app/virtual-scroll/virtual-scroll-e2e.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<section class="demo-virtual-scroll-uniform-size">
<h3>Uniform size</h3>
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
<div *cdkVirtualFor="let size of uniformItems; let i = index; let odd = odd" class="demo-item"
[style.height.px]="size" [class.demo-odd]="odd" [attr.data-index]="i">
Uniform Item #{{i}} - ({{size}}px)
</div>
</cdk-virtual-scroll-viewport>
</section>

<section class="demo-virtual-scroll-variable-size">
<h3>Random size</h3>
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
<div *cdkVirtualFor="let size of variableItems; let i = index; let odd = odd" class="demo-item"
[style.height.px]="size" [class.demo-odd]="odd" [attr.data-index]="i">
Variable Item #{{i}} - ({{size}}px)
</div>
</cdk-virtual-scroll-viewport>
</section>
16 changes: 16 additions & 0 deletions src/e2e-app/virtual-scroll/virtual-scroll-e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Component} from '@angular/core';


const itemSizeSample = [100, 25, 50, 50, 100, 200, 75, 100, 50, 250];


@Component({
moduleId: module.id,
selector: 'virtual-scroll-e2e',
templateUrl: 'virtual-scroll-e2e.html',
styleUrls: ['virtual-scroll-e2e.css'],
})
export class VirtualScrollE2E {
uniformItems = Array(1000).fill(50);
variableItems = Array(100).fill(0).reduce(acc => acc.concat(itemSizeSample), []);
}
35 changes: 31 additions & 4 deletions tools/gulp/tasks/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {task} from 'gulp';
import {join} from 'path';
import {ngcBuildTask, copyTask, execNodeTask, serverTask} from '../util/task_helpers';
import {copySync} from 'fs-extra';
import {buildConfig, sequenceTask, watchFiles} from 'material2-build-tools';
import {buildConfig, sequenceTask, triggerLivereload, watchFiles} from 'material2-build-tools';

// There are no type definitions available for these imports.
const gulpConnect = require('gulp-connect');
Expand All @@ -13,6 +13,7 @@ const {outputDir, packagesDir, projectDir} = buildConfig;
const releasesDir = join(outputDir, 'releases');

const appDir = join(packagesDir, 'e2e-app');
const e2eTestDir = join(projectDir, 'e2e');
const outDir = join(outputDir, 'packages', 'e2e-app');

const PROTRACTOR_CONFIG_PATH = join(projectDir, 'test/protractor.conf.js');
Expand All @@ -21,16 +22,42 @@ const tsconfigPath = join(outDir, 'tsconfig-build.json');
/** Glob that matches all files that need to be copied to the output folder. */
const assetsGlob = join(appDir, '**/*.+(html|css|json|ts)');

/**
* Builds and serves the e2e-app and runs protractor once the e2e-app is ready.
*/
/** Builds and serves the e2e-app and runs protractor once the e2e-app is ready. */
task('e2e', sequenceTask(
[':test:protractor:setup', 'serve:e2eapp'],
':test:protractor',
':serve:e2eapp:stop',
'screenshots',
));

/**
* Builds and serves the e2e-app and runs protractor when the app is ready. Re-runs protractor when
* the app or tests change.
*/
task('e2e:watch', sequenceTask(
[':test:protractor:setup', 'serve:e2eapp'],
[':test:protractor', 'material:watch', ':e2e:watch'],
));

/** Watches the e2e app and tests for changes and triggers a test rerun on change. */
task(':e2e:watch', () => {
watchFiles([join(appDir, '**/*.+(html|ts|css)'), join(e2eTestDir, '**/*.+(html|ts)')],
[':e2e:rerun'], false);
});

/** Updates the e2e app and runs the protractor tests. */
task(':e2e:rerun', sequenceTask(
'e2e-app:copy-assets',
'e2e-app:build-ts',
':e2e:reload',
':test:protractor'
));

/** Triggers a reload of the e2e app. */
task(':e2e:reload', () => {
return triggerLivereload();
});

/** Task that builds the e2e-app in AOT mode. */
task('e2e-app:build', sequenceTask(
'clean',
Expand Down
Loading