mirror of
https://github.com/tradingview/lightweight-charts.git
synced 2024-11-25 16:50:59 +08:00
added interactions e2e tests
Added new e2e tests for chart interactions such as mouse and touch interactions.
This commit is contained in:
parent
3992003e0a
commit
05ea046da1
@ -220,6 +220,19 @@ jobs:
|
||||
- store_test_results:
|
||||
path: test-results/
|
||||
|
||||
interactions:
|
||||
executor: node16-browsers-executor
|
||||
environment:
|
||||
NO_SANDBOX: "true"
|
||||
TESTS_REPORT_FILE: "test-results/interactions/results.xml"
|
||||
steps:
|
||||
- checkout-with-deps
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run: scripts/run-interactions-tests.sh
|
||||
- store_test_results:
|
||||
path: test-results/
|
||||
|
||||
size-limit:
|
||||
executor: node16-executor
|
||||
steps:
|
||||
@ -336,6 +349,10 @@ workflows:
|
||||
filters: *default-filters
|
||||
requires:
|
||||
- build
|
||||
- interactions:
|
||||
filters: *default-filters
|
||||
requires:
|
||||
- build
|
||||
- lint-dts:
|
||||
filters: *default-filters
|
||||
requires:
|
||||
|
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@ -41,6 +41,16 @@
|
||||
"${input:testStandalonePath}"
|
||||
],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Interaction tests",
|
||||
"program": "${workspaceFolder}/tests/e2e/interactions/runner.js",
|
||||
"args": [
|
||||
"${input:testStandalonePath}"
|
||||
],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
8
scripts/run-interactions-test.sh
Normal file
8
scripts/run-interactions-test.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "Preparing"
|
||||
|
||||
npm run build
|
||||
|
||||
echo "Interactions tests"
|
||||
node ./tests/e2e/interactions/runner.js ./dist/lightweight-charts.standalone.development.js
|
@ -107,3 +107,21 @@ Alternatively, you can run the test on a specific file like this:
|
||||
```bash
|
||||
node ./tests/e2e/memleaks/runner.js ./dist/lightweight-charts.standalone.development.js
|
||||
```
|
||||
|
||||
### Interactions
|
||||
|
||||
The interactions tests check whether the library is correctly handling user interaction on the chart. Interactions include: mouse scrolling, mouse dragging, and touches.
|
||||
|
||||
#### Running the Interactions tests
|
||||
|
||||
You can run the interactions tests with the following command:
|
||||
|
||||
```bash
|
||||
./scripts/run-interactions-tests.sh
|
||||
```
|
||||
|
||||
Alternatively, you can run the tests on a specific file like this:
|
||||
|
||||
```bash
|
||||
node ./tests/e2e/interactions/runner.js ./dist/lightweight-charts.standalone.development.js
|
||||
```
|
||||
|
@ -13,9 +13,12 @@ import puppeteer, {
|
||||
HTTPResponse,
|
||||
launch as launchPuppeteer,
|
||||
Page,
|
||||
type CDPSession,
|
||||
} from 'puppeteer';
|
||||
|
||||
import { doHorizontalDrag, doKineticAnimation, doVerticalDrag } from '../helpers/mouse-drag-actions';
|
||||
import { doMouseScrolls } from '../helpers/mouse-scroll-actions';
|
||||
import { doLongTouch, doPinchZoomTouch, doSwipeTouch } from '../helpers/touch-actions';
|
||||
|
||||
import { expectedCoverage, threshold } from './coverage-config';
|
||||
|
||||
const coverageScript = fs.readFileSync(path.join(__dirname, 'coverage-script.js'), { encoding: 'utf-8' });
|
||||
@ -24,31 +27,6 @@ const testStandalonePathEnvKey = 'TEST_STANDALONE_PATH';
|
||||
|
||||
const testStandalonePath: string = process.env[testStandalonePathEnvKey] || '';
|
||||
|
||||
async function doMouseScrolls(page: Page, element: ElementHandle): Promise<void> {
|
||||
const boundingBox = await element.boundingBox();
|
||||
if (!boundingBox) {
|
||||
throw new Error('Unable to get boundingBox for element.');
|
||||
}
|
||||
|
||||
// move mouse to center of element
|
||||
await page.mouse.move(
|
||||
boundingBox.x + boundingBox.width / 2,
|
||||
boundingBox.y + boundingBox.height / 2
|
||||
);
|
||||
|
||||
await page.mouse.wheel({ deltaX: 10.0 });
|
||||
|
||||
await page.mouse.wheel({ deltaY: 10.0 });
|
||||
|
||||
await page.mouse.wheel({ deltaX: -10.0 });
|
||||
|
||||
await page.mouse.wheel({ deltaY: -10.0 });
|
||||
|
||||
await page.mouse.wheel({ deltaX: 10.0, deltaY: 10.0 });
|
||||
|
||||
await page.mouse.wheel({ deltaX: -10.0, deltaY: -10.0 });
|
||||
}
|
||||
|
||||
async function doZoomInZoomOut(page: Page): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const prevViewport = page.viewport()!;
|
||||
@ -60,157 +38,6 @@ async function doZoomInZoomOut(page: Page): Promise<void> {
|
||||
await page.setViewport(prevViewport);
|
||||
}
|
||||
|
||||
async function doVerticalDrag(page: Page, element: ElementHandle): Promise<void> {
|
||||
const elBox = await element.boundingBox() as BoundingBox;
|
||||
|
||||
const elMiddleX = elBox.x + elBox.width / 2;
|
||||
const elMiddleY = elBox.y + elBox.height / 2;
|
||||
|
||||
// move mouse to the middle of element
|
||||
await page.mouse.move(elMiddleX, elMiddleY);
|
||||
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await page.mouse.move(elMiddleX, elMiddleY - 20);
|
||||
await page.mouse.move(elMiddleX, elMiddleY + 40);
|
||||
await page.mouse.up({ button: 'left' });
|
||||
}
|
||||
|
||||
async function doHorizontalDrag(page: Page, element: ElementHandle): Promise<void> {
|
||||
const elBox = await element.boundingBox() as BoundingBox;
|
||||
|
||||
const elMiddleX = elBox.x + elBox.width / 2;
|
||||
const elMiddleY = elBox.y + elBox.height / 2;
|
||||
|
||||
// move mouse to the middle of element
|
||||
await page.mouse.move(elMiddleX, elMiddleY);
|
||||
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await page.mouse.move(elMiddleX - 20, elMiddleY);
|
||||
await page.mouse.move(elMiddleX + 40, elMiddleY);
|
||||
await page.mouse.up({ button: 'left' });
|
||||
}
|
||||
|
||||
// await a setTimeout delay evaluated within page context
|
||||
async function pageTimeout(page: Page, delay: number): Promise<void> {
|
||||
return page.evaluate(
|
||||
(ms: number) => new Promise<void>(
|
||||
(resolve: () => void) => setTimeout(resolve, ms)
|
||||
),
|
||||
delay
|
||||
);
|
||||
}
|
||||
|
||||
async function doKineticAnimation(page: Page, element: ElementHandle): Promise<void> {
|
||||
const elBox = await element.boundingBox() as BoundingBox;
|
||||
|
||||
const elMiddleX = elBox.x + elBox.width / 2;
|
||||
const elMiddleY = elBox.y + elBox.height / 2;
|
||||
|
||||
// move mouse to the middle of element
|
||||
await page.mouse.move(elMiddleX, elMiddleY);
|
||||
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await pageTimeout(page, 50);
|
||||
await page.mouse.move(elMiddleX - 40, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 55, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 105, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 155, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 205, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 255, elMiddleY);
|
||||
await page.mouse.up({ button: 'left' });
|
||||
|
||||
await pageTimeout(page, 200);
|
||||
// stop animation
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await page.mouse.up({ button: 'left' });
|
||||
}
|
||||
|
||||
// Simulate a long touch action in a single position
|
||||
async function doLongTouch(page: Page, element: ElementHandle, duration: number): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const elCenterX = elBox.x + elBox.width / 2;
|
||||
const elCenterY = elBox.y + elBox.height / 2;
|
||||
|
||||
const client = await page.target().createCDPSession();
|
||||
|
||||
await client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints: [
|
||||
{ x: elCenterX, y: elCenterY },
|
||||
],
|
||||
});
|
||||
await pageTimeout(page, duration);
|
||||
return client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [
|
||||
{ x: elCenterX, y: elCenterY },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate a touch swipe gesture
|
||||
async function doSwipeTouch(
|
||||
devToolsSession: CDPSession,
|
||||
element: ElementHandle,
|
||||
{
|
||||
horizontal = false,
|
||||
vertical = false,
|
||||
}: { horizontal?: boolean; vertical?: boolean }
|
||||
): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const elCenterX = elBox.x + elBox.width / 2;
|
||||
const elCenterY = elBox.y + elBox.height / 2;
|
||||
const xStep = horizontal ? elBox.width / 8 : 0;
|
||||
const yStep = vertical ? elBox.height / 8 : 0;
|
||||
|
||||
for (let i = 2; i > 0; i--) {
|
||||
const type = i === 2 ? 'touchStart' : 'touchMove';
|
||||
await devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type,
|
||||
touchPoints: [{ x: elCenterX - i * xStep, y: elCenterY - i * yStep }],
|
||||
});
|
||||
}
|
||||
return devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [{ x: elCenterX - xStep, y: elCenterY - yStep }],
|
||||
});
|
||||
}
|
||||
|
||||
// Perform a pinch or zoom touch gesture within the specified element.
|
||||
async function doPinchZoomTouch(
|
||||
devToolsSession: CDPSession,
|
||||
element: ElementHandle,
|
||||
zoom?: boolean
|
||||
): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const sign = zoom ? -1 : 1;
|
||||
const elCenterX = elBox.x + elBox.width / 2;
|
||||
const elCenterY = elBox.y + elBox.height / 2;
|
||||
const xStep = (sign * elBox.width) / 8;
|
||||
const yStep = (sign * elBox.height) / 8;
|
||||
|
||||
for (let i = 2; i > 0; i--) {
|
||||
const type = i === 2 ? 'touchStart' : 'touchMove';
|
||||
await devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type,
|
||||
touchPoints: [
|
||||
{ x: elCenterX - i * xStep, y: elCenterY - i * yStep },
|
||||
{ x: elCenterX + i * xStep, y: elCenterY + i * xStep },
|
||||
],
|
||||
});
|
||||
}
|
||||
return devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [
|
||||
{ x: elCenterX - xStep, y: elCenterY - yStep },
|
||||
{ x: elCenterX + xStep, y: elCenterY + xStep },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function doUserInteractions(page: Page): Promise<void> {
|
||||
const chartContainer = await page.$('#container') as ElementHandle<Element>;
|
||||
const chartBox = await chartContainer.boundingBox() as BoundingBox;
|
||||
|
67
tests/e2e/helpers/mouse-drag-actions.ts
Normal file
67
tests/e2e/helpers/mouse-drag-actions.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { BoundingBox, ElementHandle, Page } from 'puppeteer';
|
||||
|
||||
import { pageTimeout } from './page-timeout';
|
||||
|
||||
export async function doVerticalDrag(
|
||||
page: Page,
|
||||
element: ElementHandle
|
||||
): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const elMiddleX = elBox.x + elBox.width / 2;
|
||||
const elMiddleY = elBox.y + elBox.height / 2;
|
||||
|
||||
// move mouse to the middle of element
|
||||
await page.mouse.move(elMiddleX, elMiddleY);
|
||||
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await page.mouse.move(elMiddleX, elMiddleY - 20);
|
||||
await page.mouse.move(elMiddleX, elMiddleY + 40);
|
||||
await page.mouse.up({ button: 'left' });
|
||||
}
|
||||
|
||||
export async function doHorizontalDrag(
|
||||
page: Page,
|
||||
element: ElementHandle
|
||||
): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const elMiddleX = elBox.x + elBox.width / 2;
|
||||
const elMiddleY = elBox.y + elBox.height / 2;
|
||||
|
||||
// move mouse to the middle of element
|
||||
await page.mouse.move(elMiddleX, elMiddleY);
|
||||
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await page.mouse.move(elMiddleX - 20, elMiddleY);
|
||||
await page.mouse.move(elMiddleX + 40, elMiddleY);
|
||||
await page.mouse.up({ button: 'left' });
|
||||
}
|
||||
|
||||
export async function doKineticAnimation(
|
||||
page: Page,
|
||||
element: ElementHandle
|
||||
): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const elMiddleX = elBox.x + elBox.width / 2;
|
||||
const elMiddleY = elBox.y + elBox.height / 2;
|
||||
|
||||
// move mouse to the middle of element
|
||||
await page.mouse.move(elMiddleX, elMiddleY);
|
||||
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await pageTimeout(page, 50);
|
||||
await page.mouse.move(elMiddleX - 40, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 55, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 105, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 155, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 205, elMiddleY);
|
||||
await page.mouse.move(elMiddleX - 255, elMiddleY);
|
||||
await page.mouse.up({ button: 'left' });
|
||||
|
||||
await pageTimeout(page, 200);
|
||||
// stop animation
|
||||
await page.mouse.down({ button: 'left' });
|
||||
await page.mouse.up({ button: 'left' });
|
||||
}
|
48
tests/e2e/helpers/mouse-scroll-actions.ts
Normal file
48
tests/e2e/helpers/mouse-scroll-actions.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { ElementHandle, Page } from 'puppeteer';
|
||||
|
||||
export async function centerMouseOnElement(
|
||||
page: Page,
|
||||
element: ElementHandle
|
||||
): Promise<void> {
|
||||
const boundingBox = await element.boundingBox();
|
||||
if (!boundingBox) {
|
||||
throw new Error('Unable to get boundingBox for element.');
|
||||
}
|
||||
|
||||
// move mouse to center of element
|
||||
await page.mouse.move(
|
||||
boundingBox.x + boundingBox.width / 2,
|
||||
boundingBox.y + boundingBox.height / 2
|
||||
);
|
||||
}
|
||||
|
||||
interface MouseScrollDelta {
|
||||
x?: number;
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export async function doMouseScroll(
|
||||
deltas: MouseScrollDelta,
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
await page.mouse.wheel({ deltaX: deltas.x || 0, deltaY: deltas.y || 0 });
|
||||
}
|
||||
|
||||
export async function doMouseScrolls(
|
||||
page: Page,
|
||||
element: ElementHandle
|
||||
): Promise<void> {
|
||||
await centerMouseOnElement(page, element);
|
||||
|
||||
await doMouseScroll({ x: 10.0 }, page);
|
||||
|
||||
await doMouseScroll({ y: 10.0 }, page);
|
||||
|
||||
await doMouseScroll({ x: -10.0 }, page);
|
||||
|
||||
await doMouseScroll({ y: -10.0 }, page);
|
||||
|
||||
await doMouseScroll({ x: 10.0, y: 10.0 }, page);
|
||||
|
||||
await doMouseScroll({ x: -10.0, y: -10.0 }, page);
|
||||
}
|
11
tests/e2e/helpers/page-timeout.ts
Normal file
11
tests/e2e/helpers/page-timeout.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Page } from 'puppeteer';
|
||||
|
||||
// await a setTimeout delay evaluated within page context
|
||||
export async function pageTimeout(page: Page, delay: number): Promise<void> {
|
||||
return page.evaluate(
|
||||
(ms: number) => new Promise<void>(
|
||||
(resolve: () => void) => setTimeout(resolve, ms)
|
||||
),
|
||||
delay
|
||||
);
|
||||
}
|
89
tests/e2e/helpers/touch-actions.ts
Normal file
89
tests/e2e/helpers/touch-actions.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { BoundingBox, ElementHandle, Page, type CDPSession } from 'puppeteer';
|
||||
|
||||
import { pageTimeout } from './page-timeout';
|
||||
|
||||
// Simulate a long touch action in a single position
|
||||
export async function doLongTouch(page: Page, element: ElementHandle, duration: number): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const elCenterX = elBox.x + elBox.width / 2;
|
||||
const elCenterY = elBox.y + elBox.height / 2;
|
||||
|
||||
const client = await page.target().createCDPSession();
|
||||
|
||||
await client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints: [
|
||||
{ x: elCenterX, y: elCenterY },
|
||||
],
|
||||
});
|
||||
await pageTimeout(page, duration);
|
||||
return client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [
|
||||
{ x: elCenterX, y: elCenterY },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate a touch swipe gesture
|
||||
export async function doSwipeTouch(
|
||||
devToolsSession: CDPSession,
|
||||
element: ElementHandle,
|
||||
{
|
||||
horizontal = false,
|
||||
vertical = false,
|
||||
}: { horizontal?: boolean; vertical?: boolean }
|
||||
): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const elCenterX = elBox.x + elBox.width / 2;
|
||||
const elCenterY = elBox.y + elBox.height / 2;
|
||||
const xStep = horizontal ? elBox.width / 8 : 0;
|
||||
const yStep = vertical ? elBox.height / 8 : 0;
|
||||
|
||||
for (let i = 2; i > 0; i--) {
|
||||
const type = i === 2 ? 'touchStart' : 'touchMove';
|
||||
await devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type,
|
||||
touchPoints: [{ x: elCenterX - i * xStep, y: elCenterY - i * yStep }],
|
||||
});
|
||||
}
|
||||
return devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [{ x: elCenterX - xStep, y: elCenterY - yStep }],
|
||||
});
|
||||
}
|
||||
|
||||
// Perform a pinch or zoom touch gesture within the specified element.
|
||||
export async function doPinchZoomTouch(
|
||||
devToolsSession: CDPSession,
|
||||
element: ElementHandle,
|
||||
zoom?: boolean
|
||||
): Promise<void> {
|
||||
const elBox = (await element.boundingBox()) as BoundingBox;
|
||||
|
||||
const sign = zoom ? -1 : 1;
|
||||
const elCenterX = elBox.x + elBox.width / 2;
|
||||
const elCenterY = elBox.y + elBox.height / 2;
|
||||
const xStep = (sign * elBox.width) / 8;
|
||||
const yStep = (sign * elBox.height) / 8;
|
||||
|
||||
for (let i = 2; i > 0; i--) {
|
||||
const type = i === 2 ? 'touchStart' : 'touchMove';
|
||||
await devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type,
|
||||
touchPoints: [
|
||||
{ x: elCenterX - i * xStep, y: elCenterY - i * yStep },
|
||||
{ x: elCenterX + i * xStep, y: elCenterY + i * xStep },
|
||||
],
|
||||
});
|
||||
}
|
||||
return devToolsSession.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [
|
||||
{ x: elCenterX - xStep, y: elCenterY - yStep },
|
||||
{ x: elCenterX + xStep, y: elCenterY + xStep },
|
||||
],
|
||||
});
|
||||
}
|
60
tests/e2e/interactions/helpers/get-test-cases.ts
Normal file
60
tests/e2e/interactions/helpers/get-test-cases.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface TestCase {
|
||||
name: string;
|
||||
caseContent: string;
|
||||
}
|
||||
|
||||
const testCasesDir = path.join(__dirname, '..', 'test-cases');
|
||||
|
||||
function extractTestCaseName(fileName: string): string | null {
|
||||
const match = /^([^.].+)\.js$/.exec(path.basename(fileName));
|
||||
return match && match[1];
|
||||
}
|
||||
|
||||
function isTestCaseFile(filePath: string): boolean {
|
||||
return fs.lstatSync(filePath).isFile() && extractTestCaseName(filePath) !== null;
|
||||
}
|
||||
|
||||
interface TestCasesGroupInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function getTestCaseGroups(): TestCasesGroupInfo[] {
|
||||
return [
|
||||
{
|
||||
name: '',
|
||||
path: testCasesDir,
|
||||
},
|
||||
...fs.readdirSync(testCasesDir)
|
||||
.filter((filePath: string) => fs.lstatSync(path.join(testCasesDir, filePath)).isDirectory())
|
||||
.map((filePath: string) => {
|
||||
return {
|
||||
name: filePath,
|
||||
path: path.join(testCasesDir, filePath),
|
||||
};
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function getTestCases(): Record<string, TestCase[]> {
|
||||
const result: Record<string, TestCase[]> = {};
|
||||
|
||||
for (const group of getTestCaseGroups()) {
|
||||
result[group.name] = fs.readdirSync(group.path)
|
||||
.map((filePath: string) => path.join(group.path, filePath))
|
||||
.filter(isTestCaseFile)
|
||||
.map((testCaseFile: string) => {
|
||||
return {
|
||||
name: extractTestCaseName(testCaseFile) as string,
|
||||
caseContent: fs.readFileSync(testCaseFile, { encoding: 'utf-8' }),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
23
tests/e2e/interactions/helpers/test-page-dummy.html
Normal file
23
tests/e2e/interactions/helpers/test-page-dummy.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
|
||||
<title>Test case page</title>
|
||||
</head>
|
||||
|
||||
<body style="padding: 0; margin: 0;">
|
||||
<div id="container" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
|
||||
<script type="text/javascript" src="PATH_TO_STANDALONE_MODULE"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
TEST_CASE_SCRIPT
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.interactions = interactionsToPerform();
|
||||
window.finishedSetup = beforeInteractions(document.getElementById('container'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
171
tests/e2e/interactions/interactions-test-cases.ts
Normal file
171
tests/e2e/interactions/interactions-test-cases.ts
Normal file
@ -0,0 +1,171 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import puppeteer, {
|
||||
Browser,
|
||||
HTTPResponse,
|
||||
launch as launchPuppeteer,
|
||||
} from 'puppeteer';
|
||||
|
||||
import { doMouseScroll } from '../helpers/mouse-scroll-actions';
|
||||
|
||||
import { getTestCases, TestCase } from './helpers/get-test-cases';
|
||||
|
||||
const dummyContent = fs.readFileSync(
|
||||
path.join(__dirname, 'helpers', 'test-page-dummy.html'),
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
|
||||
function generatePageContent(
|
||||
standaloneBundlePath: string,
|
||||
testCaseCode: string
|
||||
): string {
|
||||
return dummyContent
|
||||
.replace('PATH_TO_STANDALONE_MODULE', standaloneBundlePath)
|
||||
.replace('TEST_CASE_SCRIPT', testCaseCode);
|
||||
}
|
||||
|
||||
const testStandalonePathEnvKey = 'TEST_STANDALONE_PATH';
|
||||
|
||||
const testStandalonePath: string = process.env[testStandalonePathEnvKey] || '';
|
||||
|
||||
type Interaction = 'scrollLeft' | 'scrollRight' | 'scrollUp' | 'scrollDown';
|
||||
|
||||
interface InternalWindow {
|
||||
interactions: Interaction[];
|
||||
finishedSetup: Promise<() => void>;
|
||||
afterInteractions: () => void;
|
||||
}
|
||||
|
||||
describe('Interactions tests', function(): void {
|
||||
// this tests are unstable sometimes.
|
||||
this.retries(5);
|
||||
|
||||
const puppeteerOptions: Parameters<typeof launchPuppeteer>[0] = {};
|
||||
if (process.env.NO_SANDBOX) {
|
||||
puppeteerOptions.args = ['--no-sandbox', '--disable-setuid-sandbox'];
|
||||
}
|
||||
|
||||
let browser: Browser;
|
||||
|
||||
before(async () => {
|
||||
expect(
|
||||
testStandalonePath,
|
||||
`path to test standalone module must be passed via ${testStandalonePathEnvKey} env var`
|
||||
).to.have.length.greaterThan(0);
|
||||
|
||||
// note that we cannot use launchPuppeteer here as soon it wrong typing in puppeteer
|
||||
// see https://github.com/puppeteer/puppeteer/issues/7529
|
||||
const browserPromise = puppeteer.launch(puppeteerOptions);
|
||||
browser = await browserPromise;
|
||||
});
|
||||
|
||||
let testCaseCount = 0;
|
||||
|
||||
const runTestCase = (testCase: TestCase) => {
|
||||
testCaseCount += 1;
|
||||
it(testCase.name, async () => {
|
||||
const pageContent = generatePageContent(
|
||||
testStandalonePath,
|
||||
testCase.caseContent
|
||||
);
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 600, height: 600 });
|
||||
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', (error: Error) => {
|
||||
errors.push(error.message);
|
||||
});
|
||||
|
||||
page.on('response', (response: HTTPResponse) => {
|
||||
if (!response.ok()) {
|
||||
errors.push(
|
||||
`Network error: ${response.url()} status=${response.status()}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await page.setContent(pageContent, { waitUntil: 'load' });
|
||||
|
||||
await page.evaluate(() => {
|
||||
return (window as unknown as InternalWindow).finishedSetup;
|
||||
});
|
||||
|
||||
const interactionsToPerform = await page.evaluate(() => {
|
||||
return (window as unknown as InternalWindow).interactions;
|
||||
});
|
||||
|
||||
for (const interactionName of interactionsToPerform) {
|
||||
switch (interactionName) {
|
||||
case 'scrollLeft':
|
||||
await doMouseScroll({ x: -10.0 }, page);
|
||||
break;
|
||||
case 'scrollRight':
|
||||
await doMouseScroll({ x: 10.0 }, page);
|
||||
break;
|
||||
case 'scrollDown':
|
||||
await doMouseScroll({ y: 10.0 }, page);
|
||||
break;
|
||||
case 'scrollUp':
|
||||
await doMouseScroll({ y: -10.0 }, page);
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const exhaustiveCheck: never = interactionName;
|
||||
throw new Error(exhaustiveCheck);
|
||||
}
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
return new Promise<void>((resolve: () => void) => {
|
||||
(window as unknown as InternalWindow).afterInteractions();
|
||||
window.requestAnimationFrame(() => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (errors.length !== 0) {
|
||||
throw new Error(`Page has errors:\n${errors.join('\n')}`);
|
||||
}
|
||||
|
||||
expect(errors.length).to.be.equal(
|
||||
0,
|
||||
'There should not be any errors thrown within the test page.'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const testCaseGroups = getTestCases();
|
||||
|
||||
for (const groupName of Object.keys(testCaseGroups)) {
|
||||
if (groupName.length === 0) {
|
||||
for (const testCase of testCaseGroups[groupName]) {
|
||||
runTestCase(testCase);
|
||||
}
|
||||
} else {
|
||||
describe(groupName, () => {
|
||||
for (const testCase of testCaseGroups[groupName]) {
|
||||
runTestCase(testCase);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
it('number of test cases', () => {
|
||||
// we need to have at least 1 test to check it
|
||||
expect(testCaseCount).to.be.greaterThan(
|
||||
0,
|
||||
'there should be at least 1 test case'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
});
|
71
tests/e2e/interactions/runner.js
Normal file
71
tests/e2e/interactions/runner.js
Normal file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const Mocha = require('mocha');
|
||||
|
||||
const serveLocalFiles = require('../serve-local-files').serveLocalFiles;
|
||||
|
||||
const mochaConfig = require('../../../.mocharc.js');
|
||||
|
||||
// override tsconfig
|
||||
process.env.TS_NODE_PROJECT = path.resolve(__dirname, '../tsconfig.composite.json');
|
||||
|
||||
mochaConfig.require.forEach(module => {
|
||||
require(module);
|
||||
});
|
||||
|
||||
if (process.argv.length !== 3) {
|
||||
console.log('Usage: runner PATH_TO_TEST_STANDALONE_MODULE');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
let testStandalonePath = process.argv[2];
|
||||
|
||||
const hostname = 'localhost';
|
||||
const port = 34567;
|
||||
const httpServerPrefix = `http://${hostname}:${port}/`;
|
||||
|
||||
const filesToServe = new Map();
|
||||
|
||||
if (fs.existsSync(testStandalonePath)) {
|
||||
const fileNameToServe = 'test.js';
|
||||
filesToServe.set(fileNameToServe, path.resolve(testStandalonePath));
|
||||
testStandalonePath = `${httpServerPrefix}${fileNameToServe}`;
|
||||
}
|
||||
|
||||
process.env.TEST_STANDALONE_PATH = testStandalonePath;
|
||||
|
||||
function runMocha(closeServer) {
|
||||
console.log('Running tests...');
|
||||
const mocha = new Mocha({
|
||||
timeout: 20000,
|
||||
slow: 10000,
|
||||
reporter: mochaConfig.reporter,
|
||||
reporterOptions: mochaConfig._reporterOptions,
|
||||
});
|
||||
|
||||
if (mochaConfig.checkLeaks) {
|
||||
mocha.checkLeaks();
|
||||
}
|
||||
|
||||
mocha.diff(mochaConfig.diff);
|
||||
mocha.addFile(path.resolve(__dirname, './interactions-test-cases.ts'));
|
||||
|
||||
mocha.run(failures => {
|
||||
if (closeServer !== null) {
|
||||
closeServer();
|
||||
}
|
||||
|
||||
const timeInSecs = (Date.now() - startTime) / 1000;
|
||||
console.log(`Done in ${timeInSecs.toFixed(2)}s with ${failures} error(s)`);
|
||||
|
||||
process.exitCode = failures !== 0 ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
serveLocalFiles(filesToServe, port, hostname)
|
||||
.then(runMocha);
|
14
tests/e2e/interactions/test-cases/.eslintrc.js
Normal file
14
tests/e2e/interactions/test-cases/.eslintrc.js
Normal file
@ -0,0 +1,14 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: false,
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^(beforeInteractions|afterInteractions|interactionsToPerform)$', args: 'none' }],
|
||||
},
|
||||
globals: {
|
||||
LightweightCharts: false,
|
||||
},
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
function generateData() {
|
||||
const res = [];
|
||||
const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
|
||||
for (let i = 0; i < 500; ++i) {
|
||||
res.push({
|
||||
time: time.getTime() / 1000,
|
||||
value: i,
|
||||
});
|
||||
|
||||
time.setUTCDate(time.getUTCDate() + 1);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function interactionsToPerform() {
|
||||
return ['scrollLeft', 'scrollDown'];
|
||||
}
|
||||
|
||||
let chart;
|
||||
let startRange;
|
||||
|
||||
function beforeInteractions(container) {
|
||||
chart = LightweightCharts.createChart(container);
|
||||
|
||||
const mainSeries = chart.addLineSeries();
|
||||
|
||||
mainSeries.setData(generateData());
|
||||
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
startRange = chart.timeScale().getVisibleLogicalRange();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function afterInteractions() {
|
||||
const endRange = chart.timeScale().getVisibleLogicalRange();
|
||||
|
||||
const pass = Boolean(startRange.from !== endRange.from && startRange.to !== endRange.to);
|
||||
|
||||
if (!pass) {
|
||||
throw new Error('Expected visible logical range to have changed.');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
function generateData() {
|
||||
const res = [];
|
||||
const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
|
||||
for (let i = 0; i < 500; ++i) {
|
||||
res.push({
|
||||
time: time.getTime() / 1000,
|
||||
value: i,
|
||||
});
|
||||
|
||||
time.setUTCDate(time.getUTCDate() + 1);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function interactionsToPerform() {
|
||||
return ['scrollLeft', 'scrollDown'];
|
||||
}
|
||||
|
||||
let chart;
|
||||
let startRange;
|
||||
|
||||
function beforeInteractions(container) {
|
||||
chart = LightweightCharts.createChart(container, {
|
||||
handleScroll: {
|
||||
mouseWheel: false,
|
||||
},
|
||||
handleScale: {
|
||||
mouseWheel: false,
|
||||
},
|
||||
});
|
||||
|
||||
const mainSeries = chart.addLineSeries();
|
||||
|
||||
mainSeries.setData(generateData());
|
||||
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
startRange = chart.timeScale().getVisibleLogicalRange();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function afterInteractions() {
|
||||
const endRange = chart.timeScale().getVisibleLogicalRange();
|
||||
|
||||
const pass = Boolean(startRange.from === endRange.from && startRange.to === endRange.to);
|
||||
|
||||
if (!pass) {
|
||||
throw new Error('Expected visible logical range to be unchanged.');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
function generateData() {
|
||||
const res = [];
|
||||
const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
|
||||
for (let i = 0; i < 500; ++i) {
|
||||
res.push({
|
||||
time: time.getTime() / 1000,
|
||||
value: i,
|
||||
});
|
||||
|
||||
time.setUTCDate(time.getUTCDate() + 1);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function interactionsToPerform() {
|
||||
return ['scrollLeft', 'scrollDown'];
|
||||
}
|
||||
|
||||
let chart;
|
||||
let startRange;
|
||||
|
||||
function beforeInteractions(container) {
|
||||
chart = LightweightCharts.createChart(container, {
|
||||
handleScroll: {
|
||||
mouseWheel: true,
|
||||
},
|
||||
handleScale: {
|
||||
mouseWheel: true,
|
||||
},
|
||||
});
|
||||
|
||||
const mainSeries = chart.addLineSeries();
|
||||
|
||||
mainSeries.setData(generateData());
|
||||
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
startRange = chart.timeScale().getVisibleLogicalRange();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function afterInteractions() {
|
||||
const endRange = chart.timeScale().getVisibleLogicalRange();
|
||||
|
||||
const pass = Boolean(startRange.from !== endRange.from && startRange.to !== endRange.to);
|
||||
|
||||
if (!pass) {
|
||||
throw new Error('Expected visible logical range to have changed.');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
function generateData() {
|
||||
const res = [];
|
||||
const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
|
||||
for (let i = 0; i < 500; ++i) {
|
||||
res.push({
|
||||
time: time.getTime() / 1000,
|
||||
value: i,
|
||||
});
|
||||
|
||||
time.setUTCDate(time.getUTCDate() + 1);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function interactionsToPerform() {
|
||||
return ['scrollLeft', 'scrollDown'];
|
||||
}
|
||||
|
||||
let chart;
|
||||
let startRange;
|
||||
|
||||
function beforeInteractions(container) {
|
||||
chart = LightweightCharts.createChart(container, {
|
||||
handleScroll: {
|
||||
mouseWheel: false,
|
||||
},
|
||||
handleScale: {
|
||||
mouseWheel: false,
|
||||
},
|
||||
});
|
||||
|
||||
const mainSeries = chart.addLineSeries();
|
||||
|
||||
mainSeries.setData(generateData());
|
||||
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
chart.applyOptions({
|
||||
handleScroll: {
|
||||
mouseWheel: true,
|
||||
},
|
||||
handleScale: {
|
||||
mouseWheel: true,
|
||||
},
|
||||
});
|
||||
startRange = chart.timeScale().getVisibleLogicalRange();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function afterInteractions() {
|
||||
const endRange = chart.timeScale().getVisibleLogicalRange();
|
||||
|
||||
const pass = Boolean(startRange.from !== endRange.from && startRange.to !== endRange.to);
|
||||
|
||||
if (!pass) {
|
||||
throw new Error('Expected visible logical range to have changed.');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
@ -13,6 +13,8 @@
|
||||
"./coverage/**/*.ts",
|
||||
"./graphics/graphics-test-cases.ts",
|
||||
"./graphics/helpers/**/*.ts",
|
||||
"./memleaks/**/*.ts"
|
||||
"./memleaks/**/*.ts",
|
||||
"./interactions/**/*.ts",
|
||||
"./helpers/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user