Merge branch 'main' into mockingproxy-headers-only
This commit is contained in:
commit
34991a032f
|
|
@ -1,18 +0,0 @@
|
||||||
test/assets/modernizr.js
|
|
||||||
/tests/third_party/
|
|
||||||
/packages/*/lib/
|
|
||||||
*.js
|
|
||||||
/packages/playwright-core/src/generated/*
|
|
||||||
/packages/playwright-core/src/third_party/
|
|
||||||
/packages/playwright-core/types/*
|
|
||||||
/packages/playwright-ct-core/src/generated/*
|
|
||||||
/index.d.ts
|
|
||||||
node_modules/
|
|
||||||
**/*.d.ts
|
|
||||||
output/
|
|
||||||
test-results/
|
|
||||||
/tests/components/
|
|
||||||
/tests/installation/fixture-scripts/
|
|
||||||
DEPS
|
|
||||||
.cache/
|
|
||||||
/utils/
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: "./.eslintrc.js",
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 9,
|
|
||||||
sourceType: "module",
|
|
||||||
project: "./tsconfig.json",
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-base-to-string": "error",
|
|
||||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
project: "./tsconfig.json"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
137
.eslintrc.js
137
.eslintrc.js
|
|
@ -1,137 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
plugins: ["@typescript-eslint", "notice"],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 9,
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended"
|
|
||||||
],
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
react: { version: "18" }
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ESLint rules
|
|
||||||
*
|
|
||||||
* All available rules: http://eslint.org/docs/rules/
|
|
||||||
*
|
|
||||||
* Rules take the following form:
|
|
||||||
* "rule-name", [severity, { opts }]
|
|
||||||
* Severity: 2 == error, 1 == warning, 0 == off.
|
|
||||||
*/
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-unused-vars": [2, {args: "none"}],
|
|
||||||
"@typescript-eslint/consistent-type-imports": [2, {disallowTypeAnnotations: false}],
|
|
||||||
/**
|
|
||||||
* Enforced rules
|
|
||||||
*/
|
|
||||||
// syntax preferences
|
|
||||||
"object-curly-spacing": ["error", "always"],
|
|
||||||
"quotes": [2, "single", {
|
|
||||||
"avoidEscape": true,
|
|
||||||
"allowTemplateLiterals": true
|
|
||||||
}],
|
|
||||||
"jsx-quotes": [2, "prefer-single"],
|
|
||||||
"no-extra-semi": 2,
|
|
||||||
"@typescript-eslint/semi": [2],
|
|
||||||
"comma-style": [2, "last"],
|
|
||||||
"wrap-iife": [2, "inside"],
|
|
||||||
"spaced-comment": [2, "always", {
|
|
||||||
"markers": ["*"]
|
|
||||||
}],
|
|
||||||
"eqeqeq": [2],
|
|
||||||
"accessor-pairs": [2, {
|
|
||||||
"getWithoutSet": false,
|
|
||||||
"setWithoutGet": false
|
|
||||||
}],
|
|
||||||
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
|
|
||||||
"curly": [2, "multi-or-nest", "consistent"],
|
|
||||||
"new-parens": 2,
|
|
||||||
"arrow-parens": [2, "as-needed"],
|
|
||||||
"prefer-const": 2,
|
|
||||||
"quote-props": [2, "consistent"],
|
|
||||||
"nonblock-statement-body-position": [2, "below"],
|
|
||||||
|
|
||||||
// anti-patterns
|
|
||||||
"no-var": 2,
|
|
||||||
"no-with": 2,
|
|
||||||
"no-multi-str": 2,
|
|
||||||
"no-caller": 2,
|
|
||||||
"no-implied-eval": 2,
|
|
||||||
"no-labels": 2,
|
|
||||||
"no-new-object": 2,
|
|
||||||
"no-octal-escape": 2,
|
|
||||||
"no-self-compare": 2,
|
|
||||||
"no-shadow-restricted-names": 2,
|
|
||||||
"no-cond-assign": 2,
|
|
||||||
"no-debugger": 2,
|
|
||||||
"no-dupe-keys": 2,
|
|
||||||
"no-duplicate-case": 2,
|
|
||||||
"no-empty-character-class": 2,
|
|
||||||
"no-unreachable": 2,
|
|
||||||
"no-unsafe-negation": 2,
|
|
||||||
"radix": 2,
|
|
||||||
"valid-typeof": 2,
|
|
||||||
"no-implicit-globals": [2],
|
|
||||||
"no-unused-expressions": [2, { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true}],
|
|
||||||
"no-proto": 2,
|
|
||||||
|
|
||||||
// es2015 features
|
|
||||||
"require-yield": 2,
|
|
||||||
"template-curly-spacing": [2, "never"],
|
|
||||||
|
|
||||||
// spacing details
|
|
||||||
"space-infix-ops": 2,
|
|
||||||
"space-in-parens": [2, "never"],
|
|
||||||
"array-bracket-spacing": [2, "never"],
|
|
||||||
"comma-spacing": [2, { "before": false, "after": true }],
|
|
||||||
"keyword-spacing": [2, "always"],
|
|
||||||
"space-before-function-paren": [2, {
|
|
||||||
"anonymous": "never",
|
|
||||||
"named": "never",
|
|
||||||
"asyncArrow": "always"
|
|
||||||
}],
|
|
||||||
"no-whitespace-before-property": 2,
|
|
||||||
"keyword-spacing": [2, {
|
|
||||||
"overrides": {
|
|
||||||
"if": {"after": true},
|
|
||||||
"else": {"after": true},
|
|
||||||
"for": {"after": true},
|
|
||||||
"while": {"after": true},
|
|
||||||
"do": {"after": true},
|
|
||||||
"switch": {"after": true},
|
|
||||||
"return": {"after": true}
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"arrow-spacing": [2, {
|
|
||||||
"after": true,
|
|
||||||
"before": true
|
|
||||||
}],
|
|
||||||
"@typescript-eslint/func-call-spacing": 2,
|
|
||||||
"@typescript-eslint/type-annotation-spacing": 2,
|
|
||||||
|
|
||||||
// file whitespace
|
|
||||||
"no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 0}],
|
|
||||||
"no-mixed-spaces-and-tabs": 2,
|
|
||||||
"no-trailing-spaces": 2,
|
|
||||||
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
|
|
||||||
"indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }],
|
|
||||||
"key-spacing": [2, {
|
|
||||||
"beforeColon": false
|
|
||||||
}],
|
|
||||||
"eol-last": 2,
|
|
||||||
|
|
||||||
// copyright
|
|
||||||
"notice/notice": [2, {
|
|
||||||
"mustMatch": "Copyright",
|
|
||||||
"templateFile": require("path").join(__dirname, "utils", "copyright.js"),
|
|
||||||
}],
|
|
||||||
|
|
||||||
// react
|
|
||||||
"react/react-in-jsx-scope": 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -909,3 +909,9 @@ Returns storage state for this request context, contains current cookies and loc
|
||||||
|
|
||||||
### option: APIRequestContext.storageState.path = %%-storagestate-option-path-%%
|
### option: APIRequestContext.storageState.path = %%-storagestate-option-path-%%
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
|
|
||||||
|
### option: APIRequestContext.storageState.indexedDB
|
||||||
|
* since: v1.51
|
||||||
|
- `indexedDB` ?<boolean>
|
||||||
|
|
||||||
|
Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
||||||
|
|
|
||||||
|
|
@ -1533,6 +1533,10 @@ Whether to emulate network being offline for the browser context.
|
||||||
|
|
||||||
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
|
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
IndexedDBs with typed arrays are currently not supported.
|
||||||
|
:::
|
||||||
|
|
||||||
## async method: BrowserContext.storageState
|
## async method: BrowserContext.storageState
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
* langs: csharp, java
|
* langs: csharp, java
|
||||||
|
|
@ -1541,6 +1545,12 @@ Returns storage state for this browser context, contains current cookies, local
|
||||||
### option: BrowserContext.storageState.path = %%-storagestate-option-path-%%
|
### option: BrowserContext.storageState.path = %%-storagestate-option-path-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
|
### option: BrowserContext.storageState.indexedDB
|
||||||
|
* since: v1.51
|
||||||
|
- `indexedDB` ?<boolean>
|
||||||
|
|
||||||
|
Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
||||||
|
|
||||||
## property: BrowserContext.tracing
|
## property: BrowserContext.tracing
|
||||||
* since: v1.12
|
* since: v1.12
|
||||||
- type: <[Tracing]>
|
- type: <[Tracing]>
|
||||||
|
|
|
||||||
95
eslint-react.config.mjs
Normal file
95
eslint-react.config.mjs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* 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 { fixupConfigRules } from '@eslint/compat';
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import notice from 'eslint-plugin-notice';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import stylistic from '@stylistic/eslint-plugin';
|
||||||
|
import { baseRules } from './eslint.config.mjs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseConfig = fixupConfigRules(compat.extends('plugin:react/recommended', 'plugin:react-hooks/recommended'));
|
||||||
|
|
||||||
|
const plugins = {
|
||||||
|
'@stylistic': stylistic,
|
||||||
|
'@typescript-eslint': typescriptEslint,
|
||||||
|
notice,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ignores = [
|
||||||
|
'.github/',
|
||||||
|
'*.js',
|
||||||
|
'**/.cache/',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/dist/**',
|
||||||
|
'index.d.ts',
|
||||||
|
'node_modules/',
|
||||||
|
'output/',
|
||||||
|
'packages/*/lib/',
|
||||||
|
'test-results/',
|
||||||
|
'tests/',
|
||||||
|
'utils/',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores },
|
||||||
|
{
|
||||||
|
plugins,
|
||||||
|
settings: {
|
||||||
|
react: { version: 'detect' },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...baseConfig,
|
||||||
|
packageSection('html-reporter'),
|
||||||
|
packageSection('recorder'),
|
||||||
|
packageSection('trace-viewer'),
|
||||||
|
];
|
||||||
|
|
||||||
|
function packageSection(packageName) {
|
||||||
|
return {
|
||||||
|
files: [
|
||||||
|
`packages/${packageName}/src/**/*.ts`,
|
||||||
|
`packages/${packageName}/src/**/*.tsx`,
|
||||||
|
`packages/web/src/**/*.ts`,
|
||||||
|
`packages/web/src/**/*.tsx`,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 9,
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
project: path.join(__dirname, 'packages', packageName, 'tsconfig.json'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...baseRules,
|
||||||
|
'no-console': 2,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
252
eslint.config.mjs
Normal file
252
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
/**
|
||||||
|
* 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 typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import notice from 'eslint-plugin-notice';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import stylistic from '@stylistic/eslint-plugin';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const plugins = {
|
||||||
|
'@stylistic': stylistic,
|
||||||
|
'@typescript-eslint': typescriptEslint,
|
||||||
|
notice,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ignores = [
|
||||||
|
'.github/',
|
||||||
|
'*.js',
|
||||||
|
'**/.cache/',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'index.d.ts',
|
||||||
|
'node_modules/',
|
||||||
|
'output/',
|
||||||
|
'packages/*/lib/',
|
||||||
|
'packages/html-reporter/**',
|
||||||
|
'packages/playwright-core/src/generated/*',
|
||||||
|
'packages/playwright-core/src/third_party/',
|
||||||
|
'packages/playwright-core/types/*',
|
||||||
|
'packages/playwright-ct-core/src/generated/*',
|
||||||
|
'packages/recorder/**',
|
||||||
|
'packages/trace-viewer/**',
|
||||||
|
'packages/web/**',
|
||||||
|
'test-results/',
|
||||||
|
'tests/assets/',
|
||||||
|
'tests/components/',
|
||||||
|
'tests/installation/fixture-scripts/',
|
||||||
|
'tests/third_party/',
|
||||||
|
'utils/',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const baseRules = {
|
||||||
|
'@typescript-eslint/no-unused-vars': [2, { args: 'none', caughtErrors: 'none' }],
|
||||||
|
'@typescript-eslint/consistent-type-imports': [2, { disallowTypeAnnotations: false }],
|
||||||
|
/**
|
||||||
|
* Enforced rules
|
||||||
|
*/
|
||||||
|
// syntax preferences
|
||||||
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'quotes': [2, 'single', {
|
||||||
|
'avoidEscape': true,
|
||||||
|
'allowTemplateLiterals': true
|
||||||
|
}],
|
||||||
|
'jsx-quotes': [2, 'prefer-single'],
|
||||||
|
'no-extra-semi': 2,
|
||||||
|
'@stylistic/semi': [2],
|
||||||
|
'comma-style': [2, 'last'],
|
||||||
|
'wrap-iife': [2, 'inside'],
|
||||||
|
'spaced-comment': [2, 'always', {
|
||||||
|
'markers': ['*']
|
||||||
|
}],
|
||||||
|
'eqeqeq': [2],
|
||||||
|
'accessor-pairs': [2, {
|
||||||
|
'getWithoutSet': false,
|
||||||
|
'setWithoutGet': false
|
||||||
|
}],
|
||||||
|
'brace-style': [2, '1tbs', { 'allowSingleLine': true }],
|
||||||
|
'curly': [2, 'multi-or-nest', 'consistent'],
|
||||||
|
'new-parens': 2,
|
||||||
|
'arrow-parens': [2, 'as-needed'],
|
||||||
|
'prefer-const': 2,
|
||||||
|
'quote-props': [2, 'consistent'],
|
||||||
|
'nonblock-statement-body-position': [2, 'below'],
|
||||||
|
|
||||||
|
// anti-patterns
|
||||||
|
'no-var': 2,
|
||||||
|
'no-with': 2,
|
||||||
|
'no-multi-str': 2,
|
||||||
|
'no-caller': 2,
|
||||||
|
'no-implied-eval': 2,
|
||||||
|
'no-labels': 2,
|
||||||
|
'no-new-object': 2,
|
||||||
|
'no-octal-escape': 2,
|
||||||
|
'no-self-compare': 2,
|
||||||
|
'no-shadow-restricted-names': 2,
|
||||||
|
'no-cond-assign': 2,
|
||||||
|
'no-debugger': 2,
|
||||||
|
'no-dupe-keys': 2,
|
||||||
|
'no-duplicate-case': 2,
|
||||||
|
'no-empty-character-class': 2,
|
||||||
|
'no-unreachable': 2,
|
||||||
|
'no-unsafe-negation': 2,
|
||||||
|
'radix': 2,
|
||||||
|
'valid-typeof': 2,
|
||||||
|
'no-implicit-globals': [2],
|
||||||
|
'no-unused-expressions': [2, { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||||
|
'no-proto': 2,
|
||||||
|
|
||||||
|
// es2015 features
|
||||||
|
'require-yield': 2,
|
||||||
|
'template-curly-spacing': [2, 'never'],
|
||||||
|
|
||||||
|
// spacing details
|
||||||
|
'space-infix-ops': 2,
|
||||||
|
'space-in-parens': [2, 'never'],
|
||||||
|
'array-bracket-spacing': [2, 'never'],
|
||||||
|
'comma-spacing': [2, { 'before': false, 'after': true }],
|
||||||
|
'keyword-spacing': [2, 'always'],
|
||||||
|
'space-before-function-paren': [2, {
|
||||||
|
'anonymous': 'never',
|
||||||
|
'named': 'never',
|
||||||
|
'asyncArrow': 'always'
|
||||||
|
}],
|
||||||
|
'no-whitespace-before-property': 2,
|
||||||
|
'keyword-spacing': [2, {
|
||||||
|
'overrides': {
|
||||||
|
'if': { 'after': true },
|
||||||
|
'else': { 'after': true },
|
||||||
|
'for': { 'after': true },
|
||||||
|
'while': { 'after': true },
|
||||||
|
'do': { 'after': true },
|
||||||
|
'switch': { 'after': true },
|
||||||
|
'return': { 'after': true }
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
'arrow-spacing': [2, {
|
||||||
|
'after': true,
|
||||||
|
'before': true
|
||||||
|
}],
|
||||||
|
'@stylistic/func-call-spacing': 2,
|
||||||
|
'@stylistic/type-annotation-spacing': 2,
|
||||||
|
|
||||||
|
// file whitespace
|
||||||
|
'no-multiple-empty-lines': [2, { 'max': 2, 'maxEOF': 0 }],
|
||||||
|
'no-mixed-spaces-and-tabs': 2,
|
||||||
|
'no-trailing-spaces': 2,
|
||||||
|
'linebreak-style': [process.platform === 'win32' ? 0 : 2, 'unix'],
|
||||||
|
'indent': [2, 2, { 'SwitchCase': 1, 'CallExpression': { 'arguments': 2 }, 'MemberExpression': 2 }],
|
||||||
|
'key-spacing': [2, {
|
||||||
|
'beforeColon': false
|
||||||
|
}],
|
||||||
|
'eol-last': 2,
|
||||||
|
|
||||||
|
// copyright
|
||||||
|
'notice/notice': [2, {
|
||||||
|
'mustMatch': 'Copyright',
|
||||||
|
'templateFile': path.join(__dirname, 'utils', 'copyright.js'),
|
||||||
|
}],
|
||||||
|
|
||||||
|
// react
|
||||||
|
'react/react-in-jsx-scope': 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const noFloatingPromisesRules = {
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const noBooleanCompareRules = {
|
||||||
|
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const noRestrictedGlobalsRules = {
|
||||||
|
'no-restricted-globals': [
|
||||||
|
'error',
|
||||||
|
{ 'name': 'window' },
|
||||||
|
{ 'name': 'document' },
|
||||||
|
{ 'name': 'globalThis' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const languageOptions = {
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 9,
|
||||||
|
sourceType: 'module',
|
||||||
|
};
|
||||||
|
|
||||||
|
const languageOptionsWithTsConfig = {
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 9,
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
project: path.join(__dirname, 'tsconfig.json'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
ignores,
|
||||||
|
}, {
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
plugins,
|
||||||
|
languageOptions,
|
||||||
|
rules: baseRules,
|
||||||
|
}, {
|
||||||
|
files: ['packages/**/*.ts'],
|
||||||
|
languageOptions: languageOptionsWithTsConfig,
|
||||||
|
rules: {
|
||||||
|
'no-console': 2,
|
||||||
|
'no-restricted-properties': [2, {
|
||||||
|
'object': 'process',
|
||||||
|
'property': 'exit',
|
||||||
|
'message': 'Please use gracefullyProcessExitDoNotHang function to exit the process.',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
files: ['packages/playwright/**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
...noFloatingPromisesRules,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
files: ['packages/playwright/src/reporters/**/*.ts'],
|
||||||
|
languageOptions: languageOptionsWithTsConfig,
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
files: ['packages/playwright-core/src/server/injected/**/*.ts'],
|
||||||
|
languageOptions: languageOptionsWithTsConfig,
|
||||||
|
rules: {
|
||||||
|
...noRestrictedGlobalsRules,
|
||||||
|
...noFloatingPromisesRules,
|
||||||
|
...noBooleanCompareRules,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
files: ['tests/**/*.spec.js', 'tests/**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 9,
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
project: path.join(__dirname, 'tests', 'tsconfig.json'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...noFloatingPromisesRules,
|
||||||
|
}
|
||||||
|
}];
|
||||||
1875
package-lock.json
generated
1875
package-lock.json
generated
File diff suppressed because it is too large
Load diff
22
package.json
22
package.json
|
|
@ -30,7 +30,7 @@
|
||||||
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
|
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
|
||||||
"ct": "playwright test tests/components/test-all.spec.js --reporter=list",
|
"ct": "playwright test tests/components/test-all.spec.js --reporter=list",
|
||||||
"test": "playwright test --config=tests/library/playwright.config.ts",
|
"test": "playwright test --config=tests/library/playwright.config.ts",
|
||||||
"eslint": "eslint --cache --report-unused-disable-directives --ext ts,tsx,js,jsx,mjs .",
|
"eslint": "eslint --cache && eslint -c eslint-react.config.mjs",
|
||||||
"tsc": "tsc -p . && tsc -p packages/html-reporter/",
|
"tsc": "tsc -p . && tsc -p packages/html-reporter/",
|
||||||
"build-installer": "babel -s --extensions \".ts\" --out-dir packages/playwright-core/lib/utils/ packages/playwright-core/src/utils",
|
"build-installer": "babel -s --extensions \".ts\" --out-dir packages/playwright-core/lib/utils/ packages/playwright-core/src/utils",
|
||||||
"doc": "node utils/doclint/cli.js",
|
"doc": "node utils/doclint/cli.js",
|
||||||
|
|
@ -62,6 +62,10 @@
|
||||||
"@babel/plugin-transform-optional-chaining": "^7.23.4",
|
"@babel/plugin-transform-optional-chaining": "^7.23.4",
|
||||||
"@babel/plugin-transform-typescript": "^7.23.6",
|
"@babel/plugin-transform-typescript": "^7.23.6",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.23.3",
|
||||||
|
"@eslint/compat": "^1.2.6",
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.19.0",
|
||||||
|
"@stylistic/eslint-plugin": "^3.0.1",
|
||||||
"@types/babel__core": "^7.20.2",
|
"@types/babel__core": "^7.20.2",
|
||||||
"@types/codemirror": "^5.60.7",
|
"@types/codemirror": "^5.60.7",
|
||||||
"@types/formidable": "^2.0.4",
|
"@types/formidable": "^2.0.4",
|
||||||
|
|
@ -71,9 +75,9 @@
|
||||||
"@types/react-dom": "^18.0.5",
|
"@types/react-dom": "^18.0.5",
|
||||||
"@types/ws": "^8.5.3",
|
"@types/ws": "^8.5.3",
|
||||||
"@types/xml2js": "^0.4.9",
|
"@types/xml2js": "^0.4.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
||||||
"@typescript-eslint/parser": "^7.15.0",
|
"@typescript-eslint/parser": "^8.23.0",
|
||||||
"@typescript-eslint/utils": "^7.15.0",
|
"@typescript-eslint/utils": "^8.23.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@zip.js/zip.js": "^2.7.29",
|
"@zip.js/zip.js": "^2.7.29",
|
||||||
|
|
@ -86,10 +90,10 @@
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"electron": "^30.1.2",
|
"electron": "^30.1.2",
|
||||||
"esbuild": "^0.18.11",
|
"esbuild": "^0.18.11",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-notice": "^0.9.10",
|
"eslint-plugin-notice": "^1.0.0",
|
||||||
"eslint-plugin-react": "^7.35.0",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"immutable": "^4.3.7",
|
"immutable": "^4.3.7",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
|
|
@ -98,7 +102,7 @@
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"ssim.js": "^3.5.0",
|
"ssim.js": "^3.5.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.4.14",
|
"vite": "^5.4.14",
|
||||||
"ws": "^8.17.1",
|
"ws": "^8.17.1",
|
||||||
"xml2js": "^0.5.0",
|
"xml2js": "^0.5.0",
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"extends": "../.eslintrc.js",
|
|
||||||
/**
|
|
||||||
* ESLint rules
|
|
||||||
*
|
|
||||||
* All available rules: http://eslint.org/docs/rules/
|
|
||||||
*
|
|
||||||
* Rules take the following form:
|
|
||||||
* "rule-name", [severity, { opts }]
|
|
||||||
* Severity: 2 == error, 1 == warning, 0 == off.
|
|
||||||
*/
|
|
||||||
"rules": {
|
|
||||||
"no-console": 2,
|
|
||||||
"no-debugger": 2,
|
|
||||||
"no-restricted-properties": [2, {
|
|
||||||
"object": "process",
|
|
||||||
"property": "exit",
|
|
||||||
"message": "Please use gracefullyProcessExitDoNotHang function to exit the process.",
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -95,7 +95,18 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
||||||
<div className='hbox m-2 mt-1'>
|
<div className='hbox m-2 mt-1'>
|
||||||
<div className='mr-1'>{author}</div>
|
<div className='mr-1'>{author}</div>
|
||||||
<div title={longTimestamp}> on {shortTimestamp}</div>
|
<div title={longTimestamp}> on {shortTimestamp}</div>
|
||||||
{info['ci.link'] && <><span className='mx-2'>·</span><a href={info['ci.link']} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>logs</a></>}
|
{info['ci.link'] && (
|
||||||
|
<>
|
||||||
|
<span className='mx-2'>·</span>
|
||||||
|
<a href={info['ci.link']} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{info['pull.link'] && (
|
||||||
|
<>
|
||||||
|
<span className='mx-2'>·</span>
|
||||||
|
<a href={info['pull.link']} target='_blank' rel='noopener noreferrer'>Pull Request</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!info['revision.link'] && <a href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
{!!info['revision.link'] && <a href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.test-case-run-duration {
|
||||||
|
color: var(--color-fg-subtle);
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.test-case-path {
|
.test-case-path {
|
||||||
flex: none;
|
flex: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ const testCase: TestCase = {
|
||||||
],
|
],
|
||||||
tags: [],
|
tags: [],
|
||||||
outcome: 'expected',
|
outcome: 'expected',
|
||||||
duration: 10,
|
duration: 200,
|
||||||
ok: true,
|
ok: true,
|
||||||
results: [result]
|
results: [result]
|
||||||
};
|
};
|
||||||
|
|
@ -215,3 +215,37 @@ test('should correctly render prev and next', async ({ mount }) => {
|
||||||
- text: "My test test.spec.ts:42 10ms"
|
- text: "My test test.spec.ts:42 10ms"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const testCaseWithTwoAttempts: TestCase = {
|
||||||
|
...testCase,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
...result,
|
||||||
|
errors: ['Error message'],
|
||||||
|
status: 'failed',
|
||||||
|
duration: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...result,
|
||||||
|
duration: 150,
|
||||||
|
status: 'passed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('total duration is selected run duration', async ({ mount, page }) => {
|
||||||
|
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCaseWithTwoAttempts} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||||
|
await expect(component).toMatchAriaSnapshot(`
|
||||||
|
- text: "My test test.spec.ts:42 200ms"
|
||||||
|
- text: "Run 50ms Retry #1 150ms"
|
||||||
|
`);
|
||||||
|
await page.locator('.tabbed-pane-tab-label', { hasText: 'Run50ms' }).click();
|
||||||
|
await expect(component).toMatchAriaSnapshot(`
|
||||||
|
- text: "My test test.spec.ts:42 200ms"
|
||||||
|
`);
|
||||||
|
await page.locator('.tabbed-pane-tab-label', { hasText: 'Retry #1150ms' }).click();
|
||||||
|
await expect(component).toMatchAriaSnapshot(`
|
||||||
|
- text: "My test test.spec.ts:42 200ms"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,10 @@ export const TestCaseView: React.FC<{
|
||||||
{test && <TabbedPane tabs={
|
{test && <TabbedPane tabs={
|
||||||
test.results.map((result, index) => ({
|
test.results.map((result, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
title: <div style={{ display: 'flex', alignItems: 'center' }}>{statusIcon(result.status)} {retryLabel(index)}</div>,
|
title: <div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
{statusIcon(result.status)} {retryLabel(index)}
|
||||||
|
{(test.results.length > 1) && <span className='test-case-run-duration'>{msToString(result.duration)}</span>}
|
||||||
|
</div>,
|
||||||
render: () => <TestResultView test={test!} result={result} />
|
render: () => <TestResultView test={test!} result={result} />
|
||||||
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
|
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,12 @@
|
||||||
"@protocol/*": ["../protocol/src/*"],
|
"@protocol/*": ["../protocol/src/*"],
|
||||||
"@web/*": ["../web/src/*"],
|
"@web/*": ["../web/src/*"],
|
||||||
"@playwright/*": ["../playwright/src/*"],
|
"@playwright/*": ["../playwright/src/*"],
|
||||||
|
"@recorder/*": ["../recorder/src/*"],
|
||||||
"@testIsomorphic/*": ["../playwright/src/isomorphic/*"],
|
"@testIsomorphic/*": ["../playwright/src/isomorphic/*"],
|
||||||
"playwright-core/lib/*": ["../playwright-core/src/*"],
|
"playwright-core/lib/*": ["../playwright-core/src/*"],
|
||||||
"playwright/lib/*": ["../playwright/src/*"],
|
"playwright/lib/*": ["../playwright/src/*"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "../web/src"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: "../../.eslintrc-with-ts-config.js",
|
|
||||||
};
|
|
||||||
|
|
@ -7,12 +7,24 @@
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "133.0.6943.35"
|
"browserVersion": "133.0.6943.35"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "chromium-headless-shell",
|
||||||
|
"revision": "1157",
|
||||||
|
"installByDefault": true,
|
||||||
|
"browserVersion": "133.0.6943.35"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
"revision": "1300",
|
"revision": "1300",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "134.0.6998.0"
|
"browserVersion": "134.0.6998.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "chromium-tip-of-tree-headless-shell",
|
||||||
|
"revision": "1300",
|
||||||
|
"installByDefault": false,
|
||||||
|
"browserVersion": "134.0.6998.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
"revision": "1474",
|
"revision": "1474",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ const semver = currentNodeVersion.split('.');
|
||||||
const [major] = [+semver[0]];
|
const [major] = [+semver[0]];
|
||||||
|
|
||||||
if (major < minimumMajorNodeVersion) {
|
if (major < minimumMajorNodeVersion) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(
|
console.error(
|
||||||
'You are running Node.js ' +
|
'You are running Node.js ' +
|
||||||
currentNodeVersion +
|
currentNodeVersion +
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
||||||
}
|
}
|
||||||
|
|
||||||
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
|
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
|
||||||
options = { ...this._browserType._defaultContextOptions, ...options };
|
options = { ...this._browserType._playwright._defaultContextOptions, ...options };
|
||||||
const contextOptions = await prepareBrowserContextParams(options, this._browserType);
|
const contextOptions = await prepareBrowserContextParams(options, this._browserType);
|
||||||
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||||
const context = BrowserContext.from(response.context);
|
const context = BrowserContext.from(response.context);
|
||||||
|
|
|
||||||
|
|
@ -424,8 +424,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageState(options: { path?: string } = {}): Promise<StorageState> {
|
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||||
const state = await this._channel.storageState();
|
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||||
if (options.path) {
|
if (options.path) {
|
||||||
await mkdirIfNeeded(options.path);
|
await mkdirIfNeeded(options.path);
|
||||||
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import type * as channels from '@protocol/channels';
|
||||||
import { Browser } from './browser';
|
import { Browser } from './browser';
|
||||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import type { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions, BrowserContextOptions, Logger } from './types';
|
import type { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions, Logger } from './types';
|
||||||
import { Connection } from './connection';
|
import { Connection } from './connection';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
@ -45,12 +45,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
_contexts = new Set<BrowserContext>();
|
_contexts = new Set<BrowserContext>();
|
||||||
_playwright!: Playwright;
|
_playwright!: Playwright;
|
||||||
|
|
||||||
// Instrumentation.
|
|
||||||
_defaultContextOptions?: BrowserContextOptions;
|
|
||||||
_defaultContextTimeout?: number;
|
|
||||||
_defaultContextNavigationTimeout?: number;
|
|
||||||
private _defaultLaunchOptions?: LaunchOptions;
|
|
||||||
|
|
||||||
static from(browserType: channels.BrowserTypeChannel): BrowserType {
|
static from(browserType: channels.BrowserTypeChannel): BrowserType {
|
||||||
return (browserType as any)._object;
|
return (browserType as any)._object;
|
||||||
}
|
}
|
||||||
|
|
@ -69,8 +63,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||||
|
|
||||||
const logger = options.logger || this._defaultLaunchOptions?.logger;
|
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||||
options = { ...this._defaultLaunchOptions, ...options };
|
options = { ...this._playwright._defaultLaunchOptions, ...options };
|
||||||
const launchOptions: channels.BrowserTypeLaunchParams = {
|
const launchOptions: channels.BrowserTypeLaunchParams = {
|
||||||
...options,
|
...options,
|
||||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||||
|
|
@ -87,14 +81,14 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
async launchServer(options: LaunchServerOptions = {}): Promise<api.BrowserServer> {
|
async launchServer(options: LaunchServerOptions = {}): Promise<api.BrowserServer> {
|
||||||
if (!this._serverLauncher)
|
if (!this._serverLauncher)
|
||||||
throw new Error('Launching server is not supported');
|
throw new Error('Launching server is not supported');
|
||||||
options = { ...this._defaultLaunchOptions, ...options };
|
options = { ...this._playwright._defaultLaunchOptions, ...options };
|
||||||
return await this._serverLauncher.launchServer(options);
|
return await this._serverLauncher.launchServer(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
|
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
|
||||||
const logger = options.logger || this._defaultLaunchOptions?.logger;
|
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||||
options = { ...this._defaultLaunchOptions, ...this._defaultContextOptions, ...options };
|
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
|
||||||
const contextParams = await prepareBrowserContextParams(options, this);
|
const contextParams = await prepareBrowserContextParams(options, this);
|
||||||
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
||||||
...contextParams,
|
...contextParams,
|
||||||
|
|
@ -237,11 +231,10 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
context._browserType = this;
|
context._browserType = this;
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._setOptions(contextOptions, browserOptions);
|
context._setOptions(contextOptions, browserOptions);
|
||||||
if (this._defaultContextTimeout !== undefined)
|
if (this._playwright._defaultContextTimeout !== undefined)
|
||||||
context.setDefaultTimeout(this._defaultContextTimeout);
|
context.setDefaultTimeout(this._playwright._defaultContextTimeout);
|
||||||
if (this._defaultContextNavigationTimeout !== undefined)
|
if (this._playwright._defaultContextNavigationTimeout !== undefined)
|
||||||
context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
|
context.setDefaultNavigationTimeout(this._playwright._defaultContextNavigationTimeout);
|
||||||
|
|
||||||
await this._instrumentation.runAfterCreateBrowserContext(context);
|
await this._instrumentation.runAfterCreateBrowserContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import { assert, headersObjectToArray, isString } from '../utils';
|
||||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import { RawHeaders } from './network';
|
import { RawHeaders } from './network';
|
||||||
import type { ClientCertificate, FilePayload, Headers, StorageState } from './types';
|
import type { ClientCertificate, FilePayload, Headers, SetStorageState, StorageState } from './types';
|
||||||
import type { Playwright } from './playwright';
|
import type { Playwright } from './playwright';
|
||||||
import { Tracing } from './tracing';
|
import { Tracing } from './tracing';
|
||||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||||
|
|
@ -47,7 +47,7 @@ export type FetchOptions = {
|
||||||
|
|
||||||
export type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
export type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
||||||
extraHTTPHeaders?: Headers,
|
extraHTTPHeaders?: Headers,
|
||||||
storageState?: string | StorageState,
|
storageState?: string | SetStorageState,
|
||||||
clientCertificates?: ClientCertificate[];
|
clientCertificates?: ClientCertificate[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -57,9 +57,6 @@ export class APIRequest implements api.APIRequest {
|
||||||
private _playwright: Playwright;
|
private _playwright: Playwright;
|
||||||
readonly _contexts = new Set<APIRequestContext>();
|
readonly _contexts = new Set<APIRequestContext>();
|
||||||
|
|
||||||
// Instrumentation.
|
|
||||||
_defaultContextOptions?: NewContextOptions & { tracesDir?: string };
|
|
||||||
|
|
||||||
constructor(playwright: Playwright) {
|
constructor(playwright: Playwright) {
|
||||||
this._playwright = playwright;
|
this._playwright = playwright;
|
||||||
}
|
}
|
||||||
|
|
@ -69,22 +66,24 @@ export class APIRequest implements api.APIRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _newContext(options: NewContextOptions = {}, channel: channels.PlaywrightChannel | channels.LocalUtilsChannel): Promise<APIRequestContext> {
|
async _newContext(options: NewContextOptions = {}, channel: channels.PlaywrightChannel | channels.LocalUtilsChannel): Promise<APIRequestContext> {
|
||||||
options = { ...this._defaultContextOptions, ...options };
|
options = {
|
||||||
|
...this._playwright._defaultContextOptions,
|
||||||
|
timeout: this._playwright._defaultContextTimeout,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
const storageState = typeof options.storageState === 'string' ?
|
const storageState = typeof options.storageState === 'string' ?
|
||||||
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
||||||
options.storageState;
|
options.storageState;
|
||||||
// We do not expose tracesDir in the API, so do not allow options to accidentally override it.
|
|
||||||
const tracesDir = this._defaultContextOptions?.tracesDir;
|
|
||||||
const context = APIRequestContext.from((await channel.newRequest({
|
const context = APIRequestContext.from((await channel.newRequest({
|
||||||
...options,
|
...options,
|
||||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||||
storageState,
|
storageState,
|
||||||
tracesDir,
|
tracesDir: this._playwright._defaultLaunchOptions?.tracesDir, // We do not expose tracesDir in the API, so do not allow options to accidentally override it.
|
||||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||||
})).request);
|
})).request);
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._request = this;
|
context._request = this;
|
||||||
context._tracing._tracesDir = tracesDir;
|
context._tracing._tracesDir = this._playwright._defaultLaunchOptions?.tracesDir;
|
||||||
await context._instrumentation.runAfterCreateRequestContext(context);
|
await context._instrumentation.runAfterCreateRequestContext(context);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
@ -264,8 +263,8 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageState(options: { path?: string } = {}): Promise<StorageState> {
|
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||||
const state = await this._channel.storageState();
|
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||||
if (options.path) {
|
if (options.path) {
|
||||||
await mkdirIfNeeded(options.path);
|
await mkdirIfNeeded(options.path);
|
||||||
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||||
|
|
@ -420,8 +419,10 @@ function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
|
||||||
if (!map)
|
if (!map)
|
||||||
return undefined;
|
return undefined;
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const [name, value] of Object.entries(map))
|
for (const [name, value] of Object.entries(map)) {
|
||||||
result.push({ name, value: String(value) });
|
if (value !== undefined)
|
||||||
|
result.push({ name, value: String(value) });
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { Electron } from './electron';
|
||||||
import { APIRequest } from './fetch';
|
import { APIRequest } from './fetch';
|
||||||
import { Selectors, SelectorsOwner } from './selectors';
|
import { Selectors, SelectorsOwner } from './selectors';
|
||||||
import { MockingProxy } from './mockingProxy';
|
import { MockingProxy } from './mockingProxy';
|
||||||
|
import type { BrowserContextOptions, LaunchOptions } from 'playwright-core';
|
||||||
|
|
||||||
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
readonly _android: Android;
|
readonly _android: Android;
|
||||||
|
|
@ -38,6 +39,12 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||||
_mockingProxy?: MockingProxy;
|
_mockingProxy?: MockingProxy;
|
||||||
|
|
||||||
|
// Instrumentation.
|
||||||
|
_defaultLaunchOptions?: LaunchOptions;
|
||||||
|
_defaultContextOptions?: BrowserContextOptions;
|
||||||
|
_defaultContextTimeout?: number;
|
||||||
|
_defaultContextNavigationTimeout?: number;
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
this.request = new APIRequest(this);
|
this.request = new APIRequest(this);
|
||||||
|
|
@ -76,6 +83,19 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
return (channel as any)._object;
|
return (channel as any)._object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _browserTypes(): BrowserType[] {
|
||||||
|
return [this.chromium, this.firefox, this.webkit, this._bidiChromium, this._bidiFirefox];
|
||||||
|
}
|
||||||
|
|
||||||
|
_allContexts() {
|
||||||
|
return this._browserTypes().flatMap(type => [...type._contexts]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_allPages() {
|
||||||
|
return this._allContexts().flatMap(context => context.pages());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async _startMockingProxy() {
|
async _startMockingProxy() {
|
||||||
const requestContext = await this.request._newContext(undefined, this._connection.localUtils()._channel);
|
const requestContext = await this.request._newContext(undefined, this._connection.localUtils()._channel);
|
||||||
const result = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel });
|
const result = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel });
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,9 @@ scheme.APIRequestContextFetchLogParams = tObject({
|
||||||
scheme.APIRequestContextFetchLogResult = tObject({
|
scheme.APIRequestContextFetchLogResult = tObject({
|
||||||
log: tArray(tString),
|
log: tArray(tString),
|
||||||
});
|
});
|
||||||
scheme.APIRequestContextStorageStateParams = tOptional(tObject({}));
|
scheme.APIRequestContextStorageStateParams = tObject({
|
||||||
|
indexedDB: tOptional(tBoolean),
|
||||||
|
});
|
||||||
scheme.APIRequestContextStorageStateResult = tObject({
|
scheme.APIRequestContextStorageStateResult = tObject({
|
||||||
cookies: tArray(tType('NetworkCookie')),
|
cookies: tArray(tType('NetworkCookie')),
|
||||||
origins: tArray(tType('OriginStorage')),
|
origins: tArray(tType('OriginStorage')),
|
||||||
|
|
@ -1058,7 +1060,9 @@ scheme.BrowserContextSetOfflineParams = tObject({
|
||||||
offline: tBoolean,
|
offline: tBoolean,
|
||||||
});
|
});
|
||||||
scheme.BrowserContextSetOfflineResult = tOptional(tObject({}));
|
scheme.BrowserContextSetOfflineResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextStorageStateParams = tOptional(tObject({}));
|
scheme.BrowserContextStorageStateParams = tObject({
|
||||||
|
indexedDB: tOptional(tBoolean),
|
||||||
|
});
|
||||||
scheme.BrowserContextStorageStateResult = tObject({
|
scheme.BrowserContextStorageStateResult = tObject({
|
||||||
cookies: tArray(tType('NetworkCookie')),
|
cookies: tArray(tType('NetworkCookie')),
|
||||||
origins: tArray(tType('OriginStorage')),
|
origins: tArray(tType('OriginStorage')),
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,8 @@ export class RawMouseImpl implements input.RawMouse {
|
||||||
|
|
||||||
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
|
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
|
||||||
// Bidi throws when x/y are not integers.
|
// Bidi throws when x/y are not integers.
|
||||||
x = Math.round(x);
|
x = Math.floor(x);
|
||||||
y = Math.round(y);
|
y = Math.floor(y);
|
||||||
await this._performActions([{ type: 'pointerMove', x, y }]);
|
await this._performActions([{ type: 'pointerMove', x, y }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -508,14 +508,14 @@ export abstract class BrowserContext extends SdkObject implements network.Reques
|
||||||
this._origins.add(origin);
|
this._origins.add(origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageState(): Promise<channels.BrowserContextStorageStateResult> {
|
async storageState(indexedDB = true): Promise<channels.BrowserContextStorageStateResult> {
|
||||||
const result: channels.BrowserContextStorageStateResult = {
|
const result: channels.BrowserContextStorageStateResult = {
|
||||||
cookies: await this.cookies(),
|
cookies: await this.cookies(),
|
||||||
origins: []
|
origins: []
|
||||||
};
|
};
|
||||||
const originsToSave = new Set(this._origins);
|
const originsToSave = new Set(this._origins);
|
||||||
|
|
||||||
const collectScript = `(${storageScript.collect})((${utilityScriptSerializers.source})(), ${this._browser.options.name === 'firefox'})`;
|
const collectScript = `(${storageScript.collect})((${utilityScriptSerializers.source})(), ${this._browser.options.name === 'firefox'}, ${indexedDB})`;
|
||||||
|
|
||||||
// First try collecting storage stage from existing pages.
|
// First try collecting storage stage from existing pages.
|
||||||
for (const page of this.pages()) {
|
for (const page of this.pages()) {
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> {
|
async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> {
|
||||||
return await this._context.storageState();
|
return await this._context.storageState(params.indexedDB);
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise<void> {
|
async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,8 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
|
||||||
this.adopt(tracing);
|
this.adopt(tracing);
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageState(): Promise<channels.APIRequestContextStorageStateResult> {
|
async storageState(params: channels.APIRequestContextStorageStateParams): Promise<channels.APIRequestContextStorageStateResult> {
|
||||||
return this._object.storageState();
|
return this._object.storageState(params.indexedDB);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispose(params: channels.APIRequestContextDisposeParams, metadata: CallMetadata): Promise<void> {
|
async dispose(params: channels.APIRequestContextDisposeParams, metadata: CallMetadata): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
abstract _defaultOptions(): FetchRequestOptions;
|
abstract _defaultOptions(): FetchRequestOptions;
|
||||||
abstract _addCookies(cookies: channels.NetworkCookie[]): Promise<void>;
|
abstract _addCookies(cookies: channels.NetworkCookie[]): Promise<void>;
|
||||||
abstract _cookies(url: URL): Promise<channels.NetworkCookie[]>;
|
abstract _cookies(url: URL): Promise<channels.NetworkCookie[]>;
|
||||||
abstract storageState(): Promise<channels.APIRequestContextStorageStateResult>;
|
abstract storageState(indexedDB?: boolean): Promise<channels.APIRequestContextStorageStateResult>;
|
||||||
|
|
||||||
private _storeResponseBody(body: Buffer): string {
|
private _storeResponseBody(body: Buffer): string {
|
||||||
const uid = createGuid();
|
const uid = createGuid();
|
||||||
|
|
@ -618,8 +618,8 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
|
||||||
return await this._context.cookies(url.toString());
|
return await this._context.cookies(url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
override async storageState(): Promise<channels.APIRequestContextStorageStateResult> {
|
override async storageState(indexedDB?: boolean): Promise<channels.APIRequestContextStorageStateResult> {
|
||||||
return this._context.storageState();
|
return this._context.storageState(indexedDB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -684,10 +684,10 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
||||||
return this._cookieStore.cookies(url);
|
return this._cookieStore.cookies(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async storageState(): Promise<channels.APIRequestContextStorageStateResult> {
|
override async storageState(indexedDB = true): Promise<channels.APIRequestContextStorageStateResult> {
|
||||||
return {
|
return {
|
||||||
cookies: this._cookieStore.allCookies(),
|
cookies: this._cookieStore.allCookies(),
|
||||||
origins: this._origins || []
|
origins: (this._origins || []).map(origin => ({ ...origin, indexedDB: indexedDB ? origin.indexedDB : [] })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
plugins: ["@typescript-eslint", "notice"],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 9,
|
|
||||||
sourceType: "module",
|
|
||||||
project: path.join(__dirname, '../../../../../tsconfig.json'),
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-restricted-globals": [
|
|
||||||
"error",
|
|
||||||
{ "name": "window" },
|
|
||||||
{ "name": "document" },
|
|
||||||
{ "name": "globalThis" },
|
|
||||||
],
|
|
||||||
'@typescript-eslint/no-floating-promises': 'error',
|
|
||||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -445,14 +445,7 @@ type BrowsersJSONDescriptor = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
|
function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
|
||||||
const headlessShells: BrowsersJSON['browsers'] = [];
|
return (browsersJSON['browsers']).map(obj => {
|
||||||
for (const browserName of ['chromium', 'chromium-tip-of-tree']) {
|
|
||||||
headlessShells.push({
|
|
||||||
...browsersJSON.browsers.find(browser => browser.name === browserName)!,
|
|
||||||
name: `${browserName}-headless-shell`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [...browsersJSON.browsers, ...headlessShells].map(obj => {
|
|
||||||
const name = obj.name;
|
const name = obj.name;
|
||||||
const revisionOverride = (obj.revisionOverrides || {})[hostPlatform];
|
const revisionOverride = (obj.revisionOverrides || {})[hostPlatform];
|
||||||
const revision = revisionOverride || obj.revision;
|
const revision = revisionOverride || obj.revision;
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import type { source } from './isomorphic/utilityScriptSerializers';
|
||||||
|
|
||||||
export type Storage = Omit<channels.OriginStorage, 'origin'>;
|
export type Storage = Omit<channels.OriginStorage, 'origin'>;
|
||||||
|
|
||||||
export async function collect(serializers: ReturnType<typeof source>, isFirefox: boolean): Promise<Storage> {
|
export async function collect(serializers: ReturnType<typeof source>, isFirefox: boolean, recordIndexedDB: boolean): Promise<Storage> {
|
||||||
const idbResult = await Promise.all((await indexedDB.databases()).map(async dbInfo => {
|
async function collectDB(dbInfo: IDBDatabaseInfo) {
|
||||||
if (!dbInfo.name)
|
if (!dbInfo.name)
|
||||||
throw new Error('Database name is empty');
|
throw new Error('Database name is empty');
|
||||||
if (!dbInfo.version)
|
if (!dbInfo.version)
|
||||||
|
|
@ -119,13 +119,13 @@ export async function collect(serializers: ReturnType<typeof source>, isFirefox:
|
||||||
version: dbInfo.version,
|
version: dbInfo.version,
|
||||||
stores,
|
stores,
|
||||||
};
|
};
|
||||||
})).catch(e => {
|
}
|
||||||
throw new Error('Unable to serialize IndexedDB: ' + e.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name)! })),
|
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name)! })),
|
||||||
indexedDB: idbResult,
|
indexedDB: recordIndexedDB ? await Promise.all((await indexedDB.databases()).map(collectDB)).catch(e => {
|
||||||
|
throw new Error('Unable to serialize IndexedDB: ' + e.message);
|
||||||
|
}) : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
wallTime: 0,
|
wallTime: 0,
|
||||||
monotonicTime: 0,
|
monotonicTime: 0,
|
||||||
sdkLanguage: context.attribution.playwright.options.sdkLanguage,
|
sdkLanguage: context.attribution.playwright.options.sdkLanguage,
|
||||||
testIdAttributeName
|
testIdAttributeName,
|
||||||
|
contextId: context.guid,
|
||||||
};
|
};
|
||||||
if (context instanceof BrowserContext) {
|
if (context instanceof BrowserContext) {
|
||||||
this._snapshotter = new Snapshotter(context, this);
|
this._snapshotter = new Snapshotter(context, this);
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntryFinished(entry: har.Entry) {
|
onEntryFinished(entry: har.Entry) {
|
||||||
this._storage.addResource(entry);
|
this._storage.addResource('', entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentBlob(sha1: string, buffer: Buffer) {
|
onContentBlob(sha1: string, buffer: Buffer) {
|
||||||
|
|
@ -85,7 +85,7 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
||||||
|
|
||||||
onFrameSnapshot(snapshot: FrameSnapshot): void {
|
onFrameSnapshot(snapshot: FrameSnapshot): void {
|
||||||
++this._snapshotCount;
|
++this._snapshotCount;
|
||||||
const renderer = this._storage.addFrameSnapshot(snapshot, []);
|
const renderer = this._storage.addFrameSnapshot('', snapshot, []);
|
||||||
this._snapshotReadyPromises.get(snapshot.snapshotName || '')?.resolve(renderer);
|
this._snapshotReadyPromises.get(snapshot.snapshotName || '')?.resolve(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
13
packages/playwright-core/types/types.d.ts
vendored
13
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -9268,9 +9268,17 @@ export interface BrowserContext {
|
||||||
/**
|
/**
|
||||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
|
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
|
||||||
* snapshot.
|
* snapshot.
|
||||||
|
*
|
||||||
|
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
||||||
|
*
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
storageState(options?: {
|
storageState(options?: {
|
||||||
|
/**
|
||||||
|
* Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
||||||
|
*/
|
||||||
|
indexedDB?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file path to save the storage state to. If
|
* The file path to save the storage state to. If
|
||||||
* [`path`](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state-option-path) is a
|
* [`path`](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state-option-path) is a
|
||||||
|
|
@ -18531,6 +18539,11 @@ export interface APIRequestContext {
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
storageState(options?: {
|
storageState(options?: {
|
||||||
|
/**
|
||||||
|
* Defaults to `true`. Set to `false` to omit IndexedDB from snapshot.
|
||||||
|
*/
|
||||||
|
indexedDB?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file path to save the storage state to. If
|
* The file path to save the storage state to. If
|
||||||
* [`path`](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-storage-state-option-path) is
|
* [`path`](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-storage-state-option-path) is
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: "../../.eslintrc-with-ts-config.js",
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: '../../.eslintrc-with-ts-config.js',
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-floating-promises': 'error',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -24,10 +24,9 @@ import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
|
||||||
import { rootTestType } from './common/testType';
|
import { rootTestType } from './common/testType';
|
||||||
import type { ContextReuseMode } from './common/config';
|
import type { ContextReuseMode } from './common/config';
|
||||||
import type { ApiCallData, ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
|
import type { ApiCallData, ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
|
||||||
import type { MockingProxy } from '../../playwright-core/src/client/mockingProxy';
|
|
||||||
import type { BrowserContext as BrowserContextImpl } from '../../playwright-core/src/client/browserContext';
|
import type { BrowserContext as BrowserContextImpl } from '../../playwright-core/src/client/browserContext';
|
||||||
|
import type { Playwright as PlaywrightImpl } from '../../playwright-core/src/client/playwright';
|
||||||
import { currentTestInfo } from './common/globals';
|
import { currentTestInfo } from './common/globals';
|
||||||
import type { Playwright as PlaywrightImpl } from 'playwright-core/lib/client/playwright';
|
|
||||||
export { expect } from './matchers/expect';
|
export { expect } from './matchers/expect';
|
||||||
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
||||||
|
|
||||||
|
|
@ -53,11 +52,13 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||||
};
|
};
|
||||||
|
|
||||||
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||||
|
playwright: PlaywrightImpl;
|
||||||
_browserOptions: LaunchOptions;
|
_browserOptions: LaunchOptions;
|
||||||
_optionContextReuseMode: ContextReuseMode,
|
_optionContextReuseMode: ContextReuseMode,
|
||||||
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
||||||
_reuseContext: boolean,
|
_reuseContext: boolean,
|
||||||
_mockingProxy?: void,
|
_mockingProxy?: void,
|
||||||
|
_pageSnapshot: PageSnapshotOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
|
|
@ -76,23 +77,22 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
video: ['off', { scope: 'worker', option: true }],
|
video: ['off', { scope: 'worker', option: true }],
|
||||||
trace: ['off', { scope: 'worker', option: true }],
|
trace: ['off', { scope: 'worker', option: true }],
|
||||||
mockingProxy: [undefined, { scope: 'worker', option: true }],
|
mockingProxy: [undefined, { scope: 'worker', option: true }],
|
||||||
|
_pageSnapshot: ['off', { scope: 'worker', option: true }],
|
||||||
|
|
||||||
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
||||||
const options: LaunchOptions = {
|
const options: LaunchOptions = {
|
||||||
handleSIGINT: false,
|
handleSIGINT: false,
|
||||||
...launchOptions,
|
...launchOptions,
|
||||||
|
tracesDir: tracing().tracesDir(),
|
||||||
};
|
};
|
||||||
if (headless !== undefined)
|
if (headless !== undefined)
|
||||||
options.headless = headless;
|
options.headless = headless;
|
||||||
if (channel !== undefined)
|
if (channel !== undefined)
|
||||||
options.channel = channel;
|
options.channel = channel;
|
||||||
options.tracesDir = tracing().tracesDir();
|
|
||||||
|
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit, playwright._bidiChromium, playwright._bidiFirefox])
|
playwright._defaultLaunchOptions = options;
|
||||||
(browserType as any)._defaultLaunchOptions = options;
|
|
||||||
await use(options);
|
await use(options);
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit, playwright._bidiChromium, playwright._bidiFirefox])
|
playwright._defaultLaunchOptions = undefined;
|
||||||
(browserType as any)._defaultLaunchOptions = undefined;
|
|
||||||
}, { scope: 'worker', auto: true, box: true }],
|
}, { scope: 'worker', auto: true, box: true }],
|
||||||
|
|
||||||
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
|
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
|
||||||
|
|
@ -241,30 +241,23 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
testInfo.snapshotSuffix = process.platform;
|
testInfo.snapshotSuffix = process.platform;
|
||||||
if (debugMode())
|
if (debugMode())
|
||||||
(testInfo as TestInfoImpl)._setDebugMode();
|
(testInfo as TestInfoImpl)._setDebugMode();
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
|
||||||
(browserType as any)._defaultContextOptions = _combinedContextOptions;
|
playwright._defaultContextOptions = _combinedContextOptions;
|
||||||
(browserType as any)._defaultContextTimeout = actionTimeout || 0;
|
playwright._defaultContextTimeout = actionTimeout || 0;
|
||||||
(browserType as any)._defaultContextNavigationTimeout = navigationTimeout || 0;
|
playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
|
||||||
}
|
|
||||||
(playwright.request as any)._defaultContextOptions = { ..._combinedContextOptions };
|
|
||||||
(playwright.request as any)._defaultContextOptions.tracesDir = tracing().tracesDir();
|
|
||||||
(playwright.request as any)._defaultContextOptions.timeout = actionTimeout || 0;
|
|
||||||
await use();
|
await use();
|
||||||
(playwright.request as any)._defaultContextOptions = undefined;
|
playwright._defaultContextOptions = undefined;
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
playwright._defaultContextTimeout = undefined;
|
||||||
(browserType as any)._defaultContextOptions = undefined;
|
playwright._defaultContextNavigationTimeout = undefined;
|
||||||
(browserType as any)._defaultContextTimeout = undefined;
|
|
||||||
(browserType as any)._defaultContextNavigationTimeout = undefined;
|
|
||||||
}
|
|
||||||
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
|
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
|
||||||
|
|
||||||
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
|
_setupArtifacts: [async ({ playwright, screenshot, _pageSnapshot }, use, testInfo) => {
|
||||||
// This fixture has a separate zero-timeout slot to ensure that artifact collection
|
// This fixture has a separate zero-timeout slot to ensure that artifact collection
|
||||||
// happens even after some fixtures or hooks time out.
|
// happens even after some fixtures or hooks time out.
|
||||||
// Now that default test timeout is known, we can replace zero with an actual value.
|
// Now that default test timeout is known, we can replace zero with an actual value.
|
||||||
testInfo.setTimeout(testInfo.project.timeout);
|
testInfo.setTimeout(testInfo.project.timeout);
|
||||||
|
|
||||||
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
|
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, _pageSnapshot);
|
||||||
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
||||||
|
|
||||||
const tracingGroupSteps: TestStepInternal[] = [];
|
const tracingGroupSteps: TestStepInternal[] = [];
|
||||||
|
|
@ -462,7 +455,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
type ScreenshotOption = PlaywrightWorkerOptions['screenshot'] | undefined;
|
type ScreenshotOption = PlaywrightWorkerOptions['screenshot'] | undefined;
|
||||||
type Playwright = PlaywrightWorkerArgs['playwright'];
|
type PageSnapshotOption = 'off' | 'on' | 'only-on-failure';
|
||||||
|
|
||||||
function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
|
function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
|
||||||
if (!video)
|
if (!video)
|
||||||
|
|
@ -534,54 +527,135 @@ function connectOptionsFromEnv() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArtifactsRecorder {
|
class SnapshotRecorder {
|
||||||
private _testInfo!: TestInfoImpl;
|
private _ordinal = 0;
|
||||||
private _playwright: Playwright;
|
private _temporary: string[] = [];
|
||||||
private _artifactsDir: string;
|
private _snapshottedSymbol = Symbol('snapshotted');
|
||||||
private _screenshotMode: ScreenshotMode;
|
|
||||||
private _screenshotOptions: { mode: ScreenshotMode } & Pick<playwrightLibrary.PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined;
|
|
||||||
private _temporaryScreenshots: string[] = [];
|
|
||||||
private _temporaryArtifacts: string[] = [];
|
|
||||||
private _reusedContexts = new Set<BrowserContext>();
|
|
||||||
private _screenshotOrdinal = 0;
|
|
||||||
private _screenshottedSymbol: symbol;
|
|
||||||
private _startedCollectingArtifacts: symbol;
|
|
||||||
|
|
||||||
constructor(playwright: Playwright, artifactsDir: string, screenshot: ScreenshotOption) {
|
constructor(
|
||||||
this._playwright = playwright;
|
private _artifactsRecorder: ArtifactsRecorder,
|
||||||
this._artifactsDir = artifactsDir;
|
private _mode: ScreenshotMode | PageSnapshotOption,
|
||||||
this._screenshotMode = normalizeScreenshotMode(screenshot);
|
private _name: string,
|
||||||
this._screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot;
|
private _contentType: string,
|
||||||
this._screenshottedSymbol = Symbol('screenshotted');
|
private _extension: string,
|
||||||
this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
private _doSnapshot: (page: Page, path: string) => Promise<void>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fixOrdinal() {
|
||||||
|
// Since beforeAll(s), test and afterAll(s) reuse the same TestInfo, make sure we do not
|
||||||
|
// overwrite previous screenshots.
|
||||||
|
this._ordinal = this.testInfo.attachments.filter(a => a.name === this._name).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldCaptureUponFinish() {
|
||||||
|
return this._mode === 'on' ||
|
||||||
|
(this._mode === 'only-on-failure' && this.testInfo._isFailure()) ||
|
||||||
|
(this._mode === 'on-first-failure' && this.testInfo._isFailure() && this.testInfo.retry === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async maybeCapture() {
|
||||||
|
if (!this.shouldCaptureUponFinish())
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Promise.all(this._artifactsRecorder._playwright._allPages().map(page => this._snapshotPage(page, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async persistTemporary() {
|
||||||
|
if (this.shouldCaptureUponFinish()) {
|
||||||
|
await Promise.all(this._temporary.map(async file => {
|
||||||
|
try {
|
||||||
|
const path = this._createAttachmentPath();
|
||||||
|
await fs.promises.rename(file, path);
|
||||||
|
this._attach(path);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async captureTemporary(context: BrowserContext) {
|
||||||
|
if (this._mode === 'on' || this._mode === 'only-on-failure' || (this._mode === 'on-first-failure' && this.testInfo.retry === 0))
|
||||||
|
await Promise.all(context.pages().map(page => this._snapshotPage(page, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _attach(screenshotPath: string) {
|
||||||
|
this.testInfo.attachments.push({ name: this._name, path: screenshotPath, contentType: this._contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createAttachmentPath() {
|
||||||
|
const testFailed = this.testInfo._isFailure();
|
||||||
|
const index = this._ordinal + 1;
|
||||||
|
++this._ordinal;
|
||||||
|
const path = this.testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${index}${this._extension}`);
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createTemporaryArtifact(...name: string[]) {
|
private _createTemporaryArtifact(...name: string[]) {
|
||||||
const file = path.join(this._artifactsDir, ...name);
|
const file = path.join(this._artifactsRecorder._artifactsDir, ...name);
|
||||||
this._temporaryArtifacts.push(file);
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _snapshotPage(page: Page, temporary: boolean) {
|
||||||
|
if ((page as any)[this._snapshottedSymbol])
|
||||||
|
return;
|
||||||
|
(page as any)[this._snapshottedSymbol] = true;
|
||||||
|
try {
|
||||||
|
const path = temporary ? this._createTemporaryArtifact(createGuid() + this._extension) : this._createAttachmentPath();
|
||||||
|
await this._doSnapshot(page, path);
|
||||||
|
if (temporary)
|
||||||
|
this._temporary.push(path);
|
||||||
|
else
|
||||||
|
this._attach(path);
|
||||||
|
} catch {
|
||||||
|
// snapshot may fail, just ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get testInfo(): TestInfoImpl {
|
||||||
|
return this._artifactsRecorder._testInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtifactsRecorder {
|
||||||
|
_testInfo!: TestInfoImpl;
|
||||||
|
_playwright: PlaywrightImpl;
|
||||||
|
_artifactsDir: string;
|
||||||
|
private _reusedContexts = new Set<BrowserContext>();
|
||||||
|
private _startedCollectingArtifacts: symbol;
|
||||||
|
|
||||||
|
private _pageSnapshotRecorder: SnapshotRecorder;
|
||||||
|
private _screenshotRecorder: SnapshotRecorder;
|
||||||
|
|
||||||
|
constructor(playwright: PlaywrightImpl, artifactsDir: string, screenshot: ScreenshotOption, pageSnapshot: PageSnapshotOption) {
|
||||||
|
this._playwright = playwright;
|
||||||
|
this._artifactsDir = artifactsDir;
|
||||||
|
const screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot;
|
||||||
|
this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
||||||
|
|
||||||
|
this._screenshotRecorder = new SnapshotRecorder(this, normalizeScreenshotMode(screenshot), 'screenshot', 'image/png', '.png', async (page, path) => {
|
||||||
|
await page.screenshot({ ...screenshotOptions, timeout: 5000, path, caret: 'initial' });
|
||||||
|
});
|
||||||
|
|
||||||
|
this._pageSnapshotRecorder = new SnapshotRecorder(this, pageSnapshot, 'pageSnapshot', 'text/plain', '.ariasnapshot', async (page, path) => {
|
||||||
|
const ariaSnapshot = await page.locator('body').ariaSnapshot();
|
||||||
|
await fs.promises.writeFile(path, ariaSnapshot);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async willStartTest(testInfo: TestInfoImpl) {
|
async willStartTest(testInfo: TestInfoImpl) {
|
||||||
this._testInfo = testInfo;
|
this._testInfo = testInfo;
|
||||||
testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction();
|
testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction();
|
||||||
|
|
||||||
// Since beforeAll(s), test and afterAll(s) reuse the same TestInfo, make sure we do not
|
this._screenshotRecorder.fixOrdinal();
|
||||||
// overwrite previous screenshots.
|
this._pageSnapshotRecorder.fixOrdinal();
|
||||||
this._screenshotOrdinal = testInfo.attachments.filter(a => a.name === 'screenshot').length;
|
|
||||||
|
|
||||||
// Process existing contexts.
|
// Process existing contexts.
|
||||||
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) {
|
await Promise.all(this._playwright._allContexts().map(async context => {
|
||||||
const promises: (Promise<void> | undefined)[] = [];
|
if ((context as any)[kIsReusedContext])
|
||||||
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
this._reusedContexts.add(context);
|
||||||
for (const context of existingContexts) {
|
else
|
||||||
if ((context as any)[kIsReusedContext])
|
await this.didCreateBrowserContext(context);
|
||||||
this._reusedContexts.add(context);
|
}));
|
||||||
else
|
|
||||||
promises.push(this.didCreateBrowserContext(context));
|
|
||||||
}
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
const existingApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
const existingApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
await Promise.all(existingApiRequests.map(c => this.didCreateRequestContext(c)));
|
await Promise.all(existingApiRequests.map(c => this.didCreateRequestContext(c)));
|
||||||
|
|
@ -598,11 +672,9 @@ class ArtifactsRecorder {
|
||||||
if (this._reusedContexts.has(context))
|
if (this._reusedContexts.has(context))
|
||||||
return;
|
return;
|
||||||
await this._stopTracing(context.tracing);
|
await this._stopTracing(context.tracing);
|
||||||
if (this._screenshotMode === 'on' || this._screenshotMode === 'only-on-failure' || (this._screenshotMode === 'on-first-failure' && this._testInfo.retry === 0)) {
|
|
||||||
// Capture screenshot for now. We'll know whether we have to preserve them
|
await this._screenshotRecorder.captureTemporary(context);
|
||||||
// after the test finishes.
|
await this._pageSnapshotRecorder.captureTemporary(context);
|
||||||
await Promise.all(context.pages().map(page => this._screenshotPage(page, true)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async didCreateRequestContext(context: APIRequestContext) {
|
async didCreateRequestContext(context: APIRequestContext) {
|
||||||
|
|
@ -615,26 +687,15 @@ class ArtifactsRecorder {
|
||||||
await this._stopTracing(tracing);
|
await this._stopTracing(tracing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _shouldCaptureScreenshotUponFinish() {
|
|
||||||
return this._screenshotMode === 'on' ||
|
|
||||||
(this._screenshotMode === 'only-on-failure' && this._testInfo._isFailure()) ||
|
|
||||||
(this._screenshotMode === 'on-first-failure' && this._testInfo._isFailure() && this._testInfo.retry === 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async didFinishTestFunction() {
|
async didFinishTestFunction() {
|
||||||
if (this._shouldCaptureScreenshotUponFinish())
|
await this._screenshotRecorder.maybeCapture();
|
||||||
await this._screenshotOnTestFailure();
|
await this._pageSnapshotRecorder.maybeCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
async didFinishTest() {
|
async didFinishTest() {
|
||||||
const captureScreenshots = this._shouldCaptureScreenshotUponFinish();
|
await this.didFinishTestFunction();
|
||||||
if (captureScreenshots)
|
|
||||||
await this._screenshotOnTestFailure();
|
|
||||||
|
|
||||||
let leftoverContexts: BrowserContext[] = [];
|
const leftoverContexts = this._playwright._allContexts().filter(context => !this._reusedContexts.has(context));
|
||||||
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit])
|
|
||||||
leftoverContexts.push(...(browserType as any)._contexts);
|
|
||||||
leftoverContexts = leftoverContexts.filter(context => !this._reusedContexts.has(context));
|
|
||||||
const leftoverApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
const leftoverApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
|
|
||||||
// Collect traces/screenshots for remaining contexts.
|
// Collect traces/screenshots for remaining contexts.
|
||||||
|
|
@ -645,55 +706,8 @@ class ArtifactsRecorder {
|
||||||
await this._stopTracing(tracing);
|
await this._stopTracing(tracing);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
// Attach temporary screenshots for contexts closed before collecting the test trace.
|
await this._screenshotRecorder.persistTemporary();
|
||||||
if (captureScreenshots) {
|
await this._pageSnapshotRecorder.persistTemporary();
|
||||||
for (const file of this._temporaryScreenshots) {
|
|
||||||
try {
|
|
||||||
const screenshotPath = this._createScreenshotAttachmentPath();
|
|
||||||
await fs.promises.rename(file, screenshotPath);
|
|
||||||
this._attachScreenshot(screenshotPath);
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createScreenshotAttachmentPath() {
|
|
||||||
const testFailed = this._testInfo._isFailure();
|
|
||||||
const index = this._screenshotOrdinal + 1;
|
|
||||||
++this._screenshotOrdinal;
|
|
||||||
const screenshotPath = this._testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${index}.png`);
|
|
||||||
return screenshotPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _screenshotPage(page: Page, temporary: boolean) {
|
|
||||||
if ((page as any)[this._screenshottedSymbol])
|
|
||||||
return;
|
|
||||||
(page as any)[this._screenshottedSymbol] = true;
|
|
||||||
try {
|
|
||||||
const screenshotPath = temporary ? this._createTemporaryArtifact(createGuid() + '.png') : this._createScreenshotAttachmentPath();
|
|
||||||
// Pass caret=initial to avoid any evaluations that might slow down the screenshot
|
|
||||||
// and let the page modify itself from the problematic state it had at the moment of failure.
|
|
||||||
await page.screenshot({ ...this._screenshotOptions, timeout: 5000, path: screenshotPath, caret: 'initial' });
|
|
||||||
if (temporary)
|
|
||||||
this._temporaryScreenshots.push(screenshotPath);
|
|
||||||
else
|
|
||||||
this._attachScreenshot(screenshotPath);
|
|
||||||
} catch {
|
|
||||||
// Screenshot may fail, just ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _attachScreenshot(screenshotPath: string) {
|
|
||||||
this._testInfo.attachments.push({ name: 'screenshot', path: screenshotPath, contentType: 'image/png' });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _screenshotOnTestFailure() {
|
|
||||||
const contexts: BrowserContext[] = [];
|
|
||||||
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit])
|
|
||||||
contexts.push(...(browserType as any)._contexts);
|
|
||||||
const pages = contexts.map(ctx => ctx.pages()).flat();
|
|
||||||
await Promise.all(pages.map(page => this._screenshotPage(page, false)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _startTraceChunkOnContextCreation(tracing: Tracing) {
|
private async _startTraceChunkOnContextCreation(tracing: Tracing) {
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ export interface TestServerInterface {
|
||||||
workers?: number | string;
|
workers?: number | string;
|
||||||
updateSnapshots?: 'all' | 'changed' | 'missing' | 'none';
|
updateSnapshots?: 'all' | 'changed' | 'missing' | 'none';
|
||||||
updateSourceMethod?: 'overwrite' | 'patch' | '3way';
|
updateSourceMethod?: 'overwrite' | 'patch' | '3way';
|
||||||
|
pageSnapshot?: 'off' | 'on' | 'only-on-failure';
|
||||||
reporters?: string[],
|
reporters?: string[],
|
||||||
trace?: 'on' | 'off';
|
trace?: 'on' | 'off';
|
||||||
video?: 'on' | 'off';
|
video?: 'on' | 'off';
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,9 @@ export interface GitCommitInfo {
|
||||||
'revision.subject'?: string;
|
'revision.subject'?: string;
|
||||||
'revision.timestamp'?: number | Date;
|
'revision.timestamp'?: number | Date;
|
||||||
'revision.link'?: string;
|
'revision.link'?: string;
|
||||||
|
'revision.diff'?: string;
|
||||||
|
'pull.link'?: string;
|
||||||
|
'pull.diff'?: string;
|
||||||
|
'pull.base'?: string;
|
||||||
'ci.link'?: string;
|
'ci.link'?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,9 @@ export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerP
|
||||||
|
|
||||||
setup: async (config: FullConfig, configDir: string) => {
|
setup: async (config: FullConfig, configDir: string) => {
|
||||||
const fromEnv = linksFromEnv();
|
const fromEnv = linksFromEnv();
|
||||||
const fromCLI = await gitStatusFromCLI(options?.directory || configDir);
|
const fromCLI = await gitStatusFromCLI(options?.directory || configDir, fromEnv);
|
||||||
const info = { ...fromEnv, ...fromCLI };
|
|
||||||
if (info['revision.timestamp'] instanceof Date)
|
|
||||||
info['revision.timestamp'] = info['revision.timestamp'].getTime();
|
|
||||||
|
|
||||||
config.metadata = config.metadata || {};
|
config.metadata = config.metadata || {};
|
||||||
config.metadata['git.commit.info'] = info;
|
config.metadata['git.commit.info'] = { ...fromEnv, ...fromCLI };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -48,8 +44,8 @@ interface GitCommitInfoPluginOptions {
|
||||||
directory?: string;
|
directory?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function linksFromEnv(): Pick<GitCommitInfo, 'revision.link' | 'ci.link'> {
|
function linksFromEnv() {
|
||||||
const out: { 'revision.link'?: string; 'ci.link'?: string; } = {};
|
const out: Partial<GitCommitInfo> = {};
|
||||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
||||||
if (process.env.BUILD_URL)
|
if (process.env.BUILD_URL)
|
||||||
out['ci.link'] = process.env.BUILD_URL;
|
out['ci.link'] = process.env.BUILD_URL;
|
||||||
|
|
@ -63,28 +59,54 @@ function linksFromEnv(): Pick<GitCommitInfo, 'revision.link' | 'ci.link'> {
|
||||||
out['revision.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`;
|
out['revision.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`;
|
||||||
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
||||||
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||||
|
if (process.env.GITHUB_REF_NAME && process.env.GITHUB_REF_NAME.endsWith('/merge')) {
|
||||||
|
const pullId = process.env.GITHUB_REF_NAME.substring(0, process.env.GITHUB_REF_NAME.indexOf('/merge'));
|
||||||
|
out['pull.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${pullId}`;
|
||||||
|
out['pull.base'] = process.env.GITHUB_BASE_REF;
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gitStatusFromCLI(gitDir: string): Promise<GitCommitInfo | undefined> {
|
async function gitStatusFromCLI(gitDir: string, envInfo: Pick<GitCommitInfo, 'pull.base'>): Promise<GitCommitInfo | undefined> {
|
||||||
const separator = `:${createGuid().slice(0, 4)}:`;
|
const separator = `:${createGuid().slice(0, 4)}:`;
|
||||||
const { code, stdout } = await spawnAsync(
|
const commitInfoResult = await spawnAsync(
|
||||||
'git',
|
'git',
|
||||||
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
|
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||||
);
|
);
|
||||||
if (code)
|
if (commitInfoResult.code)
|
||||||
return;
|
return;
|
||||||
const showOutput = stdout.trim();
|
const showOutput = commitInfoResult.stdout.trim();
|
||||||
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
|
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
|
||||||
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
||||||
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
||||||
|
|
||||||
return {
|
const result: GitCommitInfo = {
|
||||||
'revision.id': id,
|
'revision.id': id,
|
||||||
'revision.author': author,
|
'revision.author': author,
|
||||||
'revision.email': email,
|
'revision.email': email,
|
||||||
'revision.subject': subject,
|
'revision.subject': subject,
|
||||||
'revision.timestamp': timestamp,
|
'revision.timestamp': timestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const diffLimit = 1_000_000; // 1MB
|
||||||
|
if (envInfo['pull.base']) {
|
||||||
|
const pullDiffResult = await spawnAsync(
|
||||||
|
'git',
|
||||||
|
['diff', envInfo['pull.base']],
|
||||||
|
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||||
|
);
|
||||||
|
if (!pullDiffResult.code)
|
||||||
|
result['pull.diff'] = pullDiffResult.stdout.substring(0, diffLimit);
|
||||||
|
} else {
|
||||||
|
const diffResult = await spawnAsync(
|
||||||
|
'git',
|
||||||
|
['diff', 'HEAD~1'],
|
||||||
|
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||||
|
);
|
||||||
|
if (!diffResult.code)
|
||||||
|
result['revision.diff'] = diffResult.stdout.substring(0, diffLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -311,6 +311,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||||
...(params.headed !== undefined ? { headless: !params.headed } : {}),
|
...(params.headed !== undefined ? { headless: !params.headed } : {}),
|
||||||
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
||||||
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
||||||
|
_pageSnapshot: params.pageSnapshot,
|
||||||
},
|
},
|
||||||
...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}),
|
...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}),
|
||||||
...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}),
|
...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}),
|
||||||
|
|
|
||||||
20
packages/protocol/src/channels.d.ts
vendored
20
packages/protocol/src/channels.d.ts
vendored
|
|
@ -349,7 +349,7 @@ export interface APIRequestContextChannel extends APIRequestContextEventTarget,
|
||||||
fetch(params: APIRequestContextFetchParams, metadata?: CallMetadata): Promise<APIRequestContextFetchResult>;
|
fetch(params: APIRequestContextFetchParams, metadata?: CallMetadata): Promise<APIRequestContextFetchResult>;
|
||||||
fetchResponseBody(params: APIRequestContextFetchResponseBodyParams, metadata?: CallMetadata): Promise<APIRequestContextFetchResponseBodyResult>;
|
fetchResponseBody(params: APIRequestContextFetchResponseBodyParams, metadata?: CallMetadata): Promise<APIRequestContextFetchResponseBodyResult>;
|
||||||
fetchLog(params: APIRequestContextFetchLogParams, metadata?: CallMetadata): Promise<APIRequestContextFetchLogResult>;
|
fetchLog(params: APIRequestContextFetchLogParams, metadata?: CallMetadata): Promise<APIRequestContextFetchLogResult>;
|
||||||
storageState(params?: APIRequestContextStorageStateParams, metadata?: CallMetadata): Promise<APIRequestContextStorageStateResult>;
|
storageState(params: APIRequestContextStorageStateParams, metadata?: CallMetadata): Promise<APIRequestContextStorageStateResult>;
|
||||||
disposeAPIResponse(params: APIRequestContextDisposeAPIResponseParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeAPIResponseResult>;
|
disposeAPIResponse(params: APIRequestContextDisposeAPIResponseParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeAPIResponseResult>;
|
||||||
dispose(params: APIRequestContextDisposeParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeResult>;
|
dispose(params: APIRequestContextDisposeParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeResult>;
|
||||||
}
|
}
|
||||||
|
|
@ -405,8 +405,12 @@ export type APIRequestContextFetchLogOptions = {
|
||||||
export type APIRequestContextFetchLogResult = {
|
export type APIRequestContextFetchLogResult = {
|
||||||
log: string[],
|
log: string[],
|
||||||
};
|
};
|
||||||
export type APIRequestContextStorageStateParams = {};
|
export type APIRequestContextStorageStateParams = {
|
||||||
export type APIRequestContextStorageStateOptions = {};
|
indexedDB?: boolean,
|
||||||
|
};
|
||||||
|
export type APIRequestContextStorageStateOptions = {
|
||||||
|
indexedDB?: boolean,
|
||||||
|
};
|
||||||
export type APIRequestContextStorageStateResult = {
|
export type APIRequestContextStorageStateResult = {
|
||||||
cookies: NetworkCookie[],
|
cookies: NetworkCookie[],
|
||||||
origins: OriginStorage[],
|
origins: OriginStorage[],
|
||||||
|
|
@ -1688,7 +1692,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
|
||||||
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
|
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
|
||||||
setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetWebSocketInterceptionPatternsResult>;
|
setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetWebSocketInterceptionPatternsResult>;
|
||||||
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
|
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
|
||||||
storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
|
storageState(params: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
|
||||||
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
|
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
|
||||||
enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise<BrowserContextEnableRecorderResult>;
|
enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise<BrowserContextEnableRecorderResult>;
|
||||||
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
||||||
|
|
@ -1921,8 +1925,12 @@ export type BrowserContextSetOfflineOptions = {
|
||||||
|
|
||||||
};
|
};
|
||||||
export type BrowserContextSetOfflineResult = void;
|
export type BrowserContextSetOfflineResult = void;
|
||||||
export type BrowserContextStorageStateParams = {};
|
export type BrowserContextStorageStateParams = {
|
||||||
export type BrowserContextStorageStateOptions = {};
|
indexedDB?: boolean,
|
||||||
|
};
|
||||||
|
export type BrowserContextStorageStateOptions = {
|
||||||
|
indexedDB?: boolean,
|
||||||
|
};
|
||||||
export type BrowserContextStorageStateResult = {
|
export type BrowserContextStorageStateResult = {
|
||||||
cookies: NetworkCookie[],
|
cookies: NetworkCookie[],
|
||||||
origins: OriginStorage[],
|
origins: OriginStorage[],
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,8 @@ APIRequestContext:
|
||||||
items: string
|
items: string
|
||||||
|
|
||||||
storageState:
|
storageState:
|
||||||
|
parameters:
|
||||||
|
indexedDB: boolean?
|
||||||
returns:
|
returns:
|
||||||
cookies:
|
cookies:
|
||||||
type: array
|
type: array
|
||||||
|
|
@ -1285,6 +1287,8 @@ BrowserContext:
|
||||||
offline: boolean
|
offline: boolean
|
||||||
|
|
||||||
storageState:
|
storageState:
|
||||||
|
parameters:
|
||||||
|
indexedDB: boolean?
|
||||||
returns:
|
returns:
|
||||||
cookies:
|
cookies:
|
||||||
type: array
|
type: array
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@
|
||||||
"@web/*": ["../web/src/*"],
|
"@web/*": ["../web/src/*"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "../web/src"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,14 +120,16 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
||||||
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
||||||
if (!snapshotServer)
|
if (!snapshotServer)
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
return snapshotServer.serveSnapshotInfo(relativePath, url.searchParams);
|
const pageOrFrameId = relativePath.substring('/snapshotInfo/'.length);
|
||||||
|
return snapshotServer.serveSnapshotInfo(pageOrFrameId, url.searchParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relativePath.startsWith('/snapshot/')) {
|
if (relativePath.startsWith('/snapshot/')) {
|
||||||
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
||||||
if (!snapshotServer)
|
if (!snapshotServer)
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
const response = snapshotServer.serveSnapshot(relativePath, url.searchParams, url.href);
|
const pageOrFrameId = relativePath.substring('/snapshot/'.length);
|
||||||
|
const response = snapshotServer.serveSnapshot(pageOrFrameId, url.searchParams, url.href);
|
||||||
if (isDeployedAsHttps)
|
if (isDeployedAsHttps)
|
||||||
response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests');
|
response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests');
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -137,7 +139,8 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
||||||
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
||||||
if (!snapshotServer)
|
if (!snapshotServer)
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
return snapshotServer.serveClosestScreenshot(relativePath, url.searchParams);
|
const pageOrFrameId = relativePath.substring('/closest-screenshot/'.length);
|
||||||
|
return snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relativePath.startsWith('/sha1/')) {
|
if (relativePath.startsWith('/sha1/')) {
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ export class SnapshotServer {
|
||||||
this._resourceLoader = resourceLoader;
|
this._resourceLoader = resourceLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
serveSnapshot(pathname: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
|
serveSnapshot(pageOrFrameId: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
|
||||||
const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams);
|
const snapshot = this._snapshot(pageOrFrameId, searchParams);
|
||||||
if (!snapshot)
|
if (!snapshot)
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
|
@ -41,16 +41,16 @@ export class SnapshotServer {
|
||||||
return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async serveClosestScreenshot(pathname: string, searchParams: URLSearchParams): Promise<Response> {
|
async serveClosestScreenshot(pageOrFrameId: string, searchParams: URLSearchParams): Promise<Response> {
|
||||||
const snapshot = this._snapshot(pathname.substring('/closest-screenshot'.length), searchParams);
|
const snapshot = this._snapshot(pageOrFrameId, searchParams);
|
||||||
const sha1 = snapshot?.closestScreenshot();
|
const sha1 = snapshot?.closestScreenshot();
|
||||||
if (!sha1)
|
if (!sha1)
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
return new Response(await this._resourceLoader(sha1));
|
return new Response(await this._resourceLoader(sha1));
|
||||||
}
|
}
|
||||||
|
|
||||||
serveSnapshotInfo(pathname: string, searchParams: URLSearchParams): Response {
|
serveSnapshotInfo(pageOrFrameId: string, searchParams: URLSearchParams): Response {
|
||||||
const snapshot = this._snapshot(pathname.substring('/snapshotInfo'.length), searchParams);
|
const snapshot = this._snapshot(pageOrFrameId, searchParams);
|
||||||
return this._respondWithJson(snapshot ? {
|
return this._respondWithJson(snapshot ? {
|
||||||
viewport: snapshot.viewport(),
|
viewport: snapshot.viewport(),
|
||||||
url: snapshot.snapshot().frameUrl,
|
url: snapshot.snapshot().frameUrl,
|
||||||
|
|
@ -61,9 +61,9 @@ export class SnapshotServer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _snapshot(pathname: string, params: URLSearchParams) {
|
private _snapshot(pageOrFrameId: string, params: URLSearchParams) {
|
||||||
const name = params.get('name')!;
|
const name = params.get('name')!;
|
||||||
return this._snapshotStorage.snapshotByName(pathname.slice(1), name);
|
return this._snapshotStorage.snapshotByName(pageOrFrameId, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _respondWithJson(object: any): Response {
|
private _respondWithJson(object: any): Response {
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,19 @@ import type { PageEntry } from '../types/entries';
|
||||||
import { LRUCache } from './lruCache';
|
import { LRUCache } from './lruCache';
|
||||||
|
|
||||||
export class SnapshotStorage {
|
export class SnapshotStorage {
|
||||||
private _resources: ResourceSnapshot[] = [];
|
|
||||||
private _frameSnapshots = new Map<string, {
|
private _frameSnapshots = new Map<string, {
|
||||||
raw: FrameSnapshot[],
|
raw: FrameSnapshot[],
|
||||||
renderers: SnapshotRenderer[]
|
renderers: SnapshotRenderer[],
|
||||||
}>();
|
}>();
|
||||||
private _cache = new LRUCache<SnapshotRenderer, string>(100_000_000); // 100MB per each trace
|
private _cache = new LRUCache<SnapshotRenderer, string>(100_000_000); // 100MB per each trace
|
||||||
|
private _contextToResources = new Map<string, ResourceSnapshot[]>();
|
||||||
|
|
||||||
addResource(resource: ResourceSnapshot): void {
|
addResource(contextId: string, resource: ResourceSnapshot): void {
|
||||||
resource.request.url = rewriteURLForCustomProtocol(resource.request.url);
|
resource.request.url = rewriteURLForCustomProtocol(resource.request.url);
|
||||||
this._resources.push(resource);
|
this._ensureResourcesForContext(contextId).push(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFrameSnapshot(snapshot: FrameSnapshot, screencastFrames: PageEntry['screencastFrames']) {
|
addFrameSnapshot(contextId: string, snapshot: FrameSnapshot, screencastFrames: PageEntry['screencastFrames']) {
|
||||||
for (const override of snapshot.resourceOverrides)
|
for (const override of snapshot.resourceOverrides)
|
||||||
override.url = rewriteURLForCustomProtocol(override.url);
|
override.url = rewriteURLForCustomProtocol(override.url);
|
||||||
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
|
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
|
||||||
|
|
@ -46,7 +46,8 @@ export class SnapshotStorage {
|
||||||
this._frameSnapshots.set(snapshot.pageId, frameSnapshots);
|
this._frameSnapshots.set(snapshot.pageId, frameSnapshots);
|
||||||
}
|
}
|
||||||
frameSnapshots.raw.push(snapshot);
|
frameSnapshots.raw.push(snapshot);
|
||||||
const renderer = new SnapshotRenderer(this._cache, this._resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1);
|
const resources = this._ensureResourcesForContext(contextId);
|
||||||
|
const renderer = new SnapshotRenderer(this._cache, resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1);
|
||||||
frameSnapshots.renderers.push(renderer);
|
frameSnapshots.renderers.push(renderer);
|
||||||
return renderer;
|
return renderer;
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +63,16 @@ export class SnapshotStorage {
|
||||||
|
|
||||||
finalize() {
|
finalize() {
|
||||||
// Resources are not necessarily sorted in the trace file, so sort them now.
|
// Resources are not necessarily sorted in the trace file, so sort them now.
|
||||||
this._resources.sort((a, b) => (a._monotonicTime || 0) - (b._monotonicTime || 0));
|
for (const resources of this._contextToResources.values())
|
||||||
|
resources.sort((a, b) => (a._monotonicTime || 0) - (b._monotonicTime || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _ensureResourcesForContext(contextId: string): ResourceSnapshot[] {
|
||||||
|
let resources = this._contextToResources.get(contextId);
|
||||||
|
if (!resources) {
|
||||||
|
resources = [];
|
||||||
|
this._contextToResources.set(contextId, resources);
|
||||||
|
}
|
||||||
|
return resources;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export class TraceModel {
|
||||||
this.contextEntries.push(contextEntry);
|
this.contextEntries.push(contextEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._snapshotStorage!.finalize();
|
this._snapshotStorage.finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasEntry(filename: string): Promise<boolean> {
|
async hasEntry(filename: string): Promise<boolean> {
|
||||||
|
|
@ -153,5 +153,6 @@ function createEmptyContext(): ContextEntry {
|
||||||
errors: [],
|
errors: [],
|
||||||
stdio: [],
|
stdio: [],
|
||||||
hasSource: false,
|
hasSource: false,
|
||||||
|
contextId: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export class TraceModernizer {
|
||||||
contextEntry.sdkLanguage = event.sdkLanguage;
|
contextEntry.sdkLanguage = event.sdkLanguage;
|
||||||
contextEntry.options = event.options;
|
contextEntry.options = event.options;
|
||||||
contextEntry.testIdAttributeName = event.testIdAttributeName;
|
contextEntry.testIdAttributeName = event.testIdAttributeName;
|
||||||
|
contextEntry.contextId = event.contextId ?? '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'screencast-frame': {
|
case 'screencast-frame': {
|
||||||
|
|
@ -156,11 +157,11 @@ export class TraceModernizer {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'resource-snapshot':
|
case 'resource-snapshot':
|
||||||
this._snapshotStorage.addResource(event.snapshot);
|
this._snapshotStorage.addResource(this._contextEntry.contextId, event.snapshot);
|
||||||
contextEntry.resources.push(event.snapshot);
|
contextEntry.resources.push(event.snapshot);
|
||||||
break;
|
break;
|
||||||
case 'frame-snapshot':
|
case 'frame-snapshot':
|
||||||
this._snapshotStorage.addFrameSnapshot(event.snapshot, this._pageEntry(event.snapshot.pageId).screencastFrames);
|
this._snapshotStorage.addFrameSnapshot(this._contextEntry.contextId, event.snapshot, this._pageEntry(event.snapshot.pageId).screencastFrames);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Make sure there is a page entry for each page, even without screencast frames,
|
// Make sure there is a page entry for each page, even without screencast frames,
|
||||||
|
|
@ -388,12 +389,13 @@ export class TraceModernizer {
|
||||||
wallTime: 0,
|
wallTime: 0,
|
||||||
monotonicTime: 0,
|
monotonicTime: 0,
|
||||||
sdkLanguage: 'javascript',
|
sdkLanguage: 'javascript',
|
||||||
|
contextId: '',
|
||||||
};
|
};
|
||||||
result.push(event);
|
result.push(event);
|
||||||
}
|
}
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.type === 'context-options') {
|
if (event.type === 'context-options') {
|
||||||
result.push({ ...event, monotonicTime: 0, origin: 'library' });
|
result.push({ ...event, monotonicTime: 0, origin: 'library', contextId: '' });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Take wall and monotonic time from the first event.
|
// Take wall and monotonic time from the first event.
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export type ContextEntry = {
|
||||||
stdio: trace.StdioTraceEvent[];
|
stdio: trace.StdioTraceEvent[];
|
||||||
errors: trace.ErrorTraceEvent[];
|
errors: trace.ErrorTraceEvent[];
|
||||||
hasSource: boolean;
|
hasSource: boolean;
|
||||||
|
contextId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PageEntry = {
|
export type PageEntry = {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
},
|
},
|
||||||
"useUnknownInCatchVariables": false,
|
"useUnknownInCatchVariables": false,
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "../web/src"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export type ContextCreatedTraceEvent = {
|
||||||
options: BrowserContextEventOptions,
|
options: BrowserContextEventOptions,
|
||||||
sdkLanguage?: Language,
|
sdkLanguage?: Language,
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
|
contextId?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScreencastFrameTraceEvent = {
|
export type ScreencastFrameTraceEvent = {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extends: '../.eslintrc.js',
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
plugins: ["@typescript-eslint", "notice"],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 9,
|
|
||||||
sourceType: "module",
|
|
||||||
project: path.join(__dirname, 'tsconfig.json'),
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-floating-promises': 'error',
|
|
||||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "ct-react-vite",
|
"name": "ct-react-vite",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineConfig, devices } from '@playwright/experimental-ct-react';
|
import { defineConfig, devices } from '@playwright/experimental-ct-react';
|
||||||
import { resolve } from 'path';
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: 'tests',
|
testDir: 'tests',
|
||||||
|
|
@ -30,7 +31,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': resolve(__dirname, './src'),
|
'@': path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "ct-vue-vite",
|
"name": "ct-vue-vite",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineConfig, devices } from '@playwright/experimental-ct-vue';
|
import { defineConfig, devices } from '@playwright/experimental-ct-vue';
|
||||||
import { resolve } from 'path';
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: 'tests',
|
testDir: 'tests',
|
||||||
|
|
@ -27,7 +28,7 @@ export default defineConfig({
|
||||||
ctViteConfig: {
|
ctViteConfig: {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': resolve(__dirname, './src'),
|
'@': path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export class RemoteServer implements PlaywrightServer {
|
||||||
|
|
||||||
async _start(childProcess: CommonFixtures['childProcess'], browserType: BrowserType, channel: string, remoteServerOptions: RemoteServerOptions = {}) {
|
async _start(childProcess: CommonFixtures['childProcess'], browserType: BrowserType, channel: string, remoteServerOptions: RemoteServerOptions = {}) {
|
||||||
this._browserType = browserType;
|
this._browserType = browserType;
|
||||||
const browserOptions = (browserType as any)._defaultLaunchOptions;
|
const browserOptions = (browserType as any)._playwright._defaultLaunchOptions;
|
||||||
// Copy options to prevent a large JSON string when launching subprocess.
|
// Copy options to prevent a large JSON string when launching subprocess.
|
||||||
// Otherwise, we get `Error: spawn ENAMETOOLONG` on Windows.
|
// Otherwise, we get `Error: spawn ENAMETOOLONG` on Windows.
|
||||||
const launchOptions: Parameters<BrowserType['launchServer']>[0] = {
|
const launchOptions: Parameters<BrowserType['launchServer']>[0] = {
|
||||||
|
|
|
||||||
|
|
@ -1183,7 +1183,7 @@ it('should send secure cookie over http for localhost', async ({ page, server })
|
||||||
expect(serverRequest.headers.cookie).toBe('a=v');
|
expect(serverRequest.headers.cookie).toBe('a=v');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept bool and numeric params', async ({ page, server }) => {
|
it('should accept bool and numeric params and filter out undefined', async ({ page, server }) => {
|
||||||
let request;
|
let request;
|
||||||
const url = new URL(server.EMPTY_PAGE);
|
const url = new URL(server.EMPTY_PAGE);
|
||||||
url.searchParams.set('str', 's');
|
url.searchParams.set('str', 's');
|
||||||
|
|
@ -1200,6 +1200,7 @@ it('should accept bool and numeric params', async ({ page, server }) => {
|
||||||
'num': 10,
|
'num': 10,
|
||||||
'bool': true,
|
'bool': true,
|
||||||
'bool2': false,
|
'bool2': false,
|
||||||
|
'none': undefined,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const params = new URLSearchParams(request!.url.substr(request!.url.indexOf('?')));
|
const params = new URLSearchParams(request!.url.substr(request!.url.indexOf('?')));
|
||||||
|
|
@ -1207,6 +1208,7 @@ it('should accept bool and numeric params', async ({ page, server }) => {
|
||||||
expect(params.get('num')).toEqual('10');
|
expect(params.get('num')).toEqual('10');
|
||||||
expect(params.get('bool')).toEqual('true');
|
expect(params.get('bool')).toEqual('true');
|
||||||
expect(params.get('bool2')).toEqual('false');
|
expect(params.get('bool2')).toEqual('false');
|
||||||
|
expect(params.has('none')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should abort requests when browser context closes', async ({ contextFactory, server }) => {
|
it('should abort requests when browser context closes', async ({ contextFactory, server }) => {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import type { BrowserContext, Page } from '@playwright/test';
|
||||||
const test = browserTest.extend<{ reusedContext: () => Promise<BrowserContext> }>({
|
const test = browserTest.extend<{ reusedContext: () => Promise<BrowserContext> }>({
|
||||||
reusedContext: async ({ browserType, browser }, use) => {
|
reusedContext: async ({ browserType, browser }, use) => {
|
||||||
await use(async () => {
|
await use(async () => {
|
||||||
const defaultContextOptions = (browserType as any)._defaultContextOptions;
|
const defaultContextOptions = (browserType as any)._playwright._defaultContextOptions;
|
||||||
const context = await (browser as any)._newContextForReuse(defaultContextOptions);
|
const context = await (browser as any)._newContextForReuse(defaultContextOptions);
|
||||||
return context;
|
return context;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -90,15 +90,19 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const openRequest = indexedDB.open('db', 42);
|
const openRequest = indexedDB.open('db', 42);
|
||||||
openRequest.onupgradeneeded = () => {
|
openRequest.onupgradeneeded = () => {
|
||||||
openRequest.result.createObjectStore('store');
|
openRequest.result.createObjectStore('store', { keyPath: 'name' });
|
||||||
|
openRequest.result.createObjectStore('store2');
|
||||||
};
|
};
|
||||||
openRequest.onsuccess = () => {
|
openRequest.onsuccess = () => {
|
||||||
const request = openRequest.result.transaction('store', 'readwrite')
|
const transaction = openRequest.result.transaction(['store', 'store2'], 'readwrite');
|
||||||
|
transaction
|
||||||
.objectStore('store')
|
.objectStore('store')
|
||||||
.put({ name: 'foo', date: new Date(0) }, 'bar');
|
.put({ name: 'foo', date: new Date(0) });
|
||||||
request.addEventListener('success', resolve);
|
transaction
|
||||||
request.addEventListener('error', reject);
|
.objectStore('store2')
|
||||||
|
.put('bar', 'foo');
|
||||||
|
transaction.addEventListener('complete', resolve);
|
||||||
|
transaction.addEventListener('error', reject);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -120,18 +124,25 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
|
||||||
expect(localStorage).toEqual({ name1: 'value1' });
|
expect(localStorage).toEqual({ name1: 'value1' });
|
||||||
const cookie = await page2.evaluate('document.cookie');
|
const cookie = await page2.evaluate('document.cookie');
|
||||||
expect(cookie).toEqual('username=John Doe');
|
expect(cookie).toEqual('username=John Doe');
|
||||||
const idbValue = await page2.evaluate(() => new Promise<string>((resolve, reject) => {
|
const idbValues = await page2.evaluate(() => new Promise((resolve, reject) => {
|
||||||
const openRequest = indexedDB.open('db', 42);
|
const openRequest = indexedDB.open('db', 42);
|
||||||
openRequest.addEventListener('success', () => {
|
openRequest.addEventListener('success', () => {
|
||||||
const db = openRequest.result;
|
const db = openRequest.result;
|
||||||
const transaction = db.transaction('store', 'readonly');
|
const transaction = db.transaction(['store', 'store2'], 'readonly');
|
||||||
const getRequest = transaction.objectStore('store').get('bar');
|
const request1 = transaction.objectStore('store').get('foo');
|
||||||
getRequest.addEventListener('success', () => resolve(getRequest.result));
|
const request2 = transaction.objectStore('store2').get('foo');
|
||||||
getRequest.addEventListener('error', () => reject(getRequest.error));
|
|
||||||
|
Promise.all([request1, request2].map(request => new Promise((resolve, reject) => {
|
||||||
|
request.addEventListener('success', () => resolve(request.result));
|
||||||
|
request.addEventListener('error', () => reject(request.error));
|
||||||
|
}))).then(resolve, reject);
|
||||||
});
|
});
|
||||||
openRequest.addEventListener('error', () => reject(openRequest.error));
|
openRequest.addEventListener('error', () => reject(openRequest.error));
|
||||||
}));
|
}));
|
||||||
expect(idbValue).toEqual({ name: 'foo', date: new Date(0) });
|
expect(idbValues).toEqual([
|
||||||
|
{ name: 'foo', date: new Date(0) },
|
||||||
|
'bar'
|
||||||
|
]);
|
||||||
await context2.close();
|
await context2.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -436,4 +447,6 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
|
||||||
- listitem:
|
- listitem:
|
||||||
- text: /Pet the cat/
|
- text: /Pet the cat/
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
expect(await context.storageState({ indexedDB: false })).toEqual({ cookies: [], origins: [] });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { playwrightTest as test, expect } from '../config/browserTest';
|
||||||
test('browserType.executablePath should work', async ({ browserType, channel, mode }) => {
|
test('browserType.executablePath should work', async ({ browserType, channel, mode }) => {
|
||||||
test.skip(!!channel, 'We skip browser download when testing a channel');
|
test.skip(!!channel, 'We skip browser download when testing a channel');
|
||||||
test.skip(mode.startsWith('service'));
|
test.skip(mode.startsWith('service'));
|
||||||
test.skip(!!(browserType as any)._defaultLaunchOptions.executablePath, 'Skip with custom executable path');
|
test.skip(!!(browserType as any)._playwright._defaultLaunchOptions.executablePath, 'Skip with custom executable path');
|
||||||
|
|
||||||
const executablePath = browserType.executablePath();
|
const executablePath = browserType.executablePath();
|
||||||
expect(fs.existsSync(executablePath)).toBe(true);
|
expect(fs.existsSync(executablePath)).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ const test = playwrightTest.extend<ExtraFixtures>({
|
||||||
await use(async (wsEndpoint, options = {}, redirectPortForTest): Promise<Browser> => {
|
await use(async (wsEndpoint, options = {}, redirectPortForTest): Promise<Browser> => {
|
||||||
(options as any).__testHookRedirectPortForwarding = redirectPortForTest;
|
(options as any).__testHookRedirectPortForwarding = redirectPortForTest;
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'x-playwright-launch-options': JSON.stringify((browserType as any)._defaultLaunchOptions || {}),
|
'x-playwright-launch-options': JSON.stringify((browserType as any)._playwright._defaultLaunchOptions || {}),
|
||||||
...options.headers,
|
...options.headers,
|
||||||
};
|
};
|
||||||
browser = await browserType.connect(wsEndpoint, options);
|
browser = await browserType.connect(wsEndpoint, options);
|
||||||
|
|
@ -173,8 +173,8 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
test('should ignore page.pause when headed', async ({ connect, startRemoteServer, browserType, channel }) => {
|
test('should ignore page.pause when headed', async ({ connect, startRemoteServer, browserType, channel }) => {
|
||||||
test.skip(channel === 'chromium-headless-shell', 'shell is never headed');
|
test.skip(channel === 'chromium-headless-shell', 'shell is never headed');
|
||||||
|
|
||||||
const headless = (browserType as any)._defaultLaunchOptions.headless;
|
const headless = (browserType as any)._playwright._defaultLaunchOptions.headless;
|
||||||
(browserType as any)._defaultLaunchOptions.headless = false;
|
(browserType as any)._playwright._defaultLaunchOptions.headless = false;
|
||||||
const remoteServer = await startRemoteServer(kind);
|
const remoteServer = await startRemoteServer(kind);
|
||||||
const browser = await connect(remoteServer.wsEndpoint());
|
const browser = await connect(remoteServer.wsEndpoint());
|
||||||
const browserContext = await browser.newContext();
|
const browserContext = await browser.newContext();
|
||||||
|
|
@ -182,7 +182,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await page.pause({ __testHookKeepTestTimeout: true });
|
await page.pause({ __testHookKeepTestTimeout: true });
|
||||||
await browser.close();
|
await browser.close();
|
||||||
(browserType as any)._defaultLaunchOptions.headless = headless;
|
(browserType as any)._playwright._defaultLaunchOptions.headless = headless;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to visit ipv6 through localhost', async ({ connect, startRemoteServer, ipV6ServerPort }) => {
|
test('should be able to visit ipv6 through localhost', async ({ connect, startRemoteServer, ipV6ServerPort }) => {
|
||||||
|
|
@ -599,7 +599,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
const browser = await browserType.connect({
|
const browser = await browserType.connect({
|
||||||
wsEndpoint: remoteServer.wsEndpoint(),
|
wsEndpoint: remoteServer.wsEndpoint(),
|
||||||
headers: {
|
headers: {
|
||||||
'x-playwright-launch-options': JSON.stringify((browserType as any)._defaultLaunchOptions || {}),
|
'x-playwright-launch-options': JSON.stringify((browserType as any)._playwright._defaultLaunchOptions || {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|
@ -630,14 +630,14 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
|
|
||||||
test('should filter launch options', async ({ connect, startRemoteServer, server, browserType }, testInfo) => {
|
test('should filter launch options', async ({ connect, startRemoteServer, server, browserType }, testInfo) => {
|
||||||
const tracesDir = testInfo.outputPath('traces');
|
const tracesDir = testInfo.outputPath('traces');
|
||||||
const oldTracesDir = (browserType as any)._defaultLaunchOptions.tracesDir;
|
const oldTracesDir = (browserType as any)._playwright._defaultTracesDir;
|
||||||
(browserType as any)._defaultLaunchOptions.tracesDir = tracesDir;
|
(browserType as any)._playwright._defaultTracesDir = tracesDir;
|
||||||
const remoteServer = await startRemoteServer(kind);
|
const remoteServer = await startRemoteServer(kind);
|
||||||
const browser = await connect(remoteServer.wsEndpoint());
|
const browser = await connect(remoteServer.wsEndpoint());
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
(browserType as any)._defaultLaunchOptions.tracesDir = oldTracesDir;
|
(browserType as any)._playwright._defaultTracesDir = oldTracesDir;
|
||||||
expect(fs.existsSync(tracesDir)).toBe(false);
|
expect(fs.existsSync(tracesDir)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const test = baseTest.extend<Fixtures>({
|
||||||
await use(async () => {
|
await use(async () => {
|
||||||
const browser = await browserType.connect(wsEndpoint, {
|
const browser = await browserType.connect(wsEndpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-playwright-launch-options': JSON.stringify((browserType as any)._defaultLaunchOptions),
|
'x-playwright-launch-options': JSON.stringify((browserType as any)._playwright._defaultLaunchOptions),
|
||||||
'x-playwright-reuse-context': '1',
|
'x-playwright-reuse-context': '1',
|
||||||
},
|
},
|
||||||
}) as BrowserWithReuse;
|
}) as BrowserWithReuse;
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ it('should have passed URL when launching with ignoreDefaultArgs: true', async (
|
||||||
it.skip(mode !== 'default');
|
it.skip(mode !== 'default');
|
||||||
|
|
||||||
const userDataDir = await createUserDataDir();
|
const userDataDir = await createUserDataDir();
|
||||||
const args = toImpl(browserType).defaultArgs((browserType as any)._defaultLaunchOptions, 'persistent', userDataDir, 0).filter(a => a !== 'about:blank');
|
const args = toImpl(browserType).defaultArgs((browserType as any)._playwright._defaultLaunchOptions, 'persistent', userDataDir, 0).filter(a => a !== 'about:blank');
|
||||||
const options = {
|
const options = {
|
||||||
args: browserName === 'firefox' ? [...args, '-new-tab', server.EMPTY_PAGE] : [...args, server.EMPTY_PAGE],
|
args: browserName === 'firefox' ? [...args, '-new-tab', server.EMPTY_PAGE] : [...args, server.EMPTY_PAGE],
|
||||||
ignoreDefaultArgs: true,
|
ignoreDefaultArgs: true,
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ it('should set playwright as user-agent', async ({ playwright, server, isWindows
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to construct with context options', async ({ playwright, browserType, server }) => {
|
it('should be able to construct with context options', async ({ playwright, browserType, server }) => {
|
||||||
const request = await playwright.request.newContext((browserType as any)._defaultContextOptions);
|
const request = await playwright.request.newContext((browserType as any)._playwright._defaultContextOptions);
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.ok()).toBeTruthy();
|
expect(response.ok()).toBeTruthy();
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
|
|
|
||||||
|
|
@ -368,8 +368,8 @@ test('should not crash when browser closes mid-trace', async ({ browserType, ser
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should survive browser.close with auto-created traces dir', async ({ browserType }, testInfo) => {
|
test('should survive browser.close with auto-created traces dir', async ({ browserType }, testInfo) => {
|
||||||
const oldTracesDir = (browserType as any)._defaultLaunchOptions.tracesDir;
|
const oldTracesDir = (browserType as any)._playwright._defaultTracesDir;
|
||||||
(browserType as any)._defaultLaunchOptions.tracesDir = undefined;
|
(browserType as any)._playwright._defaultTracesDir = undefined;
|
||||||
const browser = await browserType.launch();
|
const browser = await browserType.launch();
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.context().tracing.start();
|
await page.context().tracing.start();
|
||||||
|
|
@ -394,7 +394,7 @@ test('should survive browser.close with auto-created traces dir', async ({ brows
|
||||||
]);
|
]);
|
||||||
|
|
||||||
done.value = true;
|
done.value = true;
|
||||||
(browserType as any)._defaultLaunchOptions.tracesDir = oldTracesDir;
|
(browserType as any)._playwright._defaultTracesDir = oldTracesDir;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not stall on dialogs', async ({ page, context, server }) => {
|
test('should not stall on dialogs', async ({ page, context, server }) => {
|
||||||
|
|
|
||||||
|
|
@ -420,3 +420,71 @@ test('should take screenshot when page is closed in afterEach', async ({ runInli
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fails', 'test-failed-1.png'))).toBeTruthy();
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fails', 'test-failed-1.png'))).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work with _pageSnapshot: on', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...testFiles,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { use: { _pageSnapshot: 'on' } };
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(5);
|
||||||
|
expect(result.failed).toBe(5);
|
||||||
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
|
'.last-run.json',
|
||||||
|
'artifacts-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
'artifacts-own-context-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
'artifacts-own-context-passing',
|
||||||
|
' test-finished-1.ariasnapshot',
|
||||||
|
'artifacts-passing',
|
||||||
|
' test-finished-1.ariasnapshot',
|
||||||
|
'artifacts-persistent-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
'artifacts-persistent-passing',
|
||||||
|
' test-finished-1.ariasnapshot',
|
||||||
|
'artifacts-shared-shared-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
' test-failed-2.ariasnapshot',
|
||||||
|
'artifacts-shared-shared-passing',
|
||||||
|
' test-finished-1.ariasnapshot',
|
||||||
|
' test-finished-2.ariasnapshot',
|
||||||
|
'artifacts-two-contexts',
|
||||||
|
' test-finished-1.ariasnapshot',
|
||||||
|
' test-finished-2.ariasnapshot',
|
||||||
|
'artifacts-two-contexts-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
' test-failed-2.ariasnapshot',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work with _pageSnapshot: only-on-failure', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...testFiles,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { use: { _pageSnapshot: 'only-on-failure' } };
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(5);
|
||||||
|
expect(result.failed).toBe(5);
|
||||||
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
|
'.last-run.json',
|
||||||
|
'artifacts-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
'artifacts-own-context-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
'artifacts-persistent-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
'artifacts-shared-shared-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
' test-failed-2.ariasnapshot',
|
||||||
|
'artifacts-two-contexts-failing',
|
||||||
|
' test-failed-1.ariasnapshot',
|
||||||
|
' test-failed-2.ariasnapshot',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1213,6 +1213,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
await execGit(['init']);
|
await execGit(['init']);
|
||||||
await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
|
await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
|
||||||
await execGit(['config', '--local', 'user.name', 'William']);
|
await execGit(['config', '--local', 'user.name', 'William']);
|
||||||
|
await execGit(['add', 'playwright.config.ts']);
|
||||||
|
await execGit(['commit', '-m', 'init']);
|
||||||
await execGit(['add', '*.ts']);
|
await execGit(['add', '*.ts']);
|
||||||
await execGit(['commit', '-m', 'chore(html): make this test look nice']);
|
await execGit(['commit', '-m', 'chore(html): make this test look nice']);
|
||||||
|
|
||||||
|
|
@ -1222,6 +1224,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
GITHUB_RUN_ID: 'example-run-id',
|
GITHUB_RUN_ID: 'example-run-id',
|
||||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||||
GITHUB_SHA: 'example-sha',
|
GITHUB_SHA: 'example-sha',
|
||||||
|
GITHUB_REF_NAME: '42/merge',
|
||||||
|
GITHUB_BASE_REF: 'HEAD~1',
|
||||||
});
|
});
|
||||||
|
|
||||||
await showReport();
|
await showReport();
|
||||||
|
|
@ -1231,7 +1235,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||||
- 'link "chore(html): make this test look nice"'
|
- 'link "chore(html): make this test look nice"'
|
||||||
- text: /^William <shakespeare@example.local> on/
|
- text: /^William <shakespeare@example.local> on/
|
||||||
- link "logs"
|
- link "Logs"
|
||||||
|
- link "Pull Request"
|
||||||
- link /^[a-f0-9]{7}$/
|
- link /^[a-f0-9]{7}$/
|
||||||
- text: 'foo: value1 bar: {"prop":"value2"} baz: ["value3",123]'
|
- text: 'foo: value1 bar: {"prop":"value2"} baz: ["value3",123]'
|
||||||
`);
|
`);
|
||||||
|
|
|
||||||
|
|
@ -137,29 +137,55 @@ class JSLintingService extends LintingService {
|
||||||
'vue-router',
|
'vue-router',
|
||||||
'experimental-ct',
|
'experimental-ct',
|
||||||
];
|
];
|
||||||
constructor() {
|
|
||||||
super();
|
async _init() {
|
||||||
this.eslint = new ESLint({
|
if (this._eslint)
|
||||||
overrideConfigFile: path.join(PROJECT_DIR, '.eslintrc.js'),
|
return this._eslint;
|
||||||
useEslintrc: false,
|
|
||||||
|
const { fixupConfigRules } = await import('@eslint/compat');
|
||||||
|
const { FlatCompat } = await import('@eslint/eslintrc');
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
const js = (await import('@eslint/js')).default;
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
// @ts-ignore
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all
|
||||||
|
});
|
||||||
|
const baseConfig = fixupConfigRules(compat.extends('plugin:react/recommended', 'plugin:@typescript-eslint/disable-type-checked'));
|
||||||
|
const { baseRules }= await import('../../../eslint.config.mjs');
|
||||||
|
|
||||||
|
this._eslint = new ESLint({
|
||||||
|
baseConfig,
|
||||||
|
plugins: /** @type {any}*/({
|
||||||
|
'@stylistic': (await import('@stylistic/eslint-plugin')).default,
|
||||||
|
'notice': await import('eslint-plugin-notice'),
|
||||||
|
}),
|
||||||
|
ignore: false,
|
||||||
overrideConfig: {
|
overrideConfig: {
|
||||||
plugins: ['react'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
settings: {
|
settings: {
|
||||||
react: { version: 'detect', }
|
react: { version: 'detect' },
|
||||||
},
|
},
|
||||||
extends: [
|
languageOptions: {
|
||||||
'plugin:react/recommended',
|
// @ts-ignore
|
||||||
],
|
parser: await import('@typescript-eslint/parser'),
|
||||||
rules: {
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
rules: /** @type {any}*/({
|
||||||
|
...baseRules,
|
||||||
'notice/notice': 'off',
|
'notice/notice': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'max-len': ['error', { code: 100 }],
|
'max-len': ['error', { code: 100 }],
|
||||||
'react/react-in-jsx-scope': 'off',
|
'react/react-in-jsx-scope': 'off',
|
||||||
'eol-last': 'off',
|
'eol-last': 'off',
|
||||||
},
|
'@typescript-eslint/consistent-type-imports': 'off',
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return this._eslint;
|
||||||
}
|
}
|
||||||
|
|
||||||
supports(codeLang) {
|
supports(codeLang) {
|
||||||
|
|
@ -171,9 +197,10 @@ class JSLintingService extends LintingService {
|
||||||
* @returns {Promise<LintResult>}
|
* @returns {Promise<LintResult>}
|
||||||
*/
|
*/
|
||||||
async _lintSnippet(snippet) {
|
async _lintSnippet(snippet) {
|
||||||
|
const eslint = await this._init();
|
||||||
if (this._knownBadSnippets.some(s => snippet.code.includes(s)))
|
if (this._knownBadSnippets.some(s => snippet.code.includes(s)))
|
||||||
return { status: 'ok' };
|
return { status: 'ok' };
|
||||||
const results = await this.eslint.lintText(snippet.code);
|
const results = await eslint.lintText(snippet.code, { filePath: path.join(__dirname, 'file.tsx') });
|
||||||
if (!results || !results.length || !results[0].messages.length)
|
if (!results || !results.length || !results[0].messages.length)
|
||||||
return { status: 'ok' };
|
return { status: 'ok' };
|
||||||
const result = results[0];
|
const result = results[0];
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,18 @@ Example:
|
||||||
console.log('\nUpdating browser version in browsers.json...');
|
console.log('\nUpdating browser version in browsers.json...');
|
||||||
for (const descriptor of descriptors)
|
for (const descriptor of descriptors)
|
||||||
descriptor.browserVersion = browserVersion;
|
descriptor.browserVersion = browserVersion;
|
||||||
|
|
||||||
|
// 4.1 chromium-headless-shell is equal to chromium version.
|
||||||
|
if (browserName === 'chromium') {
|
||||||
|
const headlessShellBrowser = await browsersJSON.browsers.find(b => b.name === 'chromium-headless-shell');
|
||||||
|
headlessShellBrowser.revision = revision;
|
||||||
|
headlessShellBrowser.browserVersion = browserVersion;
|
||||||
|
} else if (browserName === 'chromium-tip-of-tree') {
|
||||||
|
const tipOfTreeBrowser = await browsersJSON.browsers.find(b => b.name === 'chromium-tip-of-tree-headless-shell');
|
||||||
|
tipOfTreeBrowser.revision = revision;
|
||||||
|
tipOfTreeBrowser.browserVersion = browserVersion;
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(path.join(CORE_PATH, 'browsers.json'), JSON.stringify(browsersJSON, null, 2) + '\n');
|
fs.writeFileSync(path.join(CORE_PATH, 'browsers.json'), JSON.stringify(browsersJSON, null, 2) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue