Skip to content

Commit 9497bad

Browse files
authored
virtual-scroll: add e2e tests for autosize scroll strategy (#11345)
* set up virtual scroll page in e2e app * add gulp task for e2e:watch * add e2e tests for autosize * address comments
1 parent 2253b6d commit 9497bad

File tree

12 files changed

+255
-37
lines changed

12 files changed

+255
-37
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
/e2e/components/stepper-e2e.spec.ts @mmalerba
168168
/e2e/components/tabs-e2e.spec.ts @andrewseguin
169169
/e2e/components/toolbar-e2e.spec.ts @devversion
170+
/e2e/components/virtual-scroll-e2e.spec.ts @mmalerba
170171
/e2e/util/** @jelbourn
171172
/src/e2e-app/* @jelbourn
172173
/src/e2e-app/block-scroll-strategy/** @andrewseguin @crisbeto
@@ -185,6 +186,7 @@
185186
/src/e2e-app/sidenav/** @mmalerba
186187
/src/e2e-app/slide-toggle/** @devversion
187188
/src/e2e-app/tabs/** @andrewseguin
189+
/src/e2e-app/virtual-scroll/** @mmalerba
188190

189191
# Universal app
190192
/src/universal-app/** @jelbourn
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {browser, by, element, ElementFinder} from 'protractor';
2+
import {ILocation, ISize} from 'selenium-webdriver';
3+
4+
declare var window: any;
5+
6+
7+
describe('autosize cdk-virtual-scroll', () => {
8+
let viewport: ElementFinder;
9+
10+
describe('with uniform items', () => {
11+
beforeEach(() => {
12+
browser.get('/virtual-scroll');
13+
viewport = element(by.css('.demo-virtual-scroll-uniform-size cdk-virtual-scroll-viewport'));
14+
});
15+
16+
it('should scroll down slowly', async () => {
17+
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 2000);
18+
const offScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="39"]'));
19+
const onScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="40"]'));
20+
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
21+
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);
22+
});
23+
24+
it('should jump scroll position down and slowly scroll back up', async () => {
25+
// The estimate of the total content size is exactly correct, so we wind up scrolled to the
26+
// same place as if we slowly scrolled down.
27+
await browser.executeAsyncScript(scrollViewportTo, viewport, 2000);
28+
const offScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="39"]'));
29+
const onScreen = element(by.css('.demo-virtual-scroll-uniform-size [data-index="40"]'));
30+
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
31+
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);
32+
33+
// As we slowly scroll back up we should wind up back at the start of the content.
34+
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 0);
35+
const first = element(by.css('.demo-virtual-scroll-uniform-size [data-index="0"]'));
36+
expect(await isVisibleInViewport(first, viewport)).toBe(true);
37+
});
38+
});
39+
40+
describe('with variable size', () => {
41+
beforeEach(() => {
42+
browser.get('/virtual-scroll');
43+
viewport = element(by.css('.demo-virtual-scroll-variable-size cdk-virtual-scroll-viewport'));
44+
});
45+
46+
it('should scroll down slowly', async () => {
47+
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 2000);
48+
const offScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="19"]'));
49+
const onScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="20"]'));
50+
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
51+
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);
52+
});
53+
54+
it('should jump scroll position down and slowly scroll back up', async () => {
55+
// The estimate of the total content size is slightly different than the actual, so we don't
56+
// wind up in the same spot as if we scrolled slowly down.
57+
await browser.executeAsyncScript(scrollViewportTo, viewport, 2000);
58+
const offScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="18"]'));
59+
const onScreen = element(by.css('.demo-virtual-scroll-variable-size [data-index="19"]'));
60+
expect(await isVisibleInViewport(offScreen, viewport)).toBe(false);
61+
expect(await isVisibleInViewport(onScreen, viewport)).toBe(true);
62+
63+
// As we slowly scroll back up we should wind up back at the start of the content. As we
64+
// scroll the error from when we jumped the scroll position should be slowly corrected.
65+
await browser.executeAsyncScript(smoothScrollViewportTo, viewport, 0);
66+
const first = element(by.css('.demo-virtual-scroll-variable-size [data-index="0"]'));
67+
expect(await isVisibleInViewport(first, viewport)).toBe(true);
68+
});
69+
});
70+
});
71+
72+
73+
/** Checks if the given element is visible in the given viewport. */
74+
async function isVisibleInViewport(el: ElementFinder, viewport: ElementFinder): Promise<boolean> {
75+
if (!await el.isPresent() || !await el.isDisplayed() || !await viewport.isPresent() ||
76+
!await viewport.isDisplayed()) {
77+
return false;
78+
}
79+
const viewportRect = getRect(await viewport.getLocation(), await viewport.getSize());
80+
const elRect = getRect(await el.getLocation(), await el.getSize());
81+
return elRect.left < viewportRect.right && elRect.right > viewportRect.left &&
82+
elRect.top < viewportRect.bottom && elRect.bottom > viewportRect.top;
83+
}
84+
85+
86+
/** Gets the rect for an element given its location ans size. */
87+
function getRect(location: ILocation, size: ISize):
88+
{top: number, left: number, bottom: number, right: number} {
89+
return {
90+
top: location.y,
91+
left: location.x,
92+
bottom: location.y + size.height,
93+
right: location.x + size.width
94+
};
95+
}
96+
97+
98+
/** Immediately scrolls the viewport to the given offset. */
99+
function scrollViewportTo(viewportEl: any, offset: number, done: () => void) {
100+
viewportEl.scrollTop = offset;
101+
window.requestAnimationFrame(() => done());
102+
}
103+
104+
105+
/** Smoothly scrolls the viewport to the given offset, 25px at a time. */
106+
function smoothScrollViewportTo(viewportEl: any, offset: number, done: () => void) {
107+
let promise = Promise.resolve();
108+
let curOffset = viewportEl.scrollTop;
109+
do {
110+
const co = curOffset += Math.min(25, Math.max(-25, offset - curOffset));
111+
promise = promise.then(() => new Promise<void>(resolve => {
112+
viewportEl.scrollTop = co;
113+
window.requestAnimationFrame(() => resolve());
114+
}));
115+
} while (curOffset != offset);
116+
promise.then(() => done());
117+
}

src/cdk-experimental/scrolling/virtual-scroll-viewport.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ cdk-virtual-scroll-viewport {
1414
will-change: contents, transform;
1515
}
1616

17-
.cdk-virtual-scroll-orientation-horizontal {
17+
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
1818
bottom: 0;
1919
}
2020

21-
.cdk-virtual-scroll-orientation-vertical {
21+
.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
2222
right: 0;
2323
}
2424

src/e2e-app/e2e-app-module.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,7 @@
1+
import {ScrollingModule} from '@angular/cdk-experimental';
2+
import {FullscreenOverlayContainer, OverlayContainer} from '@angular/cdk/overlay';
13
import {NgModule} from '@angular/core';
2-
import {BrowserModule} from '@angular/platform-browser';
3-
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
4-
import {RouterModule} from '@angular/router';
5-
import {SimpleCheckboxes} from './checkbox/checkbox-e2e';
6-
import {E2EApp, Home} from './e2e-app/e2e-app';
7-
import {IconE2E} from './icon/icon-e2e';
8-
import {ButtonE2E} from './button/button-e2e';
9-
import {MenuE2E} from './menu/menu-e2e';
10-
import {SimpleRadioButtons} from './radio/radio-e2e';
11-
import {BasicTabs} from './tabs/tabs-e2e';
12-
import {DialogE2E, TestDialog} from './dialog/dialog-e2e';
13-
import {GridListE2E} from './grid-list/grid-list-e2e';
14-
import {ProgressBarE2E} from './progress-bar/progress-bar-e2e';
15-
import {ProgressSpinnerE2E} from './progress-spinner/progress-spinner-e2e';
16-
import {FullscreenE2E, TestDialogFullScreen} from './fullscreen/fullscreen-e2e';
17-
import {E2E_APP_ROUTES} from './e2e-app/routes';
18-
import {SlideToggleE2E} from './slide-toggle/slide-toggle-e2e';
19-
import {InputE2E} from './input/input-e2e';
20-
import {SidenavE2E} from './sidenav/sidenav-e2e';
21-
import {BlockScrollStrategyE2E} from './block-scroll-strategy/block-scroll-strategy-e2e';
4+
import {ReactiveFormsModule} from '@angular/forms';
225
import {
236
MatButtonModule,
247
MatCheckboxModule,
@@ -39,9 +22,28 @@ import {
3922
MatStepperModule,
4023
MatTabsModule,
4124
} from '@angular/material';
42-
import {FullscreenOverlayContainer, OverlayContainer} from '@angular/cdk/overlay';
4325
import {ExampleModule} from '@angular/material-examples';
44-
import {ReactiveFormsModule} from '@angular/forms';
26+
import {BrowserModule} from '@angular/platform-browser';
27+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
28+
import {RouterModule} from '@angular/router';
29+
import {BlockScrollStrategyE2E} from './block-scroll-strategy/block-scroll-strategy-e2e';
30+
import {ButtonE2E} from './button/button-e2e';
31+
import {SimpleCheckboxes} from './checkbox/checkbox-e2e';
32+
import {DialogE2E, TestDialog} from './dialog/dialog-e2e';
33+
import {E2EApp, Home} from './e2e-app/e2e-app';
34+
import {E2E_APP_ROUTES} from './e2e-app/routes';
35+
import {FullscreenE2E, TestDialogFullScreen} from './fullscreen/fullscreen-e2e';
36+
import {GridListE2E} from './grid-list/grid-list-e2e';
37+
import {IconE2E} from './icon/icon-e2e';
38+
import {InputE2E} from './input/input-e2e';
39+
import {MenuE2E} from './menu/menu-e2e';
40+
import {ProgressBarE2E} from './progress-bar/progress-bar-e2e';
41+
import {ProgressSpinnerE2E} from './progress-spinner/progress-spinner-e2e';
42+
import {SimpleRadioButtons} from './radio/radio-e2e';
43+
import {SidenavE2E} from './sidenav/sidenav-e2e';
44+
import {SlideToggleE2E} from './slide-toggle/slide-toggle-e2e';
45+
import {BasicTabs} from './tabs/tabs-e2e';
46+
import {VirtualScrollE2E} from './virtual-scroll/virtual-scroll-e2e';
4547

4648
/**
4749
* NgModule that contains all Material modules that are required to serve the e2e-app.
@@ -66,6 +68,7 @@ import {ReactiveFormsModule} from '@angular/forms';
6668
MatStepperModule,
6769
MatTabsModule,
6870
MatNativeDateModule,
71+
ScrollingModule,
6972
]
7073
})
7174
export class E2eMaterialModule {}
@@ -98,7 +101,8 @@ export class E2eMaterialModule {}
98101
SlideToggleE2E,
99102
TestDialog,
100103
TestDialogFullScreen,
101-
BlockScrollStrategyE2E
104+
BlockScrollStrategyE2E,
105+
VirtualScrollE2E,
102106
],
103107
bootstrap: [E2EApp],
104108
providers: [

src/e2e-app/e2e-app/e2e-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<a mat-list-item [routerLink]="['tabs']">Tabs</a>
2323
<a mat-list-item [routerLink]="['cards']">Cards</a>
2424
<a mat-list-item [routerLink]="['toolbar']">Toolbar</a>
25+
<a mat-list-item [routerLink]="['virtual-scroll']">Virtual Scroll</a>
2526
</mat-nav-list>
2627

2728
<main>

src/e2e-app/e2e-app/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Routes} from '@angular/router';
2+
import {VirtualScrollE2E} from '../virtual-scroll/virtual-scroll-e2e';
23
import {Home} from './e2e-app';
34
import {ButtonE2E} from '../button/button-e2e';
45
import {BasicTabs} from '../tabs/tabs-e2e';
@@ -47,4 +48,5 @@ export const E2E_APP_ROUTES: Routes = [
4748
{path: 'tabs', component: BasicTabs},
4849
{path: 'cards', component: CardFancyExample},
4950
{path: 'toolbar', component: ToolbarMultirowExample},
51+
{path: 'virtual-scroll', component: VirtualScrollE2E},
5052
];

src/e2e-app/tsconfig-build.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
"@angular/cdk/*": ["./cdk/*"],
2727
"@angular/material": ["./material"],
2828
"@angular/material/*": ["./material/*"],
29+
"@angular/material-experimental/*": ["./material-experimental/*"],
30+
"@angular/material-experimental": ["./material-experimental/"],
31+
"@angular/cdk-experimental/*": ["./cdk-experimental/*"],
32+
"@angular/cdk-experimental": ["./cdk-experimental/"],
2933
"@angular/material-moment-adapter": ["./material-moment-adapter"],
3034
"@angular/material-examples": ["./material-examples"]
3135
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.demo-viewport {
2+
height: 300px;
3+
width: 300px;
4+
box-shadow: 0 0 0 1px black;
5+
}
6+
7+
.demo-item {
8+
background: magenta;
9+
}
10+
11+
.demo-odd {
12+
background: cyan;
13+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<section class="demo-virtual-scroll-uniform-size">
2+
<h3>Uniform size</h3>
3+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
4+
<div *cdkVirtualFor="let size of uniformItems; let i = index; let odd = odd" class="demo-item"
5+
[style.height.px]="size" [class.demo-odd]="odd" [attr.data-index]="i">
6+
Uniform Item #{{i}} - ({{size}}px)
7+
</div>
8+
</cdk-virtual-scroll-viewport>
9+
</section>
10+
11+
<section class="demo-virtual-scroll-variable-size">
12+
<h3>Random size</h3>
13+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
14+
<div *cdkVirtualFor="let size of variableItems; let i = index; let odd = odd" class="demo-item"
15+
[style.height.px]="size" [class.demo-odd]="odd" [attr.data-index]="i">
16+
Variable Item #{{i}} - ({{size}}px)
17+
</div>
18+
</cdk-virtual-scroll-viewport>
19+
</section>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {Component} from '@angular/core';
2+
3+
4+
const itemSizeSample = [100, 25, 50, 50, 100, 200, 75, 100, 50, 250];
5+
6+
7+
@Component({
8+
moduleId: module.id,
9+
selector: 'virtual-scroll-e2e',
10+
templateUrl: 'virtual-scroll-e2e.html',
11+
styleUrls: ['virtual-scroll-e2e.css'],
12+
})
13+
export class VirtualScrollE2E {
14+
uniformItems = Array(1000).fill(50);
15+
variableItems = Array(100).fill(0).reduce(acc => acc.concat(itemSizeSample), []);
16+
}

tools/gulp/tasks/e2e.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {task} from 'gulp';
22
import {join} from 'path';
33
import {ngcBuildTask, copyTask, execNodeTask, serverTask} from '../util/task_helpers';
44
import {copySync} from 'fs-extra';
5-
import {buildConfig, sequenceTask, watchFiles} from 'material2-build-tools';
5+
import {buildConfig, sequenceTask, triggerLivereload, watchFiles} from 'material2-build-tools';
66

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

1515
const appDir = join(packagesDir, 'e2e-app');
16+
const e2eTestDir = join(projectDir, 'e2e');
1617
const outDir = join(outputDir, 'packages', 'e2e-app');
1718

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

24-
/**
25-
* Builds and serves the e2e-app and runs protractor once the e2e-app is ready.
26-
*/
25+
/** Builds and serves the e2e-app and runs protractor once the e2e-app is ready. */
2726
task('e2e', sequenceTask(
2827
[':test:protractor:setup', 'serve:e2eapp'],
2928
':test:protractor',
3029
':serve:e2eapp:stop',
3130
'screenshots',
3231
));
3332

33+
/**
34+
* Builds and serves the e2e-app and runs protractor when the app is ready. Re-runs protractor when
35+
* the app or tests change.
36+
*/
37+
task('e2e:watch', sequenceTask(
38+
[':test:protractor:setup', 'serve:e2eapp'],
39+
[':test:protractor', 'material:watch', ':e2e:watch'],
40+
));
41+
42+
/** Watches the e2e app and tests for changes and triggers a test rerun on change. */
43+
task(':e2e:watch', () => {
44+
watchFiles([join(appDir, '**/*.+(html|ts|css)'), join(e2eTestDir, '**/*.+(html|ts)')],
45+
[':e2e:rerun'], false);
46+
});
47+
48+
/** Updates the e2e app and runs the protractor tests. */
49+
task(':e2e:rerun', sequenceTask(
50+
'e2e-app:copy-assets',
51+
'e2e-app:build-ts',
52+
':e2e:reload',
53+
':test:protractor'
54+
));
55+
56+
/** Triggers a reload of the e2e app. */
57+
task(':e2e:reload', () => {
58+
return triggerLivereload();
59+
});
60+
3461
/** Task that builds the e2e-app in AOT mode. */
3562
task('e2e-app:build', sequenceTask(
3663
'clean',

0 commit comments

Comments
 (0)