chore: use composite builds for tests (#8522)

This commit is contained in:
jrandolf 2022-06-15 12:05:25 +02:00 committed by GitHub
parent cba58a12c4
commit 80373f7a12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 480 additions and 687 deletions

View File

@ -1,16 +1,8 @@
test/assets/modernizr.js
third_party/*
utils/doclint/check_public_api/test/
node6/*
node6-test/*
experimental/
assets/
build/
coverage/
doclint/
lib/
/index.d.ts
# We ignore this file because it uses ES imports which we don't yet use
# in the Puppeteer src, so it trips up the ESLint-TypeScript parser.
utils/doclint/generate_types/test/test.ts
vendor/
web-test-runner.config.mjs
test-ts-types/
website/
docs-dist/
tsconfig.tsbuildinfo
vendor/

38
.gitignore vendored
View File

@ -1,26 +1,20 @@
/node_modules/
test-ts-types/**/node_modules
test-ts-types/**/dist/
/test/output-chromium
/test/output-firefox
/test/test-user-data-dir*
.DS_Store
.vscode
/.dev_profile*
/.local-chromium/
/.local-firefox/
/.dev_profile*
.DS_Store
*.swp
*.pyc
.vscode
package-lock.json
yarn.lock
/node6
lib
test/coverage.json
temp/
new-docs/
puppeteer*.tgz
docs-api-json/
docs-dist/
/test/test-user-data-dir*
build/
coverage
website/docs
coverage/
doclint/
docs-api-json/
docs/api.html
lib/
node_modules/
package-lock.json
puppeteer*.tgz
test-ts-types/**/dist/
test-ts-types/**/node_modules
tsconfig.tsbuildinfo
yarn.lock

View File

@ -14,17 +14,12 @@
* limitations under the License.
*/
const base = require('./base.js');
module.exports = {
...base,
require: [
'./test/mocha-ts-require',
'./test/mocha-utils.ts',
'source-map-support/register',
],
spec: 'test/*.spec.ts',
extension: ['js', 'ts'],
reporter: 'dot',
logLevel: 'debug',
require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
spec: 'test/build/*.spec.js',
exit: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
parallel: !!process.env.PARALLEL,
timeout: 25 * 1000,

View File

@ -1,17 +1,17 @@
node_modules/
lib/
third_party/
vendor/
package-lock.json
yarn.lock
package.json
docs-api-json/
docs-dist/
website/
experimental/
.local-chromium/
.local-firefox/
assets/
build/
CHANGELOG.md
coverage/
doclint/
docs-api-json/
lib/
node_modules/
package-lock.json
package.json
test-ts-types/
test/assets/
/.local-chromium/
/.local-firefox/
test-ts-types
tsconfig.tsbuildinfo
vendor/
yarn.lock

View File

@ -1,3 +1,8 @@
{
"extends": "../../src/tsconfig.cjs.json"
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "../../lib/cjs/puppeteer",
"module": "CommonJS"
}
}

View File

@ -1,3 +1,8 @@
{
"extends": "../../src/tsconfig.esm.json"
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "../../lib/esm/puppeteer",
"module": "esnext"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
reporter: 'dot',
// Allow `console.log`s to show up during test execution
logLevel: 'debug',
exit: !!process.env.CI,
};

View File

@ -1,6 +0,0 @@
const base = require('./base.js');
module.exports = {
...base,
spec: 'utils/doclint/**/*.spec.js',
};

View File

@ -27,15 +27,15 @@
"node": ">=14.1.0"
},
"scripts": {
"test": "npm run build && npm run lint --silent && npm run test:unit:coverage",
"test:unit": "npm run build:tsc:cjs && npm run build:tsc:cjs:compat && mocha --config mocha-config/puppeteer-unit-tests.js",
"test": "npm run lint --silent && npm run test:unit:coverage",
"test:unit": "npm run build:test && mocha",
"test:unit:firefox": "cross-env PUPPETEER_PRODUCT=firefox npm run test:unit",
"test:unit:coverage": "c8 --check-coverage --lines 94 npm run test:unit",
"test:unit:chrome-headless": "cross-env HEADLESS=chrome npm run test:unit",
"test:protocol-revision": "ts-node -s scripts/ensure-correct-devtools-protocol-package",
"test:pinned-deps": "ts-node -s scripts/ensure-pinned-deps",
"test:install": "scripts/test-install.sh",
"test:debug": "npm run build:tsc:cjs && npm run build:tsc:cjs:compat && mocha --inspect-brk --config mocha-config/puppeteer-unit-tests.js",
"test:debug": "npm run build:test && mocha --inspect-brk",
"test:types": "ts-node -s scripts/test-ts-definition-files.ts",
"prepublishOnly": "npm run build",
"prepare": "node typescript-if-required.js && ([[ $HUSKY = 0 ]] || husky install)",
@ -54,11 +54,10 @@
"clean:lib": "rimraf lib",
"clean:docs": "rimraf docs-api-json",
"build": "npm run build:tsc && npm run generate:types && npm run generate:esm-package-json",
"build:tsc": "npm run clean:lib && tsc --version && (npm run build:tsc:cjs && npm run build:tsc:esm) && (npm run build:tsc:cjs:compat && npm run build:tsc:esm:compat)",
"build:test": "tsc -b test",
"build:tsc": "npm run clean:lib && tsc --version && (npm run build:tsc:cjs && npm run build:tsc:esm)",
"build:tsc:esm": "tsc -b src/tsconfig.esm.json",
"build:tsc:esm:compat": "tsc -b compat/esm/tsconfig.json",
"build:tsc:cjs": "tsc -b src/tsconfig.cjs.json",
"build:tsc:cjs:compat": "tsc -b compat/cjs/tsconfig.json"
"build:tsc:cjs": "tsc -b src/tsconfig.cjs.json"
},
"files": [
"lib",
@ -104,6 +103,7 @@
"cross-env": "7.0.3",
"eslint": "8.16.0",
"eslint-config-prettier": "8.5.0",
"eslint-formatter-codeframe": "7.32.1",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-mocha": "10.0.5",
"eslint-plugin-prettier": "4.0.0",
@ -122,7 +122,6 @@
"sinon": "14.0.0",
"source-map-support": "0.5.21",
"text-diff": "1.0.1",
"ts-node": "10.8.0",
"typescript": "4.7.2"
}
}

View File

View File

@ -5,5 +5,8 @@
"outDir": "../lib/cjs/puppeteer",
"module": "CommonJS"
},
"references": [{ "path": "../vendor/tsconfig.cjs.json" }]
"references": [
{ "path": "../vendor/tsconfig.cjs.json" },
{ "path": "../compat/cjs/tsconfig.json" }
]
}

View File

@ -5,5 +5,8 @@
"outDir": "../lib/esm/puppeteer",
"module": "esnext"
},
"references": [{ "path": "../vendor/tsconfig.esm.json" }]
"references": [
{ "path": "../vendor/tsconfig.esm.json" },
{ "path": "../compat/esm/tsconfig.json" }
]
}

View File

@ -1,167 +0,0 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO (@jackfranklin): convert this to TypeScript and enable type-checking
// @ts-nocheck
/* We want to ensure that all of Puppeteer's public API is tested via our unit
* tests but we can't use a tool like Istanbul because the way it instruments
* code unfortunately breaks in Puppeteer where some of that code is then being
* executed in a browser context.
*
* So instead we maintain this coverage code which does the following:
* * takes every public method that we expect to be tested
* * replaces it with a method that calls the original but also updates a Map of calls
* * in an after() test callback it asserts that every public method was called.
*
* We run this when COVERAGE=1.
*/
const path = require('path');
const fs = require('fs');
/**
* This object is also used by DocLint to know which classes to check are
* documented. It's a pretty hacky solution but DocLint is going away soon as
* part of the TSDoc migration.
*/
const MODULES_TO_CHECK_FOR_COVERAGE = {
Accessibility: '../lib/cjs/puppeteer/common/Accessibility',
Browser: '../lib/cjs/puppeteer/common/Browser',
BrowserContext: '../lib/cjs/puppeteer/common/Browser',
BrowserFetcher: '../lib/cjs/puppeteer/node/BrowserFetcher',
CDPSession: '../lib/cjs/puppeteer/common/Connection',
ConsoleMessage: '../lib/cjs/puppeteer/common/ConsoleMessage',
Coverage: '../lib/cjs/puppeteer/common/Coverage',
Dialog: '../lib/cjs/puppeteer/common/Dialog',
ElementHandle: '../lib/cjs/puppeteer/common/JSHandle',
ExecutionContext: '../lib/cjs/puppeteer/common/ExecutionContext',
EventEmitter: '../lib/cjs/puppeteer/common/EventEmitter',
FileChooser: '../lib/cjs/puppeteer/common/FileChooser',
Frame: '../lib/cjs/puppeteer/common/FrameManager',
JSHandle: '../lib/cjs/puppeteer/common/JSHandle',
Keyboard: '../lib/cjs/puppeteer/common/Input',
Mouse: '../lib/cjs/puppeteer/common/Input',
Page: '../lib/cjs/puppeteer/common/Page',
Puppeteer: '../lib/cjs/puppeteer/common/Puppeteer',
PuppeteerNode: '../lib/cjs/puppeteer/node/Puppeteer',
HTTPRequest: '../lib/cjs/puppeteer/common/HTTPRequest',
HTTPResponse: '../lib/cjs/puppeteer/common/HTTPResponse',
SecurityDetails: '../lib/cjs/puppeteer/common/SecurityDetails',
Target: '../lib/cjs/puppeteer/common/Target',
TimeoutError: '../lib/cjs/puppeteer/common/Errors',
Touchscreen: '../lib/cjs/puppeteer/common/Input',
Tracing: '../lib/cjs/puppeteer/common/Tracing',
WebWorker: '../lib/cjs/puppeteer/common/WebWorker',
};
function traceAPICoverage(apiCoverage, className, modulePath) {
const loadedModule = require(modulePath);
const classType = loadedModule[className];
if (!classType || !classType.prototype) {
console.error(
`Coverage error: could not find class for ${className}. Is src/api.ts up to date?`
);
process.exit(1);
}
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (
methodName === 'constructor' ||
typeof methodName !== 'string' ||
methodName.startsWith('_') ||
typeof method !== 'function'
) {
continue;
}
apiCoverage.set(`${className}.${methodName}`, false);
Reflect.set(classType.prototype, methodName, function (...args) {
apiCoverage.set(`${className}.${methodName}`, true);
return method.call(this, ...args);
});
}
/**
* If classes emit events, those events are exposed via an object in the same
* module named XEmittedEvents, where X is the name of the class. For example,
* the Page module exposes PageEmittedEvents.
*/
const eventsName = `${className}EmittedEvents`;
if (loadedModule[eventsName]) {
for (const event of Object.values(loadedModule[eventsName])) {
if (typeof event !== 'symbol') {
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
}
}
const method = Reflect.get(classType.prototype, 'emit');
Reflect.set(classType.prototype, 'emit', function (event, ...args) {
if (typeof event !== 'symbol' && this.listenerCount(event)) {
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
}
return method.call(this, event, ...args);
});
}
}
const coverageLocation = path.join(__dirname, 'coverage.json');
const clearOldCoverage = () => {
try {
fs.unlinkSync(coverageLocation);
} catch (error) {
// do nothing, the file didn't exist
}
};
const writeCoverage = (coverage) => {
fs.writeFileSync(coverageLocation, JSON.stringify([...coverage.entries()]));
};
const getCoverageResults = () => {
let contents;
try {
contents = fs.readFileSync(coverageLocation, { encoding: 'utf8' });
} catch (error) {
console.error('Warning: coverage file does not exist or is not readable.');
}
const coverageMap = new Map(JSON.parse(contents));
return coverageMap;
};
const trackCoverage = () => {
clearOldCoverage();
const coverageMap = new Map();
return {
beforeAll: () => {
for (const [className, moduleFilePath] of Object.entries(
MODULES_TO_CHECK_FOR_COVERAGE
)) {
traceAPICoverage(coverageMap, className, moduleFilePath);
}
},
afterAll: () => {
writeCoverage(coverageMap);
},
};
};
module.exports = {
trackCoverage,
getCoverageResults,
MODULES_TO_CHECK_FOR_COVERAGE,
};

View File

