chore: grid placeholder (#24598)
This commit is contained in:
parent
1afa9d44fb
commit
6731f5b6d5
112
package-lock.json
generated
112
package-lock.json
generated
|
|
@ -1424,6 +1424,10 @@
|
||||||
"resolved": "packages/playwright-ct-vue2",
|
"resolved": "packages/playwright-ct-vue2",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/experimental-grid": {
|
||||||
|
"resolved": "packages/playwright-grid",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"resolved": "packages/playwright-test",
|
"resolved": "packages/playwright-test",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -1524,6 +1528,25 @@
|
||||||
"@types/tern": "*"
|
"@types/tern": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/commander": {
|
||||||
|
"version": "2.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz",
|
||||||
|
"integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==",
|
||||||
|
"deprecated": "This is a stub types definition for commander (https://github.com/tj/commander.js). commander provides its own type definitions, so you don't need @types/commander installed!",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/debug": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
||||||
|
|
@ -1544,6 +1567,12 @@
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "0.7.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||||
|
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.18.34",
|
"version": "16.18.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
||||||
|
|
@ -1598,9 +1627,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||||
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
|
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
|
|
@ -6170,7 +6199,6 @@
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|
@ -6533,6 +6561,32 @@
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/playwright-grid": {
|
||||||
|
"name": "@playwright/experimental-grid",
|
||||||
|
"version": "1.37.0-next",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^11.0.0",
|
||||||
|
"debug": "^4.3.2",
|
||||||
|
"ws": "^8.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright-grid": "cli.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/commander": "^2.12.2",
|
||||||
|
"@types/debug": "^4.1.8",
|
||||||
|
"@types/ws": "^8.5.5",
|
||||||
|
"playwright-core": "1.37.0-next"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/playwright-grid/node_modules/commander": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.37.0-next",
|
"version": "1.37.0-next",
|
||||||
|
|
@ -7505,6 +7559,25 @@
|
||||||
"vue": "^2.7.14"
|
"vue": "^2.7.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@playwright/experimental-grid": {
|
||||||
|
"version": "file:packages/playwright-grid",
|
||||||
|
"requires": {
|
||||||
|
"@types/commander": "^2.12.2",
|
||||||
|
"@types/debug": "^4.1.8",
|
||||||
|
"@types/ws": "^8.5.5",
|
||||||
|
"commander": "^11.0.0",
|
||||||
|
"debug": "^4.3.2",
|
||||||
|
"playwright-core": "1.37.0-next",
|
||||||
|
"ws": "^8.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@playwright/test": {
|
"@playwright/test": {
|
||||||
"version": "file:packages/playwright-test",
|
"version": "file:packages/playwright-test",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -7593,6 +7666,24 @@
|
||||||
"@types/tern": "*"
|
"@types/tern": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/commander": {
|
||||||
|
"version": "2.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz",
|
||||||
|
"integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"commander": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/debug": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/ms": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/estree": {
|
"@types/estree": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
||||||
|
|
@ -7612,6 +7703,12 @@
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/ms": {
|
||||||
|
"version": "0.7.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||||
|
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "16.18.34",
|
"version": "16.18.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
||||||
|
|
@ -7661,9 +7758,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/ws": {
|
"@types/ws": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||||
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
|
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
|
|
@ -10638,7 +10735,6 @@
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
"dev": true,
|
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"xml2js": {
|
"xml2js": {
|
||||||
|
|
|
||||||
4
packages/playwright-grid/.npmignore
Normal file
4
packages/playwright-grid/.npmignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
**/*
|
||||||
|
!cli.js
|
||||||
|
!lib/**/*.js
|
||||||
|
!README.md
|
||||||
1
packages/playwright-grid/README.md
Normal file
1
packages/playwright-grid/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# wip
|
||||||
3
packages/playwright-grid/cli.js
Executable file
3
packages/playwright-grid/cli.js
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('./lib/cli.js');
|
||||||
30
packages/playwright-grid/package.json
Normal file
30
packages/playwright-grid/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "@playwright/experimental-grid",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"description": "Playwright Grid",
|
||||||
|
"scripts": {},
|
||||||
|
"bin": {
|
||||||
|
"playwright-grid": "./cli.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^11.0.0",
|
||||||
|
"debug": "^4.3.2",
|
||||||
|
"ws": "^8.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/commander": "^2.12.2",
|
||||||
|
"@types/debug": "^4.1.8",
|
||||||
|
"@types/ws": "^8.5.5",
|
||||||
|
"playwright-core": "1.37.0-next"
|
||||||
|
},
|
||||||
|
"repository": "github:Microsoft/playwright",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"homepage": "https://playwright.dev",
|
||||||
|
"author": {
|
||||||
|
"name": "Microsoft Corporation"
|
||||||
|
},
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
}
|
||||||
36
packages/playwright-grid/src/cli.ts
Normal file
36
packages/playwright-grid/src/cli.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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 { program } from 'commander';
|
||||||
|
const packageJSON = require('../package.json');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('Version ' + packageJSON.version)
|
||||||
|
.name('playwright-grid');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('grid')
|
||||||
|
.action(function() {
|
||||||
|
require('./grid/grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('node')
|
||||||
|
.action(function() {
|
||||||
|
require('./node/node');
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
19
packages/playwright-grid/src/common/capabilities.ts
Normal file
19
packages/playwright-grid/src/common/capabilities.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Capabilities = {
|
||||||
|
platform?: typeof process.platform;
|
||||||
|
};
|
||||||
113
packages/playwright-grid/src/common/httpServer.ts
Normal file
113
packages/playwright-grid/src/common/httpServer.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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 fs from 'fs';
|
||||||
|
import http from 'http';
|
||||||
|
import path from 'path';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { Server as WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
|
||||||
|
|
||||||
|
export class HttpServer {
|
||||||
|
readonly server: http.Server;
|
||||||
|
private _urlPrefix: string;
|
||||||
|
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._urlPrefix = '';
|
||||||
|
this.server = http.createServer(this._onRequest.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
routePrefix(prefix: string, handler: ServerRouteHandler) {
|
||||||
|
this._routes.push({ prefix, handler });
|
||||||
|
}
|
||||||
|
|
||||||
|
routePath(path: string, handler: ServerRouteHandler) {
|
||||||
|
this._routes.push({ exact: path, handler });
|
||||||
|
}
|
||||||
|
|
||||||
|
createWebSocketServer() {
|
||||||
|
return new WebSocketServer({ server: this.server });
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(port?: number): Promise<string> {
|
||||||
|
this.server.listen(port);
|
||||||
|
await new Promise(cb => this.server!.once('listening', cb));
|
||||||
|
const address = this.server.address();
|
||||||
|
this._urlPrefix = typeof address === 'string' ? address : `http://127.0.0.1:${address!.port}`;
|
||||||
|
return this._urlPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
await new Promise(cb => this.server!.close(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
urlPrefix() {
|
||||||
|
return this._urlPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
serveFile(response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(absoluteFilePath);
|
||||||
|
response.statusCode = 200;
|
||||||
|
const contentType = extensionToMime[path.extname(absoluteFilePath).substring(1)] || 'application/octet-stream';
|
||||||
|
response.setHeader('Content-Type', contentType);
|
||||||
|
response.setHeader('Content-Length', content.byteLength);
|
||||||
|
for (const [name, value] of Object.entries(headers || {}))
|
||||||
|
response.setHeader(name, value);
|
||||||
|
response.end(content);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||||
|
request.on('error', () => response.end());
|
||||||
|
try {
|
||||||
|
if (!request.url) {
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL('http://localhost' + request.url);
|
||||||
|
for (const route of this._routes) {
|
||||||
|
if (route.exact && url.pathname === route.exact && route.handler(request, response))
|
||||||
|
return;
|
||||||
|
if (route.prefix && url.pathname.startsWith(route.prefix) && route.handler(request, response))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.end();
|
||||||
|
} catch (e) {
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionToMime: { [key: string]: string } = {
|
||||||
|
'css': 'text/css',
|
||||||
|
'html': 'text/html',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'png': 'image/png',
|
||||||
|
'ttf': 'font/ttf',
|
||||||
|
'svg': 'image/svg+xml',
|
||||||
|
'webp': 'image/webp',
|
||||||
|
'woff': 'font/woff',
|
||||||
|
'woff2': 'font/woff2',
|
||||||
|
};
|
||||||
368
packages/playwright-grid/src/grid/grid.ts
Normal file
368
packages/playwright-grid/src/grid/grid.ts
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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 crypto from 'crypto';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import type { Server as WebSocketServer } from 'ws';
|
||||||
|
import { HttpServer } from '../common/httpServer';
|
||||||
|
import type { Capabilities } from '../common/capabilities';
|
||||||
|
import type http from 'http';
|
||||||
|
import type stream from 'stream';
|
||||||
|
|
||||||
|
const PORT = +(process.env.PLAYWRIGHT_GRID_PORT || '3113');
|
||||||
|
|
||||||
|
class WebSocketRequest {
|
||||||
|
private _socketError: Error | undefined;
|
||||||
|
|
||||||
|
constructor(readonly wsServer: WebSocketServer, readonly request: http.IncomingMessage, readonly socket: stream.Duplex, readonly head: Buffer) {
|
||||||
|
this.socket.on('error', e => this._socketError = e);
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade(extraHeaders: string[] = []): Promise<WebSocket | null> {
|
||||||
|
if (this._socketError || this.socket.destroyed)
|
||||||
|
return Promise.resolve(null);
|
||||||
|
|
||||||
|
return new Promise<WebSocket | null>(f => {
|
||||||
|
const socketEndTimer = setTimeout(() => {
|
||||||
|
this.socket.destroy();
|
||||||
|
f(null);
|
||||||
|
}, 5000);
|
||||||
|
this.wsServer.once('headers', headers => {
|
||||||
|
for (let i = 0; i < extraHeaders.length; i += 2) {
|
||||||
|
if (extraHeaders[i].toLowerCase().startsWith('x-playwright'))
|
||||||
|
headers.push(`${extraHeaders[i]}: ${extraHeaders[i + 1]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.wsServer.handleUpgrade(this.request, this.socket, this.head, ws => {
|
||||||
|
if (ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
|
||||||
|
f(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(socketEndTimer);
|
||||||
|
this.wsServer.emit('connection', ws, this.request);
|
||||||
|
f(ws);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientRequest = {
|
||||||
|
webSocketRequest: WebSocketRequest;
|
||||||
|
capabilities: Capabilities;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Worker {
|
||||||
|
readonly workerId = 'worker@' + createGuid();
|
||||||
|
private _workerSocketRequest: WebSocketRequest | undefined;
|
||||||
|
private _workerSocket: WebSocket | undefined;
|
||||||
|
private _clientSocket: WebSocket | undefined;
|
||||||
|
private _log: debug.Debugger;
|
||||||
|
private _state: 'new' | 'available' | 'connecting' | 'connected' | 'closed' = 'new';
|
||||||
|
private _onClose: () => void;
|
||||||
|
private _retireTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor(onClose: () => void) {
|
||||||
|
this._log = debug(`pw:grid:${this.workerId}`);
|
||||||
|
this._onClose = onClose;
|
||||||
|
this._log('worker created');
|
||||||
|
|
||||||
|
// Workers have 30 seconds to be picked up.
|
||||||
|
this._retireTimer = setTimeout(() => {
|
||||||
|
this.close();
|
||||||
|
}, 30_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
state(): 'new' | 'available' | 'connecting' | 'connected' | 'closed' {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
workerConnected(workerSocketRequest: WebSocketRequest) {
|
||||||
|
this._log('worker available');
|
||||||
|
this._state = 'available';
|
||||||
|
this._workerSocketRequest = workerSocketRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(clientRequest: ClientRequest): Promise<'workerError' | 'clientError' | 'success'> {
|
||||||
|
this._log('connect', clientRequest.webSocketRequest.request.headers);
|
||||||
|
this._state = 'connecting';
|
||||||
|
|
||||||
|
clearTimeout(this._retireTimer);
|
||||||
|
|
||||||
|
const workerSocket = await this._workerSocketRequest!.upgrade(clientRequest.webSocketRequest.request.rawHeaders);
|
||||||
|
if (!workerSocket || workerSocket.readyState === WebSocket.CLOSED || workerSocket.readyState === WebSocket.CLOSING) {
|
||||||
|
this.close();
|
||||||
|
return 'workerError';
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientSocket = await clientRequest.webSocketRequest.upgrade();
|
||||||
|
if (!clientSocket || clientSocket.readyState === WebSocket.CLOSED || clientSocket.readyState === WebSocket.CLOSING) {
|
||||||
|
this.close();
|
||||||
|
return 'clientError';
|
||||||
|
}
|
||||||
|
|
||||||
|
this._wire(workerSocket, clientSocket);
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
private _wire(workerSocket: WebSocket, clientSocket: WebSocket) {
|
||||||
|
this._log('connected');
|
||||||
|
|
||||||
|
this._state = 'connected';
|
||||||
|
workerSocket.on('close', () => this.close());
|
||||||
|
workerSocket.on('error', () => this.close());
|
||||||
|
clientSocket.on('close', () => this.close());
|
||||||
|
clientSocket.on('error', () => this.close());
|
||||||
|
clientSocket.on('message', data => {
|
||||||
|
this._workerSocket?.send(data);
|
||||||
|
});
|
||||||
|
workerSocket.on('message', data => {
|
||||||
|
this._clientSocket?.send(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._workerSocket = workerSocket;
|
||||||
|
this._clientSocket = clientSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this._state === 'closed')
|
||||||
|
return;
|
||||||
|
this._log('close');
|
||||||
|
this._state = 'closed';
|
||||||
|
this._workerSocket?.close();
|
||||||
|
this._clientSocket?.close();
|
||||||
|
this._workerSocket = undefined;
|
||||||
|
this._clientSocket = undefined;
|
||||||
|
this._onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugInfo() {
|
||||||
|
return { state: this._state };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
readonly nodeId = 'node@' + createGuid();
|
||||||
|
private _ws: WebSocket;
|
||||||
|
readonly _workers = new Map<string, Worker>();
|
||||||
|
private _log: debug.Debugger;
|
||||||
|
private _onWorkersChanged: () => void;
|
||||||
|
private _onClose: () => void;
|
||||||
|
private _capabilities: Capabilities;
|
||||||
|
private _capacity: number;
|
||||||
|
|
||||||
|
constructor(ws: WebSocket, capacity: number, capabilities: Capabilities, onWorkersChanged: () => void, onClose: () => void) {
|
||||||
|
this._capabilities = capabilities;
|
||||||
|
this._capacity = capacity;
|
||||||
|
this._log = debug(`pw:grid:${this.nodeId}`);
|
||||||
|
ws.on('close', () => this.close());
|
||||||
|
ws.on('error', () => this.close());
|
||||||
|
ws.send(JSON.stringify({ nodeId: this.nodeId }));
|
||||||
|
this._ws = ws;
|
||||||
|
this._onWorkersChanged = onWorkersChanged;
|
||||||
|
this._onClose = onClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWorker(workerId: string) {
|
||||||
|
return this._workers.has(workerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCapabilities(capabilities: Capabilities): boolean {
|
||||||
|
return !capabilities.platform || this._capabilities.platform === capabilities.platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
workers() {
|
||||||
|
return [...this._workers.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
canCreateWorker() {
|
||||||
|
return this._workers.size < this._capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWorker() {
|
||||||
|
const worker = new Worker(() => {
|
||||||
|
this._workers.delete(worker.workerId);
|
||||||
|
this._onWorkersChanged();
|
||||||
|
});
|
||||||
|
this._workers.set(worker.workerId, worker);
|
||||||
|
this._ws.send(JSON.stringify({ workerId: worker.workerId }));
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
workerConnected(workerId: string, webSocketRequest: WebSocketRequest) {
|
||||||
|
const worker = this._workers.get(workerId);
|
||||||
|
if (worker) {
|
||||||
|
worker.workerConnected(webSocketRequest);
|
||||||
|
this._onWorkersChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._log('close');
|
||||||
|
this._ws?.close();
|
||||||
|
this._onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProxyServer {
|
||||||
|
private _server: HttpServer;
|
||||||
|
private _wsServer: WebSocketServer;
|
||||||
|
private _nodes = new Map<string, Node>();
|
||||||
|
private _log: debug.Debugger;
|
||||||
|
private _clientRequests: ClientRequest[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._log = debug(`pw:grid:proxy`);
|
||||||
|
this._server = new HttpServer();
|
||||||
|
|
||||||
|
this._server.routePath('/', (request, response) => {
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.setHeader('Content-Type', 'text/plain');
|
||||||
|
response.end(this._state());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._wsServer = new WebSocket.Server({ noServer: true });
|
||||||
|
this._wsServer.on('connection', ws => {
|
||||||
|
ws.on('error', e => this._log(e));
|
||||||
|
});
|
||||||
|
this._server.server.on('upgrade', async (request, socket, head) => {
|
||||||
|
const url = new URL('http://internal' + request.url);
|
||||||
|
const params = url.searchParams;
|
||||||
|
this._log(url.toString());
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/registerNode')) {
|
||||||
|
const nodeRequest = new WebSocketRequest(this._wsServer, request, socket, head);
|
||||||
|
const ws = await nodeRequest.upgrade();
|
||||||
|
if (!ws)
|
||||||
|
return;
|
||||||
|
const capacity = +(params.get('capacity') || '1');
|
||||||
|
const capabilities = JSON.parse(params.get('caps')!) as Capabilities;
|
||||||
|
const node = new Node(ws, capacity, capabilities, () => {
|
||||||
|
this._makeAMatch();
|
||||||
|
}, () => {
|
||||||
|
this._nodes.delete(node.nodeId);
|
||||||
|
});
|
||||||
|
this._nodes.set(node.nodeId, node);
|
||||||
|
this._log('register node', node.nodeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/registerWorker')) {
|
||||||
|
const nodeId = params.get('nodeId')!;
|
||||||
|
const workerId = params.get('workerId')!;
|
||||||
|
const node = this._nodes.get(nodeId);
|
||||||
|
if (!node) {
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!node.hasWorker(workerId)) {
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workerRequest = new WebSocketRequest(this._wsServer, request, socket, head);
|
||||||
|
node.workerConnected(workerId, workerRequest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/') {
|
||||||
|
const capabilities = JSON.parse(params.get('caps') || '{}') as Capabilities;
|
||||||
|
this._addClientRequest({
|
||||||
|
webSocketRequest: new WebSocketRequest(this._wsServer, request, socket, head),
|
||||||
|
capabilities,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addClientRequest(clientRequest: ClientRequest) {
|
||||||
|
this._clientRequests.push(clientRequest);
|
||||||
|
this._makeAMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nodesWithCapabilities(capabilities: Capabilities | null): Node[] {
|
||||||
|
return [...this._nodes.values()].filter(node => !capabilities || node.hasCapabilities(capabilities));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _workers(capabilities: Capabilities | null): Worker[] {
|
||||||
|
const result: Worker[] = [];
|
||||||
|
for (const node of this._nodesWithCapabilities(capabilities))
|
||||||
|
result.push(...node.workers());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makeAMatch() {
|
||||||
|
this._log('making a match', {
|
||||||
|
clients: this._clientRequests.length,
|
||||||
|
nodes: this._nodes.size,
|
||||||
|
workers: this._workers(null).length,
|
||||||
|
availableWorkers: this._workers(null).filter(w => w.state() === 'available').length
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove closed client requests.
|
||||||
|
this._clientRequests = this._clientRequests.filter(c => c.webSocketRequest.socket.readable);
|
||||||
|
|
||||||
|
if (!this._clientRequests.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const capabilities = this._clientRequests[0].capabilities;
|
||||||
|
const nodes = this._nodesWithCapabilities(capabilities);
|
||||||
|
const availableWorkers = nodes.map(n => n.workers()).flat().filter(w => w.state() === 'available');
|
||||||
|
if (!availableWorkers.length) {
|
||||||
|
// Try getting another worker for given capabilities.
|
||||||
|
const node = nodes.find(w => w.canCreateWorker());
|
||||||
|
if (node)
|
||||||
|
node.createWorker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a match.
|
||||||
|
const worker = availableWorkers[0];
|
||||||
|
const clientRequest = this._clientRequests.shift()!;
|
||||||
|
worker.connect(clientRequest).then(result => {
|
||||||
|
if (result === 'workerError')
|
||||||
|
this._clientRequests.unshift(clientRequest);
|
||||||
|
this._makeAMatch();
|
||||||
|
}).catch(e => this._log(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _state(): string {
|
||||||
|
const lines = [this._nodes.size + ' Nodes(s)'];
|
||||||
|
for (const [nodeId, node] of this._nodes) {
|
||||||
|
lines.push(` node ${nodeId}`);
|
||||||
|
for (const [workerId, worker] of node._workers)
|
||||||
|
lines.push(` ${workerId} - ${JSON.stringify(worker.debugInfo())}`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
const url = await this._server.start(PORT);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Server is listening on: ' + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGuid(): string {
|
||||||
|
return crypto.randomBytes(16).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const proxy = new ProxyServer();
|
||||||
|
await proxy.start();
|
||||||
|
})();
|
||||||
67
packages/playwright-grid/src/node/node.ts
Normal file
67
packages/playwright-grid/src/node/node.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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 WebSocket from 'ws';
|
||||||
|
import child_process from 'child_process';
|
||||||
|
import debug from 'debug';
|
||||||
|
import type { Capabilities } from '../common/capabilities';
|
||||||
|
|
||||||
|
const log = debug('pw:grid:node');
|
||||||
|
|
||||||
|
const endpoint = process.env.PLAYWRIGHT_GRID_ENDPOINT || 'ws://localhost:3113';
|
||||||
|
const capacity = parseInt(process.env.PLAYWRIGHT_GRID_NODE_CAPACITY || '1', 10);
|
||||||
|
const caps: Capabilities = {
|
||||||
|
platform: process.platform,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
workerSeq = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
log('node created');
|
||||||
|
const ws = new WebSocket(endpoint + `/registerNode?capacity=${capacity}&caps=${JSON.stringify(caps)}`);
|
||||||
|
let nodeId = '';
|
||||||
|
ws.on('error', error => {
|
||||||
|
log(error);
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
ws.on('message', data => {
|
||||||
|
const text = data.toString();
|
||||||
|
const message = JSON.parse(text);
|
||||||
|
if (message.nodeId) {
|
||||||
|
nodeId = message.nodeId;
|
||||||
|
log('node id', nodeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workerId = message.workerId;
|
||||||
|
log('worked requested', workerId);
|
||||||
|
child_process.fork(require.resolve('./worker.js'), {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PLAYWRIGHT_GRID_NODE_ID: nodeId,
|
||||||
|
PLAYWRIGHT_GRID_WORKER_ID: workerId,
|
||||||
|
PLAYWRIGHT_GRID_ENDPOINT: endpoint,
|
||||||
|
},
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
ws.on('close', () => process.exit(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Node();
|
||||||
66
packages/playwright-grid/src/node/worker.ts
Normal file
66
packages/playwright-grid/src/node/worker.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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 debug from 'debug';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import { DispatcherConnection, RootDispatcher, PlaywrightDispatcher, createPlaywright, serverSideCallMetadata } from 'playwright-core/lib/server';
|
||||||
|
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
|
const workerId = process.env.PLAYWRIGHT_GRID_WORKER_ID!;
|
||||||
|
const log = debug('pw:grid:browser@' + workerId);
|
||||||
|
|
||||||
|
class Worker {
|
||||||
|
constructor() {
|
||||||
|
log('worker created');
|
||||||
|
const dispatcherConnection = new DispatcherConnection();
|
||||||
|
let browserName: 'chromium' | 'webkit' | 'firefox';
|
||||||
|
let launchOptions: any;
|
||||||
|
|
||||||
|
const ws = new WebSocket(process.env.PLAYWRIGHT_GRID_ENDPOINT + `/registerWorker?nodeId=${process.env.PLAYWRIGHT_GRID_NODE_ID}&workerId=${workerId}`);
|
||||||
|
dispatcherConnection.onmessage = message => ws.send(JSON.stringify(message));
|
||||||
|
ws.on('upgrade', response => {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
for (let i = 0; i < response.rawHeaders.length; i += 2)
|
||||||
|
headers[response.rawHeaders[i]] = response.rawHeaders[i + 1];
|
||||||
|
|
||||||
|
browserName = headers['x-playwright-browser'] as any || 'chromium';
|
||||||
|
launchOptions = JSON.parse(headers['x-playwright-launch-options'] || '{}');
|
||||||
|
log('browserName', browserName);
|
||||||
|
log('launchOptions', launchOptions);
|
||||||
|
});
|
||||||
|
ws.once('open', () => {
|
||||||
|
log('worker opened');
|
||||||
|
new RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => {
|
||||||
|
const playwright = createPlaywright({ sdkLanguage });
|
||||||
|
const browser = await playwright[browserName].launch(serverSideCallMetadata(), launchOptions);
|
||||||
|
return new PlaywrightDispatcher(rootScope, playwright, undefined, browser);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ws.on('message', message => dispatcherConnection.dispatch(JSON.parse(message.toString())));
|
||||||
|
ws.on('error', error => {
|
||||||
|
log('socket error');
|
||||||
|
dispatcherConnection.onmessage = () => {};
|
||||||
|
gracefullyProcessExitDoNotHang(0);
|
||||||
|
});
|
||||||
|
ws.on('close', async () => {
|
||||||
|
log('worker deleted');
|
||||||
|
dispatcherConnection.onmessage = () => {};
|
||||||
|
gracefullyProcessExitDoNotHang(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Worker();
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import { start } from '../../packages/playwright-core/lib/outofprocess';
|
import { start } from '../../packages/playwright-core/lib/outofprocess';
|
||||||
import type { Playwright } from '../../packages/playwright-core/lib/client/playwright';
|
import type { Playwright } from '../../packages/playwright-core/lib/client/playwright';
|
||||||
|
|
||||||
export type TestModeName = 'default' | 'driver' | 'service' | 'service2';
|
export type TestModeName = 'default' | 'driver' | 'service' | 'service2' | 'service-grid';
|
||||||
|
|
||||||
interface TestMode {
|
interface TestMode {
|
||||||
setup(): Promise<Playwright>;
|
setup(): Promise<Playwright>;
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,11 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
|
||||||
mode: ['default', { scope: 'worker', option: true }],
|
mode: ['default', { scope: 'worker', option: true }],
|
||||||
playwright: [async ({ mode }, run) => {
|
playwright: [async ({ mode }, run) => {
|
||||||
const testMode = {
|
const testMode = {
|
||||||
default: new DefaultTestMode(),
|
'default': new DefaultTestMode(),
|
||||||
service: new DefaultTestMode(),
|
'service': new DefaultTestMode(),
|
||||||
service2: new DefaultTestMode(),
|
'service2': new DefaultTestMode(),
|
||||||
driver: new DriverTestMode(),
|
'service-grid': new DefaultTestMode(),
|
||||||
|
'driver': new DriverTestMode(),
|
||||||
}[mode];
|
}[mode];
|
||||||
require('playwright-core/lib/utils').setUnderTest();
|
require('playwright-core/lib/utils').setUnderTest();
|
||||||
const playwright = await testMode.setup();
|
const playwright = await testMode.setup();
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ import { browserTest as it, expect } from '../../config/browserTest';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
it.skip(({ mode }) => mode === 'service2', 'Fixed in v1.37');
|
|
||||||
|
|
||||||
it('should output a trace', async ({ browser, server }, testInfo) => {
|
it('should output a trace', async ({ browser, server }, testInfo) => {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
const outputTraceFile = testInfo.outputPath(path.join(`trace.json`));
|
const outputTraceFile = testInfo.outputPath(path.join(`trace.json`));
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,16 @@ const os: 'linux' | 'windows' = (process.env.PLAYWRIGHT_SERVICE_OS as 'linux' |
|
||||||
const runId = process.env.PLAYWRIGHT_SERVICE_RUN_ID || new Date().toISOString(); // name the test run
|
const runId = process.env.PLAYWRIGHT_SERVICE_RUN_ID || new Date().toISOString(); // name the test run
|
||||||
|
|
||||||
let connectOptions: any;
|
let connectOptions: any;
|
||||||
if (mode === 'service')
|
let webServer: any;
|
||||||
|
|
||||||
|
if (mode === 'service') {
|
||||||
connectOptions = { wsEndpoint: 'ws://localhost:3333/' };
|
connectOptions = { wsEndpoint: 'ws://localhost:3333/' };
|
||||||
|
webServer = {
|
||||||
|
command: 'npx playwright run-server --port=3333',
|
||||||
|
url: 'http://localhost:3333',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (mode === 'service2') {
|
if (mode === 'service2') {
|
||||||
process.env.PW_VERSION_OVERRIDE = '1.37';
|
process.env.PW_VERSION_OVERRIDE = '1.37';
|
||||||
connectOptions = {
|
connectOptions = {
|
||||||
|
|
@ -68,6 +76,36 @@ if (mode === 'service2') {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode === 'service-grid') {
|
||||||
|
connectOptions = {
|
||||||
|
wsEndpoint: 'ws://localhost:3333/',
|
||||||
|
timeout: 60 * 60 * 1000,
|
||||||
|
};
|
||||||
|
webServer = [
|
||||||
|
{
|
||||||
|
command: 'node ../../packages/playwright-grid/cli.js grid',
|
||||||
|
url: 'http://localhost:3333',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
env: {
|
||||||
|
PLAYWRIGHT_GRID_PORT: '3333',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
command: 'node ../../packages/playwright-grid/cli.js node',
|
||||||
|
env: {
|
||||||
|
PLAYWRIGHT_GRID_ENDPOINT: 'ws://localhost:3333',
|
||||||
|
PLAYWRIGHT_GRID_NODE_CAPACITY: '2',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: 'node ../../packages/playwright-grid/cli.js node',
|
||||||
|
env: {
|
||||||
|
PLAYWRIGHT_GRID_ENDPOINT: 'ws://localhost:3333',
|
||||||
|
PLAYWRIGHT_GRID_NODE_CAPACITY: '2',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
||||||
testDir,
|
testDir,
|
||||||
outputDir,
|
outputDir,
|
||||||
|
|
@ -88,11 +126,7 @@ const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & Playwrigh
|
||||||
use: {
|
use: {
|
||||||
connectOptions,
|
connectOptions,
|
||||||
},
|
},
|
||||||
webServer: mode === 'service' ? {
|
webServer,
|
||||||
command: 'npx playwright run-server --port=3333',
|
|
||||||
url: 'http://localhost:3333',
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
} : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const browserNames = ['chromium', 'webkit', 'firefox'] as BrowserName[];
|
const browserNames = ['chromium', 'webkit', 'firefox'] as BrowserName[];
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,11 @@ const workspace = new Workspace(ROOT_PATH, [
|
||||||
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue2'),
|
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue2'),
|
||||||
files: ['LICENSE'],
|
files: ['LICENSE'],
|
||||||
}),
|
}),
|
||||||
|
new PWPackage({
|
||||||
|
name: '@playwright/experimental-grid',
|
||||||
|
path: path.join(ROOT_PATH, 'packages', 'playwright-grid'),
|
||||||
|
files: ['LICENSE'],
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue