chore(testrunner): typescript test files (#2751)

This lets our spec files be .ts instead of just .js. The typescript files will be checked against the public types. Tests are compiled with babel just in time before running them, emulating the jest experience. TypeScript tests are also linted with eslint.

I converted keyboard.spec.js as the first demo. I'll follow up converting some more more tests.
This commit is contained in:
Joel Einbinder 2020-07-08 00:20:36 -07:00 committed by GitHub
parent e97badcca8
commit 8611ee8df7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1521 additions and 81 deletions

View file

@ -2,7 +2,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'notice'],
parserOptions: {
project: './tsconfig.json',
project: ['./tsconfig.json', './test/tsconfig.json'],
ecmaVersion: 9,
sourceType: 'module',
},

1392
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,8 +35,9 @@
"prepare": "node install-from-github.js",
"build": "node utils/runWebpack.js --mode='development' && tsc -p . && npm run generate-types",
"watch": "node utils/runWebpack.js --mode='development' --watch --silent | tsc -w -p .",
"test-types": "npm run generate-types && npx -p typescript@3.7.5 tsc -p utils/generate_types/test/tsconfig.json",
"generate-types": "node utils/generate_types/"
"test-types": "npm run generate-types && npx -p typescript@3.7.5 tsc -p utils/generate_types/test/tsconfig.json && npm run typecheck-tests",
"generate-types": "node utils/generate_types/",
"typecheck-tests": "tsc -p ./test/"
},
"author": {
"name": "Microsoft Corporation"
@ -56,6 +57,9 @@
"ws": "^6.1.0"
},
"devDependencies": {
"@babel/core": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"@babel/preset-typescript": "^7.10.1",
"@types/debug": "0.0.31",
"@types/extract-zip": "^1.6.2",
"@types/mime": "^2.0.1",
@ -77,6 +81,7 @@
"formidable": "^1.2.1",
"ncp": "^2.0.0",
"node-stream-zip": "^1.8.2",
"pirates": "^4.0.1",
"pixelmatch": "^4.0.2",
"socksv5": "0.0.6",
"text-diff": "^1.0.1",

View file

@ -14,10 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import utils from './utils';
import type { Page } from '..';
const utils = require('./utils');
const {FFOX, CHROMIUM, WEBKIT, MAC, LINUX} = require('./utils').testOptions(browserType);
const {FFOX, WEBKIT, MAC} = utils.testOptions(browserType);
describe('Keyboard', function() {
it('should type into a textarea', async ({page, server}) => {
await page.evaluate(() => {
@ -68,15 +68,16 @@ describe('Keyboard', function() {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
page.on('console', m => console.log(m.text()));
await page.evaluate(() => {
window.events = [];
const events = await page.evaluateHandle(() => {
const events: string[] = [];
document.addEventListener('keydown', e => events.push(e.type));
document.addEventListener('keyup', e => events.push(e.type));
document.addEventListener('keypress', e => events.push(e.type));
document.addEventListener('input', e => events.push(e.type));
return events;
});
await page.keyboard.insertText('hello world');
expect(await page.evaluate('window.events')).toEqual(['input']);
expect(await events.jsonValue()).toEqual(['input']);
});
it.fail(FFOX && MAC)('should report shiftKey', async ({page, server}) => {
await page.goto(server.PREFIX + '/input/keyboard.html');
@ -153,7 +154,7 @@ describe('Keyboard', function() {
}, false);
});
await page.keyboard.type('Hello World!');
expect(await page.evaluate(() => textarea.value)).toBe('He Wrd!');
expect(await page.$eval('textarea', textarea => textarea.value)).toBe('He Wrd!');
});
it('should press plus', async ({page, server}) => {
await page.goto(server.PREFIX + '/input/keyboard.html');
@ -207,51 +208,49 @@ describe('Keyboard', function() {
it('should specify repeat property', async ({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
await page.evaluate(() => document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true));
const lastEvent = await captureLastKeydown(page);
await page.keyboard.down('a');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false);
expect(await lastEvent.evaluate(e => e.repeat)).toBe(false);
await page.keyboard.press('a');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true);
expect(await lastEvent.evaluate(e => e.repeat)).toBe(true);
await page.keyboard.down('b');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false);
expect(await lastEvent.evaluate(e => e.repeat)).toBe(false);
await page.keyboard.down('b');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true);
expect(await lastEvent.evaluate(e => e.repeat)).toBe(true);
await page.keyboard.up('a');
await page.keyboard.down('a');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false);
expect(await lastEvent.evaluate(e => e.repeat)).toBe(false);
});
it('should type all kinds of characters', async ({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
const text = 'This text goes onto two lines.\nThis character is 嗨.';
await page.keyboard.type(text);
expect(await page.evaluate('result')).toBe(text);
expect(await page.$eval('textarea', t => t.value)).toBe(text);
});
it('should specify location', async ({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.evaluate(() => {
window.addEventListener('keydown', event => window.keyLocation = event.location, true);
});
const lastEvent = await captureLastKeydown(page);
const textarea = await page.$('textarea');
await textarea.press('Digit5');
expect(await page.evaluate('keyLocation')).toBe(0);
expect(await lastEvent.evaluate(e => e.location)).toBe(0);
await textarea.press('ControlLeft');
expect(await page.evaluate('keyLocation')).toBe(1);
expect(await lastEvent.evaluate(e => e.location)).toBe(1);
await textarea.press('ControlRight');
expect(await page.evaluate('keyLocation')).toBe(2);
expect(await lastEvent.evaluate(e => e.location)).toBe(2);
await textarea.press('NumpadSubtract');
expect(await page.evaluate('keyLocation')).toBe(3);
expect(await lastEvent.evaluate(e => e.location)).toBe(3);
});
it('should press Enter', async ({page, server}) => {
await page.setContent('<textarea></textarea>');
await page.focus('textarea');
await page.evaluate(() => window.addEventListener('keydown', e => window.lastEvent = {key: e.key, code:e.code}));
const lastEventHandle = await captureLastKeydown(page);
await testEnterKey('Enter', 'Enter', 'Enter');
await testEnterKey('NumpadEnter', 'Enter', 'NumpadEnter');
await testEnterKey('\n', 'Enter', 'Enter');
@ -259,7 +258,7 @@ describe('Keyboard', function() {
async function testEnterKey(key, expectedKey, expectedCode) {
await page.keyboard.press(key);
const lastEvent = await page.evaluate('lastEvent');
const lastEvent = await lastEventHandle.jsonValue();
expect(lastEvent.key).toBe(expectedKey, `${JSON.stringify(key)} had the wrong key: ${lastEvent.key}`);
expect(lastEvent.code).toBe(expectedCode, `${JSON.stringify(key)} had the wrong code: ${lastEvent.code}`);
const value = await page.$eval('textarea', t => t.value);
@ -319,14 +318,9 @@ describe('Keyboard', function() {
expect(await page.$eval('textarea', textarea => textarea.value)).toBe('some tex');
});
it('should press the meta key', async ({page}) => {
await page.evaluate(() => {
window.result = null;
document.addEventListener('keydown', event => {
window.result = [event.key, event.code, event.metaKey];
});
});
const lastEvent = await captureLastKeydown(page);
await page.keyboard.press('Meta');
const [key, code, metaKey] = await page.evaluate('result');
const {key, code, metaKey} = await lastEvent.jsonValue();
if (FFOX && !MAC)
expect(key).toBe('OS');
else
@ -346,20 +340,14 @@ describe('Keyboard', function() {
it('should work after a cross origin navigation', async ({page, server}) => {
await page.goto(server.PREFIX + '/empty.html');
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
await page.evaluate(() => {
document.addEventListener('keydown', event => window.lastKey = event);
})
const lastEvent = await captureLastKeydown(page);
await page.keyboard.press('a');
expect(await page.evaluate('lastKey.key')).toBe('a');
expect(await lastEvent.evaluate(l => l.key)).toBe('a');
});
// event.keyIdentifier has been removed from all browsers except WebKit
it.skip(!WEBKIT)('should expose keyIdentifier in webkit', async ({page, server}) => {
await page.evaluate(() => {
document.addEventListener('keydown', event => {
window.lastKeyIdentifier = event.keyIdentifier
});
});
const lastEvent = await captureLastKeydown(page);
const keyMap = {
'ArrowUp': 'Up',
'ArrowDown': 'Down',
@ -374,7 +362,7 @@ describe('Keyboard', function() {
};
for (const [key, keyIdentifier] of Object.entries(keyMap)) {
await page.keyboard.press(key);
expect(await page.evaluate('lastKeyIdentifier')).toBe(keyIdentifier);
expect(await lastEvent.evaluate(e => e.keyIdentifier)).toBe(keyIdentifier);
}
});
it('should scroll with PageDown', async ({page, server}) => {
@ -386,3 +374,27 @@ describe('Keyboard', function() {
await page.waitForFunction(() => scrollY > 0);
});
});
async function captureLastKeydown(page: Page) {
const lastEvent = await page.evaluateHandle(() => {
const lastEvent = {
repeat: false,
location: -1,
code: '',
key: '',
metaKey: false,
keyIdentifier: 'unsupported'
};
document.addEventListener('keydown', e => {
lastEvent.repeat = e.repeat;
lastEvent.location = e.location;
lastEvent.key = e.key;
lastEvent.code = e.code;
lastEvent.metaKey = e.metaKey;
// keyIdentifier only exists in WebKit, and isn't in TypeScript's lib.
lastEvent.keyIdentifier = 'keyIdentifier' in e && (e as any).keyIdentifier;
}, true);
return lastEvent;
});
return lastEvent;
}

View file

@ -446,7 +446,7 @@ describe('Page.goto', function() {
});
/**
* @param {import('../src/frames').Frame} frame
* @param {import('../index').Frame} frame
* @param {TestServer} server
* @param {() => Promise<void>} action
* @param {boolean} isSetContent

View file

@ -85,7 +85,7 @@ module.exports = {
'./focus.spec.js',
'./input.spec.js',
'./jshandle.spec.js',
'./keyboard.spec.js',
'./keyboard.spec.ts',
'./mouse.spec.js',
'./navigation.spec.js',
'./network.spec.js',

View file

@ -17,6 +17,9 @@
const fs = require('fs');
const utils = require('./utils');
const path = require('path');
const pirates = require('pirates');
const babel = require('@babel/core');
const TestRunner = require('../utils/testrunner/');
const { PlaywrightEnvironment, BrowserTypeEnvironment, BrowserEnvironment, PageEnvironment} = require('./environments.js');
@ -132,7 +135,18 @@ function collect(browserNames) {
}
}
for (const file of spec.files || []) {
const revert = pirates.addHook((code, filename) => {
const result = babel.transformFileSync(filename, {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript']
});
return result.code;
}, {
exts: ['.ts']
});
require(file);
revert();
delete require.cache[require.resolve(file)];
}
});

View file

@ -6,6 +6,7 @@
"moduleResolution": "node",
"target": "ESNext",
"strictNullChecks": false,
"allowSyntheticDefaultImports": true,
},
"include": ["*.spec.js", "types.d.ts"]
"include": ["*.spec.js", "types.d.ts", "*.spec.ts"]
}

30
test/types.d.ts vendored
View file

@ -1,7 +1,7 @@
type ServerResponse = import('http').ServerResponse;
type IncomingMessage = import('http').IncomingMessage;
type Falsy = false|""|0|null|undefined;
type Falsy = false|''|0|null|undefined;
interface Expect<T> {
toBe(other: T, message?: string): void;
toBeFalsy(message?: string): void;
@ -24,7 +24,12 @@ interface Expect<T> {
type DescribeFunction = ((name: string, inner: () => void) => void) & {fail(condition: boolean): DescribeFunction};
type ItFunction<STATE> = ((name: string, inner: (state: STATE) => Promise<void>) => void) & {fail(condition: boolean): ItFunction<STATE>; repeat(n: number): ItFunction<STATE>};
type ItFunction<STATE> = ((name: string, inner: (state: STATE) => Promise<void>) => void) & {
fail(condition: boolean): ItFunction<STATE>;
skip(condition: boolean): ItFunction<STATE>;
slow(): ItFunction<STATE>;
repeat(n: number): ItFunction<STATE>;
};
type TestRunner<STATE> = {
describe: DescribeFunction;
@ -50,9 +55,9 @@ interface TestSetup<STATE> {
WIN: boolean;
playwright: typeof import('../index');
browserType: import('../index').BrowserType<import('../index').Browser>;
selectors: import('../src/selectors').Selectors;
selectors: import('../index').Selectors;
expect<T>(value: T): Expect<T>;
defaultBrowserOptions: import('../src/server/browserType').LaunchOptions;
defaultBrowserOptions: import('../index').LaunchOptions;
playwrightPath;
headless: boolean;
ASSETS_DIR: string;
@ -100,3 +105,20 @@ interface TestServer {
EMPTY_PAGE: string;
}
declare const describe: DescribeFunction;
declare const fdescribe: DescribeFunction;
declare const xdescribe: DescribeFunction;
declare function expect<T>(value: T): Expect<T>;
declare const it: ItFunction<PageState>;
declare const fit: ItFunction<PageState>;
declare const dit: ItFunction<PageState>;
declare const xit: ItFunction<PageState>;
declare const browserType: import('../index').BrowserType<import('../index').Browser>;
// global variables in assets
// keyboard.html
declare function getResult(): string;