@ -1,11 +0,0 @@
const path = require('path');
require('ts-node').register({
/**
* We ignore the lib/ directory because that's already been TypeScript
* compiled and checked. So we don't want to check it again as part of running
* the unit tests.
*/
ignore: ['lib/*', 'node_modules'],
project: path.join(__dirname, 'tsconfig.test.json'),
});

View File

@ -1,33 +0,0 @@
#!/usr/bin/env node
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
const { TestServer } = require('../utils/testserver/index.js');
const port = 8907;
const httpsPort = 8908;
const assetsPath = path.join(__dirname, 'assets');
const cachedPath = path.join(__dirname, 'assets', 'cached');
Promise.all([
TestServer.create(assetsPath, port),
TestServer.createHTTPS(assetsPath, httpsPort),
]).then(([server, httpsServer]) => {
server.enableHTTPCache(cachedPath);
httpsServer.enableHTTPCache(cachedPath);
console.log(`HTTP: server is running on http://localhost:${port}`);
console.log(`HTTPS: server is running on https://localhost:${httpsPort}`);
});

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { EventEmitter } from '../lib/cjs/puppeteer/common/EventEmitter.js';
import { EventEmitter } from '../../lib/cjs/puppeteer/common/EventEmitter.js';
import sinon from 'sinon';
import expect from 'expect';

View File

@ -20,10 +20,10 @@ import expect from 'expect';
import {
NetworkManager,
NetworkManagerEmittedEvents,
} from '../lib/cjs/puppeteer/common/NetworkManager.js';
import { HTTPRequest } from '../lib/cjs/puppeteer/common/HTTPRequest.js';
import { EventEmitter } from '../lib/cjs/puppeteer/common/EventEmitter.js';
import { Frame } from '../lib/cjs/puppeteer/common/FrameManager.js';
} from '../../lib/cjs/puppeteer/common/NetworkManager.js';
import { HTTPRequest } from '../../lib/cjs/puppeteer/common/HTTPRequest.js';
import { EventEmitter } from '../../lib/cjs/puppeteer/common/EventEmitter.js';
import { Frame } from '../../lib/cjs/puppeteer/common/FrameManager.js';
class MockCDPSession extends EventEmitter {
async send(): Promise<any> {}

View File

@ -22,7 +22,7 @@ import {
describeChromeOnly,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js';
import { ElementHandle } from '../../lib/cjs/puppeteer/common/JSHandle.js';
import utils from './utils.js';
describeChromeOnly('AriaQueryHandler', () => {

View File

@ -25,7 +25,7 @@ import {
} from './mocha-utils'; // eslint-disable-line import/extensions
import utils from './utils.js';
import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js';
import { ElementHandle } from '../../lib/cjs/puppeteer/common/JSHandle.js';
describe('ElementHandle specs', function () {
setupTestBrowserHooks();

View File

@ -32,7 +32,7 @@ describe('Fixtures', function () {
dumpio: true,
});
const res = spawn('node', [
path.join(__dirname, 'fixtures', 'dumpio.js'),
path.join(__dirname, '../fixtures', 'dumpio.js'),
puppeteerPath,
JSON.stringify(options),
]);
@ -47,7 +47,7 @@ describe('Fixtures', function () {
const { spawn } = await import('child_process');
const options = Object.assign({}, defaultBrowserOptions, { dumpio: true });
const res = spawn('node', [
path.join(__dirname, 'fixtures', 'dumpio.js'),
path.join(__dirname, '../fixtures', 'dumpio.js'),
puppeteerPath,
JSON.stringify(options),
]);
@ -64,7 +64,7 @@ describe('Fixtures', function () {
dumpio: false,
});
const res = spawn('node', [
path.join(__dirname, 'fixtures', 'closeme.js'),
path.join(__dirname, '../fixtures', 'closeme.js'),
puppeteerPath,
JSON.stringify(options),
]);

View File

@ -22,7 +22,7 @@ import {
setupTestPageAndContextHooks,
itFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { CDPSession } from '../lib/cjs/puppeteer/common/Connection.js';
import { CDPSession } from '../../lib/cjs/puppeteer/common/Connection.js';
describe('Frame specs', function () {
setupTestBrowserHooks();

View File

@ -31,7 +31,7 @@ const mkdtempAsync = promisify(fs.mkdtemp);
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const extensionPath = path.join(__dirname, 'assets', 'simple-extension');
const extensionPath = path.join(__dirname, '../assets', 'simple-extension');
describeChromeOnly('headful tests', function () {
/* These tests fire up an actual browser so let's

View File

@ -15,6 +15,7 @@
*/
import expect from 'expect';
import { TLSSocket } from 'tls';
import {
getTestState,
describeFailsFirefox,
@ -65,7 +66,9 @@ describe('ignoreHTTPSErrors', function () {
]);
const securityDetails = response.securityDetails();
expect(securityDetails.issuer()).toBe('puppeteer-tests');
const protocol = serverRequest.socket.getProtocol().replace('v', ' ');
const protocol = (serverRequest.socket as TLSSocket)
.getProtocol()
.replace('v', ' ');
expect(securityDetails.protocol()).toBe(protocol);
expect(securityDetails.subjectName()).toBe('puppeteer-tests');
expect(securityDetails.validFrom()).toBe(1589357069);
@ -94,7 +97,9 @@ describe('ignoreHTTPSErrors', function () {
expect(responses.length).toBe(2);
expect(responses[0].status()).toBe(302);
const securityDetails = responses[0].securityDetails();
const protocol = serverRequest.socket.getProtocol().replace('v', ' ');
const protocol = (serverRequest.socket as TLSSocket)
.getProtocol()
.replace('v', ' ');
expect(securityDetails.protocol()).toBe(protocol);
});
});

View File

@ -23,7 +23,7 @@ import {
describeFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt');
const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
describe('input tests', function () {
setupTestBrowserHooks();
@ -228,9 +228,9 @@ describe('input tests', function () {
.accept([
path.relative(
process.cwd(),
__dirname + '/assets/file-to-upload.txt'
__dirname + '/../assets/file-to-upload.txt'
),
path.relative(process.cwd(), __dirname + '/assets/pptr.png'),
path.relative(process.cwd(), __dirname + '/../assets/pptr.png'),
])
.catch((error_) => (error = error_));
expect(error).not.toBe(null);

View File

@ -15,7 +15,7 @@
*/
import expect from 'expect';
import { JSHandle } from '../lib/cjs/puppeteer/common/JSHandle.js';
import { JSHandle } from '../../lib/cjs/puppeteer/common/JSHandle.js';
import {
getTestState,
setupTestBrowserHooks,

View File

@ -23,7 +23,7 @@ import {
setupTestPageAndContextHooks,
itFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js';
import { KeyInput } from '../../lib/cjs/puppeteer/common/USKeyboardLayout.js';
describe('Keyboard', function () {
setupTestBrowserHooks();

View File

@ -29,7 +29,8 @@ import {
import utils from './utils.js';
import expect from 'expect';
import rimraf from 'rimraf';
import { Page } from '../lib/cjs/puppeteer/common/Page.js';
import { Page } from '../../lib/cjs/puppeteer/common/Page.js';
import { TLSSocket } from 'tls';
const mkdtempAsync = promisify(fs.mkdtemp);
const readFileAsync = promisify(fs.readFile);
@ -677,7 +678,9 @@ describe('Launcher specs', function () {
expect(error).toBe(null);
expect(response.ok()).toBe(true);
expect(response.securityDetails()).toBeTruthy();
const protocol = serverRequest.socket.getProtocol().replace('v', ' ');
const protocol = (serverRequest.socket as TLSSocket)
.getProtocol()
.replace('v', ' ');
expect(response.securityDetails().protocol()).toBe(protocol);
await page.close();
await browser.close();

View File

@ -24,17 +24,17 @@ import sinon from 'sinon';
import {
Browser,
BrowserContext,
} from '../lib/cjs/puppeteer/common/Browser.js';
import { isErrorLike } from '../lib/cjs/puppeteer/common/util.js';
import { Page } from '../lib/cjs/puppeteer/common/Page.js';
import { PuppeteerNode } from '../lib/cjs/puppeteer/node/Puppeteer.js';
import puppeteer from '../lib/cjs/puppeteer/puppeteer.js';
import { TestServer } from '../utils/testserver/index.js';
} from '../../lib/cjs/puppeteer/common/Browser.js';
import { isErrorLike } from '../../lib/cjs/puppeteer/common/util.js';
import { Page } from '../../lib/cjs/puppeteer/common/Page.js';
import { PuppeteerNode } from '../../lib/cjs/puppeteer/node/Puppeteer.js';
import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js';
import utils from './utils.js';
import { TestServer } from '../../utils/testserver/lib/index.js';
const setupServer = async () => {
const assetsPath = path.join(__dirname, 'assets');
const cachedPath = path.join(__dirname, 'assets', 'cached');
const assetsPath = path.join(__dirname, '../assets');
const cachedPath = path.join(__dirname, '../assets', 'cached');
const port = 8907;
const server = await TestServer.create(assetsPath, port);
@ -122,8 +122,8 @@ declare module 'expect/build/types' {
const setupGoldenAssertions = (): void => {
const suffix = product.toLowerCase();
const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix);
const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix);
const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`);
const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
if (fs.existsSync(OUTPUT_DIR)) {
rimraf.sync(OUTPUT_DIR);
}
@ -140,8 +140,8 @@ interface PuppeteerTestState {
defaultBrowserOptions: {
[x: string]: any;
};
server: any;
httpsServer: any;
server: TestServer;
httpsServer: TestServer;
isFirefox: boolean;
isChrome: boolean;
isHeadless: boolean;
@ -299,7 +299,7 @@ export const mochaHooks = {
state.isChrome = isChrome;
state.isHeadless = isHeadless;
state.headless = headless;
state.puppeteerPath = path.resolve(path.join(__dirname, '..'));
state.puppeteerPath = path.resolve(path.join(__dirname, '../..'));
},
],

View File

@ -21,7 +21,7 @@ import {
setupTestPageAndContextHooks,
itFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js';
import { KeyInput } from '../../lib/cjs/puppeteer/common/USKeyboardLayout.js';
interface Dimensions {
x: number;

View File

@ -27,7 +27,7 @@ import {
itChromeOnly,
itFirefoxOnly,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { HTTPResponse } from '../lib/cjs/puppeteer/api-docs-entry.js';
import { HTTPResponse } from '../../lib/cjs/puppeteer/api-docs-entry.js';
describe('network', function () {
setupTestBrowserHooks();
@ -363,7 +363,7 @@ describe('network', function () {
const response = await page.goto(server.PREFIX + '/pptr.png');
const imageBuffer = fs.readFileSync(
path.join(__dirname, 'assets', 'pptr.png')
path.join(__dirname, '../assets', 'pptr.png')
);
const responseBuffer = await response.buffer();
expect(responseBuffer.equals(imageBuffer)).toBe(true);
@ -374,7 +374,7 @@ describe('network', function () {
server.enableGzip('/pptr.png');
const response = await page.goto(server.PREFIX + '/pptr.png');
const imageBuffer = fs.readFileSync(
path.join(__dirname, 'assets', 'pptr.png')
path.join(__dirname, '../assets', 'pptr.png')
);
const responseBuffer = await response.buffer();
expect(responseBuffer.equals(imageBuffer)).toBe(true);

View File

@ -25,7 +25,7 @@ import {
Browser,
BrowserContext,
Page,
} from '../lib/cjs/puppeteer/api-docs-entry.js';
} from '../../lib/cjs/puppeteer/api-docs-entry.js';
describeChromeOnly('OOPIF', function () {
/* We use a special browser for this test as we need the --site-per-process flag */

View File

@ -26,9 +26,9 @@ import {
itFailsFirefox,
describeFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { Page, Metrics } from '../lib/cjs/puppeteer/common/Page.js';
import { CDPSession } from '../lib/cjs/puppeteer/common/Connection.js';
import { JSHandle } from '../lib/cjs/puppeteer/common/JSHandle.js';
import { Page, Metrics } from '../../lib/cjs/puppeteer/common/Page.js';
import { CDPSession } from '../../lib/cjs/puppeteer/common/Connection.js';
import { JSHandle } from '../../lib/cjs/puppeteer/common/JSHandle.js';
describe('Page', function () {
setupTestBrowserHooks();
@ -967,7 +967,7 @@ describe('Page', function () {
it('should throw exception in page context', async () => {
const { page } = getTestState();
await page.exposeFunction('woof', function () {
await page.exposeFunction('woof', () => {
throw new Error('WOOF WOOF');
});
const { message, stack } = await page.evaluate(async () => {
@ -978,7 +978,7 @@ describe('Page', function () {
}
});
expect(message).toBe('WOOF WOOF');
expect(stack).toContain(__filename);
expect(stack).toContain('page.spec.ts');
});
it('should support throwing "null"', async () => {
const { page } = getTestState();
@ -1425,7 +1425,7 @@ describe('Page', function () {
await page.goto(server.EMPTY_PAGE);
await page.addScriptTag({
path: path.join(__dirname, 'assets/es6/es6pathimport.js'),
path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
type: 'module',
});
await page.waitForFunction('window.__es6injected');
@ -1462,7 +1462,7 @@ describe('Page', function () {
await page.goto(server.EMPTY_PAGE);
const scriptHandle = await page.addScriptTag({
path: path.join(__dirname, 'assets/injectedfile.js'),
path: path.join(__dirname, '../assets/injectedfile.js'),
});
expect(scriptHandle.asElement()).not.toBeNull();
expect(await page.evaluate(() => globalThis.__injected)).toBe(42);
@ -1473,7 +1473,7 @@ describe('Page', function () {
await page.goto(server.EMPTY_PAGE);
await page.addScriptTag({
path: path.join(__dirname, 'assets/injectedfile.js'),
path: path.join(__dirname, '../assets/injectedfile.js'),
});
const result = await page.evaluate(
() => globalThis.__injectedError.stack
@ -1572,7 +1572,7 @@ describe('Page', function () {
await page.goto(server.EMPTY_PAGE);
const styleHandle = await page.addStyleTag({
path: path.join(__dirname, 'assets/injectedstyle.css'),
path: path.join(__dirname, '../assets/injectedstyle.css'),
});
expect(styleHandle.asElement()).not.toBeNull();
expect(
@ -1587,7 +1587,7 @@ describe('Page', function () {
await page.goto(server.EMPTY_PAGE);
await page.addStyleTag({
path: path.join(__dirname, 'assets/injectedstyle.css'),
path: path.join(__dirname, '../assets/injectedstyle.css'),
});
const styleHandle = await page.$('style');
const styleContent = await page.evaluate(
@ -1717,7 +1717,7 @@ describe('Page', function () {
return;
}
const outputFile = __dirname + '/assets/output.pdf';
const outputFile = __dirname + '/../assets/output.pdf';
await page.pdf({ path: outputFile });
expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0);
fs.unlinkSync(outputFile);

View File

@ -23,8 +23,9 @@ import {
itFailsWindows,
} from './mocha-utils'; // eslint-disable-line import/extensions
import type { Server, IncomingMessage, ServerResponse } from 'http';
import type { Browser } from '../lib/cjs/puppeteer/common/Browser.js';
import type { Browser } from '../../lib/cjs/puppeteer/common/Browser.js';
import type { AddressInfo } from 'net';
import { TestServer } from '../../utils/testserver/lib/index.js';
const HOSTNAME = os.hostname().toLowerCase();
@ -32,7 +33,7 @@ const HOSTNAME = os.hostname().toLowerCase();
* Requests to localhost do not get proxied by default. Create a URL using the hostname
* instead.
*/
function getEmptyPageUrl(server: { PORT: number; EMPTY_PAGE: string }): string {
function getEmptyPageUrl(server: TestServer): string {
const emptyPagePath = new URL(server.EMPTY_PAGE).pathname;
return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`;

View File

@ -19,7 +19,7 @@ import {
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.js';
import { CustomQueryHandler } from '../../lib/cjs/puppeteer/common/QueryHandler.js';
describe('querySelector', function () {
setupTestBrowserHooks();

View File

@ -24,8 +24,8 @@ import {
setupTestPageAndContextHooks,
describeFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { ActionResult } from '../lib/cjs/puppeteer/api-docs-entry.js';
import { InterceptResolutionAction } from '../lib/cjs/puppeteer/common/HTTPRequest.js';
import { ActionResult } from '../../lib/cjs/puppeteer/api-docs-entry.js';
import { InterceptResolutionAction } from '../../lib/cjs/puppeteer/common/HTTPRequest.js';
describe('request interception', function () {
setupTestBrowserHooks();
@ -592,7 +592,7 @@ describe('request interception', function () {
request.continue({}, 0);
});
await page.goto(
pathToFileURL(path.join(__dirname, 'assets', 'one-style.html'))
pathToFileURL(path.join(__dirname, '../assets', 'one-style.html'))
);
expect(urls.size).toBe(2);
expect(urls.has('one-style.html')).toBe(true);
@ -827,7 +827,7 @@ describe('request interception', function () {
await page.setRequestInterception(true);
page.on('request', (request) => {
const imageBuffer = fs.readFileSync(
path.join(__dirname, 'assets', 'pptr.png')
path.join(__dirname, '../assets', 'pptr.png')
);
request.respond(
{

View File

@ -507,7 +507,7 @@ describe('request interception', function () {
request.continue();
});
await page.goto(
pathToFileURL(path.join(__dirname, 'assets', 'one-style.html'))
pathToFileURL(path.join(__dirname, '../assets', 'one-style.html'))
);
expect(urls.size).toBe(2);
expect(urls.has('one-style.html')).toBe(true);
@ -759,7 +759,7 @@ describe('request interception', function () {
await page.setRequestInterception(true);
page.on('request', (request) => {
const imageBuffer = fs.readFileSync(
path.join(__dirname, 'assets', 'pptr.png')
path.join(__dirname, '../assets', 'pptr.png')
);
request.respond({
contentType: 'image/png',

View File

@ -23,7 +23,7 @@ import {
setupTestPageAndContextHooks,
itFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { Target } from '../lib/cjs/puppeteer/common/Target.js';
import { Target } from '../../lib/cjs/puppeteer/common/Target.js';
describe('Target', function () {
setupTestBrowserHooks();

View File

@ -32,7 +32,7 @@ describeChromeOnly('Tracing', function () {
const { defaultBrowserOptions, puppeteer } = getTestState();
browser = await puppeteer.launch(defaultBrowserOptions);
page = await browser.newPage();
outputFile = path.join(__dirname, 'assets', 'trace.json');
outputFile = path.join(__dirname, 'trace.json');
});
afterEach(async () => {

View File

@ -17,13 +17,10 @@
// TODO (@jackfranklin): convert to TS and enable type checking.
// @ts-nocheck
const fs = require('fs');
const path = require('path');
const expect = require('expect');
const GoldenUtils = require('./golden-utils.js');
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json'))
? path.join(__dirname, '..')
: path.join(__dirname, '..', '..');
const PROJECT_ROOT = path.join(__dirname, '..', '..');
const utils = (module.exports = {
extendExpectWithToBeGolden: function (goldenDir, outputDir) {

View File

@ -22,8 +22,8 @@ import {
describeFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import utils from './utils.js';
import { WebWorker } from '../lib/cjs/puppeteer/common/WebWorker.js';
import { ConsoleMessage } from '../lib/cjs/puppeteer/common/ConsoleMessage.js';
import { WebWorker } from '../../lib/cjs/puppeteer/common/WebWorker.js';
import { ConsoleMessage } from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
const { waitEvent } = utils;
describeFailsFirefox('Workers', function () {

View File

@ -1,6 +1,21 @@
{
"extends": "./tsconfig.test.json",
"compilerOptions": {
"noEmit": true
}
"esModuleInterop": true,
"allowJs": true,
"checkJs": false,
"target": "esnext",
"module": "CommonJS",
"moduleResolution": "node",
"declaration": false,
"declarationMap": false,
"resolveJsonModule": true,
"sourceMap": true,
"rootDir": "src",
"outDir": "build"
},
"include": ["src"],
"references": [
{ "path": "../src/tsconfig.cjs.json" },
{ "path": "../utils/testserver/tsconfig.json" }
]
}

View File

@ -1,14 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true,
"allowJs": true,
"checkJs": true,
"target": "esnext",
"module": "CommonJS",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"resolveJsonModule": true,
"sourceMap": true
}
}

View File

@ -18,9 +18,36 @@ const jsBuilder = require('./JSBuilder.js');
const mdBuilder = require('./MDBuilder.js');
const Documentation = require('./Documentation.js');
const Message = require('../Message.js');
const {
MODULES_TO_CHECK_FOR_COVERAGE,
} = require('../../../test/coverage-utils.js');
const MODULES_TO_CHECK_FOR_COVERAGE = {
Accessibility: '../../lib/cjs/puppeteer/common/Accessibility',
Browser: '../../lib/cjs/puppeteer/common/Browser',
BrowserContext: '../../lib/cjs/puppeteer/common/Browser',
BrowserFetcher: '../../lib/cjs/puppeteer/node/BrowserFetcher',
CDPSession: '../../lib/cjs/puppeteer/common/Connection',
ConsoleMessage: '../../lib/cjs/puppeteer/common/ConsoleMessage',
Coverage: '../../lib/cjs/puppeteer/common/Coverage',
Dialog: '../../lib/cjs/puppeteer/common/Dialog',
ElementHandle: '../../lib/cjs/puppeteer/common/JSHandle',
ExecutionContext: '../../lib/cjs/puppeteer/common/ExecutionContext',
EventEmitter: '../../lib/cjs/puppeteer/common/EventEmitter',
FileChooser: '../../lib/cjs/puppeteer/common/FileChooser',
Frame: '../../lib/cjs/puppeteer/common/FrameManager',
JSHandle: '../../lib/cjs/puppeteer/common/JSHandle',
Keyboard: '../../lib/cjs/puppeteer/common/Input',
Mouse: '../../lib/cjs/puppeteer/common/Input',
Page: '../../lib/cjs/puppeteer/common/Page',
Puppeteer: '../../lib/cjs/puppeteer/common/Puppeteer',
PuppeteerNode: '../../lib/cjs/puppeteer/node/Puppeteer',
HTTPRequest: '../../lib/cjs/puppeteer/common/HTTPRequest',
HTTPResponse: '../../lib/cjs/puppeteer/common/HTTPResponse',
SecurityDetails: '../../lib/cjs/puppeteer/common/SecurityDetails',
Target: '../../lib/cjs/puppeteer/common/Target',
TimeoutError: '../../lib/cjs/puppeteer/common/Errors',
Touchscreen: '../../lib/cjs/puppeteer/common/Input',
Tracing: '../../lib/cjs/puppeteer/common/Tracing',
WebWorker: '../../lib/cjs/puppeteer/common/WebWorker',
};
const EXCLUDE_PROPERTIES = new Set([
'Browser.create',

View File

@ -1,303 +0,0 @@
// @ts-nocheck
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const http = require('http');
const https = require('https');
const url = require('url');
const fs = require('fs');
const path = require('path');
const mime = require('mime');
const WebSocketServer = require('ws').Server;
const fulfillSymbol = Symbol('fullfil callback');
const rejectSymbol = Symbol('reject callback');
class TestServer {
/** @type number */
PORT = undefined;
/** @type string */
PREFIX = undefined;
/** @type string */
CROSS_PROCESS_PREFIX = undefined;
/** @type string */
EMPTY_PAGE = undefined;
/**
* @param {string} dirPath
* @param {number} port
* @returns {!TestServer}
*/
static async create(dirPath, port) {
const server = new TestServer(dirPath, port);
await new Promise((x) => server._server.once('listening', x));
return server;
}
/**
* @param {string} dirPath
* @param {number} port
* @returns {!TestServer}
*/
static async createHTTPS(dirPath, port) {
const server = new TestServer(dirPath, port, {
key: fs.readFileSync(path.join(__dirname, 'key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'cert.pem')),
passphrase: 'aaaa',
});
await new Promise((x) => server._server.once('listening', x));
return server;
}
/**
* @param {string} dirPath
* @param {number} port
* @param {!Object=} sslOptions
*/
constructor(dirPath, port, sslOptions) {
if (sslOptions) {
this._server = https.createServer(sslOptions, this._onRequest.bind(this));
} else {
this._server = http.createServer(this._onRequest.bind(this));
}
this._server.on('connection', (socket) => this._onSocket(socket));
this._wsServer = new WebSocketServer({ server: this._server });
this._wsServer.on('connection', this._onWebSocketConnection.bind(this));
this._server.listen(port);
this._dirPath = dirPath;
this._startTime = new Date();
this._cachedPathPrefix = null;
/** @type {!Set<!net.Socket>} */
this._sockets = new Set();
/** @type {!Map<string, function(!IncomingMessage, !ServerResponse)>} */
this._routes = new Map();
/** @type {!Map<string, !{username:string, password:string}>} */
this._auths = new Map();
/** @type {!Map<string, string>} */
this._csp = new Map();
/** @type {!Set<string>} */
this._gzipRoutes = new Set();
/** @type {!Map<string, !Promise>} */
this._requestSubscribers = new Map();
}
_onSocket(socket) {
this._sockets.add(socket);
// ECONNRESET is a legit error given
// that tab closing simply kills process.
socket.on('error', (error) => {
if (error.code !== 'ECONNRESET') {
throw error;
}
});
socket.once('close', () => this._sockets.delete(socket));
}
/**
* @param {string} pathPrefix
*/
enableHTTPCache(pathPrefix) {
this._cachedPathPrefix = pathPrefix;
}
/**
* @param {string} path
* @param {string} username
* @param {string} password
*/
setAuth(path, username, password) {
this._auths.set(path, { username, password });
}
enableGzip(path) {
this._gzipRoutes.add(path);
}
/**
* @param {string} path
* @param {string} csp
*/
setCSP(path, csp) {
this._csp.set(path, csp);
}
async stop() {
this.reset();
for (const socket of this._sockets) {
socket.destroy();
}
this._sockets.clear();
await new Promise((x) => this._server.close(x));
}
/**
* @param {string} path
* @param {function(!IncomingMessage, !ServerResponse)} handler
*/
setRoute(path, handler) {
this._routes.set(path, handler);
}
/**
* @param {string} from
* @param {string} to
*/
setRedirect(from, to) {
this.setRoute(from, (req, res) => {
res.writeHead(302, { location: to });
res.end();
});
}
/**
* @param {string} path
* @returns {!Promise<!IncomingMessage>}
*/
waitForRequest(path) {
let promise = this._requestSubscribers.get(path);
if (promise) {
return promise;
}
let fulfill, reject;
promise = new Promise((f, r) => {
fulfill = f;
reject = r;
});
promise[fulfillSymbol] = fulfill;
promise[rejectSymbol] = reject;
this._requestSubscribers.set(path, promise);
return promise;
}
reset() {
this._routes.clear();
this._auths.clear();
this._csp.clear();
this._gzipRoutes.clear();
const error = new Error('Static Server has been reset');
for (const subscriber of this._requestSubscribers.values()) {
subscriber[rejectSymbol].call(null, error);
}
this._requestSubscribers.clear();
}
_onRequest(request, response) {
request.on('error', (error) => {
if (error.code === 'ECONNRESET') {
response.end();
} else {
throw error;
}
});
request.postBody = new Promise((resolve) => {
let body = '';
request.on('data', (chunk) => (body += chunk));
request.on('end', () => resolve(body));
});
const pathName = url.parse(request.url).path;
if (this._auths.has(pathName)) {
const auth = this._auths.get(pathName);
const credentials = Buffer.from(
(request.headers.authorization || '').split(' ')[1] || '',
'base64'
).toString();
if (credentials !== `${auth.username}:${auth.password}`) {
response.writeHead(401, {
'WWW-Authenticate': 'Basic realm="Secure Area"',
});
response.end('HTTP Error 401 Unauthorized: Access is denied');
return;
}
}
// Notify request subscriber.
if (this._requestSubscribers.has(pathName)) {
this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request);
this._requestSubscribers.delete(pathName);
}
const handler = this._routes.get(pathName);
if (handler) {
handler.call(null, request, response);
} else {
const pathName = url.parse(request.url).path;
this.serveFile(request, response, pathName);
}
}
/**
* @param {!IncomingMessage} request
* @param {!ServerResponse} response
* @param {string} pathName
*/
serveFile(request, response, pathName) {
if (pathName === '/') {
pathName = '/index.html';
}
const filePath = path.join(this._dirPath, pathName.substring(1));
if (
this._cachedPathPrefix !== null &&
filePath.startsWith(this._cachedPathPrefix)
) {
if (request.headers['if-modified-since']) {
response.statusCode = 304; // not modified
response.end();
return;
}
response.setHeader('Cache-Control', 'public, max-age=31536000');
response.setHeader('Last-Modified', this._startTime.toISOString());
} else {
response.setHeader('Cache-Control', 'no-cache, no-store');
}
if (this._csp.has(pathName)) {
response.setHeader('Content-Security-Policy', this._csp.get(pathName));
}
fs.readFile(filePath, (err, data) => {
if (err) {
response.statusCode = 404;
response.end(`File not found: ${filePath}`);
return;
}
const mimeType = mime.getType(filePath);
const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(
mimeType
);
const contentType = isTextEncoding
? `${mimeType}; charset=utf-8`
: mimeType;
response.setHeader('Content-Type', contentType);
if (this._gzipRoutes.has(pathName)) {
response.setHeader('Content-Encoding', 'gzip');
const zlib = require('zlib');
zlib.gzip(data, (_, result) => {
response.end(result);
});
} else {
response.end(data);
}
});
}
_onWebSocketConnection(connection) {
connection.send('opened');
}
}
module.exports = { TestServer };

View File

@ -2,7 +2,7 @@
"name": "@pptr/testserver",
"version": "0.5.0",
"description": "testing server",
"main": "index.js",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},

View File

@ -0,0 +1,278 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import assert from 'assert';
import { readFile, readFileSync } from 'fs';
import {
createServer as createHttpServer,
IncomingMessage,
RequestListener,
Server as HttpServer,
ServerResponse,
} from 'http';
import {
createServer as createHttpsServer,
Server as HttpsServer,
ServerOptions as HttpsServerOptions,
} from 'https';
import { getType as getMimeType } from 'mime';
import { join } from 'path';
import { Duplex } from 'stream';
import { Server as WebSocketServer, WebSocket } from 'ws';
import { gzip } from 'zlib';
interface Subscriber {
resolve: (msg: IncomingMessage) => void;
reject: (err?: Error) => void;
promise: Promise<IncomingMessage>;
}
type TestIncomingMessage = IncomingMessage & { postBody?: Promise<string> };
export class TestServer {
PORT?: number;
PREFIX?: string;
CROSS_PROCESS_PREFIX?: string;
EMPTY_PAGE?: string;
#dirPath: string;
#server: HttpsServer | HttpServer;
#wsServer: WebSocketServer;
#startTime = new Date();
#cachedPathPrefix?: string;
#connections = new Set<Duplex>();
#routes = new Map<
string,
(msg: IncomingMessage, res: ServerResponse) => void
>();
#auths = new Map<string, { username: string; password: string }>();
#csp = new Map<string, string>();
#gzipRoutes = new Set<string>();
#requestSubscribers = new Map<string, Subscriber>();
static async create(dirPath: string, port: number): Promise<TestServer> {
const server = new TestServer(dirPath, port);
await new Promise((x) => server.#server.once('listening', x));
return server;
}
static async createHTTPS(dirPath: string, port: number): Promise<TestServer> {
const server = new TestServer(dirPath, port, {
key: readFileSync(join(__dirname, '..', 'key.pem')),
cert: readFileSync(join(__dirname, '..', 'cert.pem')),
passphrase: 'aaaa',
});
await new Promise((x) => server.#server.once('listening', x));
return server;
}
constructor(dirPath: string, port: number, sslOptions?: HttpsServerOptions) {
this.#dirPath = dirPath;
if (sslOptions) {
this.#server = createHttpsServer(sslOptions, this.#onRequest);
} else {
this.#server = createHttpServer(this.#onRequest);
}
this.#server.on('connection', this.#onServerConnection);
this.#wsServer = new WebSocketServer({ server: this.#server });
this.#wsServer.on('connection', this.#onWebSocketConnection);
this.#server.listen(port);
}
#onServerConnection = (connection: Duplex): void => {
this.#connections.add(connection);
// ECONNRESET is a legit error given
// that tab closing simply kills process.
connection.on('error', (error) => {
if ((error as NodeJS.ErrnoException).code !== 'ECONNRESET') {
throw error;
}
});
connection.once('close', () => this.#connections.delete(connection));
};
enableHTTPCache(pathPrefix: string): void {
this.#cachedPathPrefix = pathPrefix;
}
setAuth(path: string, username: string, password: string): void {
this.#auths.set(path, { username, password });
}
enableGzip(path: string): void {
this.#gzipRoutes.add(path);
}
setCSP(path: string, csp: string): void {
this.#csp.set(path, csp);
}
async stop(): Promise<void> {
this.reset();
for (const socket of this.#connections) {
socket.destroy();
}
this.#connections.clear();
await new Promise((x) => this.#server.close(x));
}
setRoute(
path: string,
handler: (req: IncomingMessage, res: ServerResponse) => void
): void {
this.#routes.set(path, handler);
}
setRedirect(from: string, to: string): void {
this.setRoute(from, (_, res) => {
res.writeHead(302, { location: to });
res.end();
});
}
waitForRequest(path: string): Promise<TestIncomingMessage> {
const subscriber = this.#requestSubscribers.get(path);
if (subscriber) {
return subscriber.promise;
}
let resolve!: (value: IncomingMessage) => void;
let reject!: (reason?: Error) => void;
const promise = new Promise<IncomingMessage>((res, rej) => {
resolve = res;
reject = rej;
});
this.#requestSubscribers.set(path, { resolve, reject, promise });
return promise;
}
reset(): void {
this.#routes.clear();
this.#auths.clear();
this.#csp.clear();
this.#gzipRoutes.clear();
const error = new Error('Static Server has been reset');
for (const subscriber of this.#requestSubscribers.values()) {
subscriber.reject.call(undefined, error);
}
this.#requestSubscribers.clear();
}
#onRequest: RequestListener = (
request: TestIncomingMessage,
response
): void => {
request.on('error', (error: { code: string }) => {
if (error.code === 'ECONNRESET') {
response.end();
} else {
throw error;
}
});
request.postBody = new Promise((resolve) => {
let body = '';
request.on('data', (chunk: string) => (body += chunk));
request.on('end', () => resolve(body));
});
assert(request.url);
const url = new URL(request.url, `https://${request.headers.host}`);
const path = url.pathname + url.search;
const auth = this.#auths.get(path);
if (auth) {
const credentials = Buffer.from(
(request.headers.authorization || '').split(' ')[1] || '',
'base64'
).toString();
if (credentials !== `${auth.username}:${auth.password}`) {
response.writeHead(401, {
'WWW-Authenticate': 'Basic realm="Secure Area"',
});
response.end('HTTP Error 401 Unauthorized: Access is denied');
return;
}
}
const subscriber = this.#requestSubscribers.get(path);
if (subscriber) {
subscriber.resolve.call(undefined, request);
this.#requestSubscribers.delete(path);
}
const handler = this.#routes.get(path);
if (handler) {
handler.call(undefined, request, response);
} else {
this.serveFile(request, response, path);
}
};
serveFile(
request: IncomingMessage,
response: ServerResponse,
pathName: string
): void {
if (pathName === '/') {
pathName = '/index.html';
}
const filePath = join(this.#dirPath, pathName.substring(1));
if (this.#cachedPathPrefix && filePath.startsWith(this.#cachedPathPrefix)) {
if (request.headers['if-modified-since']) {
response.statusCode = 304; // not modified
response.end();
return;
}
response.setHeader('Cache-Control', 'public, max-age=31536000');
response.setHeader('Last-Modified', this.#startTime.toISOString());
} else {
response.setHeader('Cache-Control', 'no-cache, no-store');
}
const csp = this.#csp.get(pathName);
if (csp) {
response.setHeader('Content-Security-Policy', csp);
}
readFile(filePath, (err, data) => {
if (err) {
response.statusCode = 404;
response.end(`File not found: ${filePath}`);
return;
}
const mimeType = getMimeType(filePath);
if (mimeType) {
const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(
mimeType
);
const contentType = isTextEncoding
? `${mimeType}; charset=utf-8`
: mimeType;
response.setHeader('Content-Type', contentType);
}
if (this.#gzipRoutes.has(pathName)) {
response.setHeader('Content-Encoding', 'gzip');
gzip(data, (_, result) => {
response.end(result);
});
} else {
response.end(data);
}
});
}
#onWebSocketConnection = (socket: WebSocket): void => {
socket.send('opened');
};
}

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"composite": true,
"allowJs": true,
"module": "CommonJS"
},
"include": ["src"]
}