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-%%
|
||||
* 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.
|
||||
|
||||
:::note
|
||||
IndexedDBs with typed arrays are currently not supported.
|
||||
:::
|
||||
|
||||
## async method: BrowserContext.storageState
|
||||
* since: v1.8
|
||||
* 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-%%
|
||||
* 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
|
||||
* since: v1.12
|
||||
- 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",
|
||||
"ct": "playwright test tests/components/test-all.spec.js --reporter=list",
|
||||
"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/",
|
||||
"build-installer": "babel -s --extensions \".ts\" --out-dir packages/playwright-core/lib/utils/ packages/playwright-core/src/utils",
|
||||
"doc": "node utils/doclint/cli.js",
|
||||
|
|
@ -62,6 +62,10 @@
|
|||
"@babel/plugin-transform-optional-chaining": "^7.23.4",
|
||||
"@babel/plugin-transform-typescript": "^7.23.6",
|
||||
"@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/codemirror": "^5.60.7",
|
||||
"@types/formidable": "^2.0.4",
|
||||
|
|
@ -71,9 +75,9 @@
|
|||
"@types/react-dom": "^18.0.5",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"@typescript-eslint/utils": "^7.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
||||
"@typescript-eslint/parser": "^8.23.0",
|
||||
"@typescript-eslint/utils": "^8.23.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@zip.js/zip.js": "^2.7.29",
|
||||
|
|
@ -86,10 +90,10 @@
|
|||
"dotenv": "^16.4.5",
|
||||
"electron": "^30.1.2",
|
||||
"esbuild": "^0.18.11",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-notice": "^0.9.10",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-notice": "^1.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"formidable": "^2.1.1",
|
||||
"immutable": "^4.3.7",
|
||||
"license-checker": "^25.0.1",
|
||||
|
|
@ -98,7 +102,7 @@
|
|||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.14",
|
||||
"ws": "^8.17.1",
|
||||
"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='mr-1'>{author}</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>
|
||||
{!!info['revision.link'] && <a href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@
|
|||
line-height: 24px;
|
||||
}
|
||||
|
||||
.test-case-run-duration {
|
||||
color: var(--color-fg-subtle);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.test-case-path {
|
||||
flex: none;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const testCase: TestCase = {
|
|||
],
|
||||
tags: [],
|
||||
outcome: 'expected',
|
||||
duration: 10,
|
||||
duration: 200,
|
||||
ok: true,
|
||||
results: [result]
|
||||
};
|
||||
|
|
@ -215,3 +215,37 @@ test('should correctly render prev and next', async ({ mount }) => {
|
|||
- 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.results.map((result, 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} />
|
||||
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@
|
|||
"@protocol/*": ["../protocol/src/*"],
|
||||
"@web/*": ["../web/src/*"],
|
||||
"@playwright/*": ["../playwright/src/*"],
|
||||
"@recorder/*": ["../recorder/src/*"],
|
||||
"@testIsomorphic/*": ["../playwright/src/isomorphic/*"],
|
||||
"playwright-core/lib/*": ["../playwright-core/src/*"],
|
||||
"playwright/lib/*": ["../playwright/src/*"],
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "../web/src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
extends: "../../.eslintrc-with-ts-config.js",
|
||||
};
|
||||
|
|
@ -7,12 +7,24 @@
|
|||
"installByDefault": true,
|
||||
"browserVersion": "133.0.6943.35"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1157",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "133.0.6943.35"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1300",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "134.0.6998.0"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree-headless-shell",
|
||||
"revision": "1300",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "134.0.6998.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1474",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ const semver = currentNodeVersion.split('.');
|
|||
const [major] = [+semver[0]];
|
||||
|
||||
if (major < minimumMajorNodeVersion) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'You are running Node.js ' +
|
||||
currentNodeVersion +
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
}
|
||||
|
||||
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 response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||
const context = BrowserContext.from(response.context);
|
||||
|
|
|
|||
|
|
@ -424,8 +424,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
});
|
||||
}
|
||||
|
||||
async storageState(options: { path?: string } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState();
|
||||
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
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 { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
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 { Events } from './events';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
|
@ -45,12 +45,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
_contexts = new Set<BrowserContext>();
|
||||
_playwright!: Playwright;
|
||||
|
||||
// Instrumentation.
|
||||
_defaultContextOptions?: BrowserContextOptions;
|
||||
_defaultContextTimeout?: number;
|
||||
_defaultContextNavigationTimeout?: number;
|
||||
private _defaultLaunchOptions?: LaunchOptions;
|
||||
|
||||
static from(browserType: channels.BrowserTypeChannel): BrowserType {
|
||||
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).port, 'Cannot specify a port without launching as a server.');
|
||||
|
||||
const logger = options.logger || this._defaultLaunchOptions?.logger;
|
||||
options = { ...this._defaultLaunchOptions, ...options };
|
||||
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...options };
|
||||
const launchOptions: channels.BrowserTypeLaunchParams = {
|
||||
...options,
|
||||
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> {
|
||||
if (!this._serverLauncher)
|
||||
throw new Error('Launching server is not supported');
|
||||
options = { ...this._defaultLaunchOptions, ...options };
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...options };
|
||||
return await this._serverLauncher.launchServer(options);
|
||||
}
|
||||
|
||||
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.');
|
||||
options = { ...this._defaultLaunchOptions, ...this._defaultContextOptions, ...options };
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
|
||||
const contextParams = await prepareBrowserContextParams(options, this);
|
||||
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
||||
...contextParams,
|
||||
|
|
@ -237,11 +231,10 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
context._browserType = this;
|
||||
this._contexts.add(context);
|
||||
context._setOptions(contextOptions, browserOptions);
|
||||
if (this._defaultContextTimeout !== undefined)
|
||||
context.setDefaultTimeout(this._defaultContextTimeout);
|
||||
if (this._defaultContextNavigationTimeout !== undefined)
|
||||
context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
|
||||
|
||||
if (this._playwright._defaultContextTimeout !== undefined)
|
||||
context.setDefaultTimeout(this._playwright._defaultContextTimeout);
|
||||
if (this._playwright._defaultContextNavigationTimeout !== undefined)
|
||||
context.setDefaultNavigationTimeout(this._playwright._defaultContextNavigationTimeout);
|
||||
await this._instrumentation.runAfterCreateBrowserContext(context);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { assert, headersObjectToArray, isString } from '../utils';
|
|||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
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 { Tracing } from './tracing';
|
||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||
|
|
@ -47,7 +47,7 @@ export type FetchOptions = {
|
|||
|
||||
export type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
||||
extraHTTPHeaders?: Headers,
|
||||
storageState?: string | StorageState,
|
||||
storageState?: string | SetStorageState,
|
||||
clientCertificates?: ClientCertificate[];
|
||||
};
|
||||
|
||||
|
|
@ -57,9 +57,6 @@ export class APIRequest implements api.APIRequest {
|
|||
private _playwright: Playwright;
|
||||
readonly _contexts = new Set<APIRequestContext>();
|
||||
|
||||
// Instrumentation.
|
||||
_defaultContextOptions?: NewContextOptions & { tracesDir?: string };
|
||||
|
||||
constructor(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> {
|
||||
options = { ...this._defaultContextOptions, ...options };
|
||||
options = {
|
||||
...this._playwright._defaultContextOptions,
|
||||
timeout: this._playwright._defaultContextTimeout,
|
||||
...options,
|
||||
};
|
||||
const storageState = typeof options.storageState === 'string' ?
|
||||
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
||||
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({
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
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),
|
||||
})).request);
|
||||
this._contexts.add(context);
|
||||
context._request = this;
|
||||
context._tracing._tracesDir = tracesDir;
|
||||
context._tracing._tracesDir = this._playwright._defaultLaunchOptions?.tracesDir;
|
||||
await context._instrumentation.runAfterCreateRequestContext(context);
|
||||
return context;
|
||||
}
|
||||
|
|
@ -264,8 +263,8 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
});
|
||||
}
|
||||
|
||||
async storageState(options: { path?: string } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState();
|
||||
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
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)
|
||||
return undefined;
|
||||
const result = [];
|
||||
for (const [name, value] of Object.entries(map))
|
||||
result.push({ name, value: String(value) });
|
||||
for (const [name, value] of Object.entries(map)) {
|
||||
if (value !== undefined)
|
||||
result.push({ name, value: String(value) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { Electron } from './electron';
|
|||
import { APIRequest } from './fetch';
|
||||
import { Selectors, SelectorsOwner } from './selectors';
|
||||
import { MockingProxy } from './mockingProxy';
|
||||
import type { BrowserContextOptions, LaunchOptions } from 'playwright-core';
|
||||
|
||||
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||
readonly _android: Android;
|
||||
|
|
@ -38,6 +39,12 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
|||
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||
_mockingProxy?: MockingProxy;
|
||||
|
||||
// Instrumentation.
|
||||
_defaultLaunchOptions?: LaunchOptions;
|
||||
_defaultContextOptions?: BrowserContextOptions;
|
||||
_defaultContextTimeout?: number;
|
||||
_defaultContextNavigationTimeout?: number;
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.request = new APIRequest(this);
|
||||
|
|
@ -76,6 +83,19 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
|||
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() {
|
||||
const requestContext = await this.request._newContext(undefined, this._connection.localUtils()._channel);
|
||||
const result = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel });
|
||||
|
|
|
|||
|
|
@ -234,7 +234,9 @@ scheme.APIRequestContextFetchLogParams = tObject({
|
|||
scheme.APIRequestContextFetchLogResult = tObject({
|
||||
log: tArray(tString),
|
||||
});
|
||||
scheme.APIRequestContextStorageStateParams = tOptional(tObject({}));
|
||||
scheme.APIRequestContextStorageStateParams = tObject({
|
||||
indexedDB: tOptional(tBoolean),
|
||||
});
|
||||
scheme.APIRequestContextStorageStateResult = tObject({
|
||||
cookies: tArray(tType('NetworkCookie')),
|
||||
origins: tArray(tType('OriginStorage')),
|
||||
|
|
@ -1058,7 +1060,9 @@ scheme.BrowserContextSetOfflineParams = tObject({
|
|||
offline: tBoolean,
|
||||
});
|
||||
scheme.BrowserContextSetOfflineResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextStorageStateParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextStorageStateParams = tObject({
|
||||
indexedDB: tOptional(tBoolean),
|
||||
});
|
||||
scheme.BrowserContextStorageStateResult = tObject({
|
||||
cookies: tArray(tType('NetworkCookie')),
|
||||
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> {
|
||||
// Bidi throws when x/y are not integers.
|
||||
x = Math.round(x);
|
||||
y = Math.round(y);
|
||||
x = Math.floor(x);
|
||||
y = Math.floor(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);
|
||||
}
|
||||
|
||||
async storageState(): Promise<channels.BrowserContextStorageStateResult> {
|
||||
async storageState(indexedDB = true): Promise<channels.BrowserContextStorageStateResult> {
|
||||
const result: channels.BrowserContextStorageStateResult = {
|
||||
cookies: await this.cookies(),
|
||||
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.
|
||||
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> {
|
||||
return await this._context.storageState();
|
||||
return await this._context.storageState(params.indexedDB);
|
||||
}
|
||||
|
||||
async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -197,8 +197,8 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
|
|||
this.adopt(tracing);
|
||||
}
|
||||
|
||||
async storageState(): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
return this._object.storageState();
|
||||
async storageState(params: channels.APIRequestContextStorageStateParams): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
return this._object.storageState(params.indexedDB);
|
||||
}
|
||||
|
||||
async dispose(params: channels.APIRequestContextDisposeParams, metadata: CallMetadata): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
abstract _defaultOptions(): FetchRequestOptions;
|
||||
abstract _addCookies(cookies: channels.NetworkCookie[]): Promise<void>;
|
||||
abstract _cookies(url: URL): Promise<channels.NetworkCookie[]>;
|
||||
abstract storageState(): Promise<channels.APIRequestContextStorageStateResult>;
|
||||
abstract storageState(indexedDB?: boolean): Promise<channels.APIRequestContextStorageStateResult>;
|
||||
|
||||
private _storeResponseBody(body: Buffer): string {
|
||||
const uid = createGuid();
|
||||
|
|
@ -618,8 +618,8 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
|
|||
return await this._context.cookies(url.toString());
|
||||
}
|
||||
|
||||
override async storageState(): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
return this._context.storageState();
|
||||
override async storageState(indexedDB?: boolean): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
return this._context.storageState(indexedDB);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -684,10 +684,10 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
|||
return this._cookieStore.cookies(url);
|
||||
}
|
||||
|
||||
override async storageState(): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
override async storageState(indexedDB = true): Promise<channels.APIRequestContextStorageStateResult> {
|
||||
return {
|
||||
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[] {
|
||||
const headlessShells: BrowsersJSON['browsers'] = [];
|
||||
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 => {
|
||||
return (browsersJSON['browsers']).map(obj => {
|
||||
const name = obj.name;
|
||||
const revisionOverride = (obj.revisionOverrides || {})[hostPlatform];
|
||||
const revision = revisionOverride || obj.revision;
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import type { source } from './isomorphic/utilityScriptSerializers';
|
|||
|
||||
export type Storage = Omit<channels.OriginStorage, 'origin'>;
|
||||
|
||||
export async function collect(serializers: ReturnType<typeof source>, isFirefox: boolean): Promise<Storage> {
|
||||
const idbResult = await Promise.all((await indexedDB.databases()).map(async dbInfo => {
|
||||
export async function collect(serializers: ReturnType<typeof source>, isFirefox: boolean, recordIndexedDB: boolean): Promise<Storage> {
|
||||
async function collectDB(dbInfo: IDBDatabaseInfo) {
|
||||
if (!dbInfo.name)
|
||||
throw new Error('Database name is empty');
|
||||
if (!dbInfo.version)
|
||||
|
|
@ -119,13 +119,13 @@ export async function collect(serializers: ReturnType<typeof source>, isFirefox:
|
|||
version: dbInfo.version,
|
||||
stores,
|
||||
};
|
||||
})).catch(e => {
|
||||
throw new Error('Unable to serialize IndexedDB: ' + e.message);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
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,
|
||||
monotonicTime: 0,
|
||||
sdkLanguage: context.attribution.playwright.options.sdkLanguage,
|
||||
testIdAttributeName
|
||||
testIdAttributeName,
|
||||
contextId: context.guid,
|
||||
};
|
||||
if (context instanceof BrowserContext) {
|
||||
this._snapshotter = new Snapshotter(context, this);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
|||
}
|
||||
|
||||
onEntryFinished(entry: har.Entry) {
|
||||
this._storage.addResource(entry);
|
||||
this._storage.addResource('', entry);
|
||||
}
|
||||
|
||||
onContentBlob(sha1: string, buffer: Buffer) {
|
||||
|
|
@ -85,7 +85,7 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
|||
|
||||
onFrameSnapshot(snapshot: FrameSnapshot): void {
|
||||
++this._snapshotCount;
|
||||
const renderer = this._storage.addFrameSnapshot(snapshot, []);
|
||||
const renderer = this._storage.addFrameSnapshot('', snapshot, []);
|
||||
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
|
||||
* snapshot.
|
||||
*
|
||||
* **NOTE** IndexedDBs with typed arrays are currently not supported.
|
||||
*
|
||||
* @param 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
|
||||
* [`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
|
||||
*/
|
||||
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
|
||||
* [`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 type { ContextReuseMode } from './common/config';
|
||||
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 { Playwright as PlaywrightImpl } from '../../playwright-core/src/client/playwright';
|
||||
import { currentTestInfo } from './common/globals';
|
||||
import type { Playwright as PlaywrightImpl } from 'playwright-core/lib/client/playwright';
|
||||
export { expect } from './matchers/expect';
|
||||
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
||||
|
||||
|
|
@ -53,11 +52,13 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
|||
};
|
||||
|
||||
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||
playwright: PlaywrightImpl;
|
||||
_browserOptions: LaunchOptions;
|
||||
_optionContextReuseMode: ContextReuseMode,
|
||||
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
||||
_reuseContext: boolean,
|
||||
_mockingProxy?: void,
|
||||
_pageSnapshot: PageSnapshotOption,
|
||||
};
|
||||
|
||||
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||
|
|
@ -76,23 +77,22 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
video: ['off', { scope: 'worker', option: true }],
|
||||
trace: ['off', { scope: 'worker', option: true }],
|
||||
mockingProxy: [undefined, { scope: 'worker', option: true }],
|
||||
_pageSnapshot: ['off', { scope: 'worker', option: true }],
|
||||
|
||||
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
||||
const options: LaunchOptions = {
|
||||
handleSIGINT: false,
|
||||
...launchOptions,
|
||||
tracesDir: tracing().tracesDir(),
|
||||
};
|
||||
if (headless !== undefined)
|
||||
options.headless = headless;
|
||||
if (channel !== undefined)
|
||||
options.channel = channel;
|
||||
options.tracesDir = tracing().tracesDir();
|
||||
|
||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit, playwright._bidiChromium, playwright._bidiFirefox])
|
||||
(browserType as any)._defaultLaunchOptions = options;
|
||||
playwright._defaultLaunchOptions = options;
|
||||
await use(options);
|
||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit, playwright._bidiChromium, playwright._bidiFirefox])
|
||||
(browserType as any)._defaultLaunchOptions = undefined;
|
||||
playwright._defaultLaunchOptions = undefined;
|
||||
}, { scope: 'worker', auto: true, box: true }],
|
||||
|
||||
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
|
||||
|
|
@ -241,30 +241,23 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
testInfo.snapshotSuffix = process.platform;
|
||||
if (debugMode())
|
||||
(testInfo as TestInfoImpl)._setDebugMode();
|
||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
||||
(browserType as any)._defaultContextOptions = _combinedContextOptions;
|
||||
(browserType as any)._defaultContextTimeout = actionTimeout || 0;
|
||||
(browserType as any)._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;
|
||||
|
||||
playwright._defaultContextOptions = _combinedContextOptions;
|
||||
playwright._defaultContextTimeout = actionTimeout || 0;
|
||||
playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
|
||||
await use();
|
||||
(playwright.request as any)._defaultContextOptions = undefined;
|
||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
||||
(browserType as any)._defaultContextOptions = undefined;
|
||||
(browserType as any)._defaultContextTimeout = undefined;
|
||||
(browserType as any)._defaultContextNavigationTimeout = undefined;
|
||||
}
|
||||
playwright._defaultContextOptions = undefined;
|
||||
playwright._defaultContextTimeout = undefined;
|
||||
playwright._defaultContextNavigationTimeout = undefined;
|
||||
}, { 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
|
||||
// happens even after some fixtures or hooks time out.
|
||||
// Now that default test timeout is known, we can replace zero with an actual value.
|
||||
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);
|
||||
|
||||
const tracingGroupSteps: TestStepInternal[] = [];
|
||||
|
|
@ -462,7 +455,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
});
|
||||
|
||||
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 {
|
||||
if (!video)
|
||||
|
|
@ -534,54 +527,135 @@ function connectOptionsFromEnv() {
|
|||
};
|
||||
}
|
||||
|
||||
class ArtifactsRecorder {
|
||||
private _testInfo!: TestInfoImpl;
|
||||
private _playwright: Playwright;
|
||||
private _artifactsDir: string;
|
||||
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;
|
||||
class SnapshotRecorder {
|
||||
private _ordinal = 0;
|
||||
private _temporary: string[] = [];
|
||||
private _snapshottedSymbol = Symbol('snapshotted');
|
||||
|
||||
constructor(playwright: Playwright, artifactsDir: string, screenshot: ScreenshotOption) {
|
||||
this._playwright = playwright;
|
||||
this._artifactsDir = artifactsDir;
|
||||
this._screenshotMode = normalizeScreenshotMode(screenshot);
|
||||
this._screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot;
|
||||
this._screenshottedSymbol = Symbol('screenshotted');
|
||||
this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
||||
constructor(
|
||||
private _artifactsRecorder: ArtifactsRecorder,
|
||||
private _mode: ScreenshotMode | PageSnapshotOption,
|
||||
private _name: string,
|
||||
private _contentType: string,
|
||||
private _extension: string,
|
||||
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[]) {
|
||||
const file = path.join(this._artifactsDir, ...name);
|
||||
this._temporaryArtifacts.push(file);
|
||||
const file = path.join(this._artifactsRecorder._artifactsDir, ...name);
|
||||
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) {
|
||||
this._testInfo = testInfo;
|
||||
testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction();
|
||||
|
||||
// Since beforeAll(s), test and afterAll(s) reuse the same TestInfo, make sure we do not
|
||||
// overwrite previous screenshots.
|
||||
this._screenshotOrdinal = testInfo.attachments.filter(a => a.name === 'screenshot').length;
|
||||
this._screenshotRecorder.fixOrdinal();
|
||||
this._pageSnapshotRecorder.fixOrdinal();
|
||||
|
||||
// Process existing contexts.
|
||||
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) {
|
||||
const promises: (Promise<void> | undefined)[] = [];
|
||||
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
||||
for (const context of existingContexts) {
|
||||
if ((context as any)[kIsReusedContext])
|
||||
this._reusedContexts.add(context);
|
||||
else
|
||||
promises.push(this.didCreateBrowserContext(context));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
await Promise.all(this._playwright._allContexts().map(async context => {
|
||||
if ((context as any)[kIsReusedContext])
|
||||
this._reusedContexts.add(context);
|
||||
else
|
||||
await this.didCreateBrowserContext(context);
|
||||
}));
|
||||
{
|
||||
const existingApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||
await Promise.all(existingApiRequests.map(c => this.didCreateRequestContext(c)));
|
||||
|
|
@ -598,11 +672,9 @@ class ArtifactsRecorder {
|
|||
if (this._reusedContexts.has(context))
|
||||
return;
|
||||
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
|
||||
// after the test finishes.
|
||||
await Promise.all(context.pages().map(page => this._screenshotPage(page, true)));
|
||||
}
|
||||
|
||||
await this._screenshotRecorder.captureTemporary(context);
|
||||
await this._pageSnapshotRecorder.captureTemporary(context);
|
||||
}
|
||||
|
||||
async didCreateRequestContext(context: APIRequestContext) {
|
||||
|
|
@ -615,26 +687,15 @@ class ArtifactsRecorder {
|
|||
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() {
|
||||
if (this._shouldCaptureScreenshotUponFinish())
|
||||
await this._screenshotOnTestFailure();
|
||||
await this._screenshotRecorder.maybeCapture();
|
||||
await this._pageSnapshotRecorder.maybeCapture();
|
||||
}
|
||||
|
||||
async didFinishTest() {
|
||||
const captureScreenshots = this._shouldCaptureScreenshotUponFinish();
|
||||
if (captureScreenshots)
|
||||
await this._screenshotOnTestFailure();
|
||||
await this.didFinishTestFunction();
|
||||
|
||||
let leftoverContexts: BrowserContext[] = [];
|
||||
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 leftoverContexts = this._playwright._allContexts().filter(context => !this._reusedContexts.has(context));
|
||||
const leftoverApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||
|
||||
// Collect traces/screenshots for remaining contexts.
|
||||
|
|
@ -645,55 +706,8 @@ class ArtifactsRecorder {
|
|||
await this._stopTracing(tracing);
|
||||
})));
|
||||
|
||||
// Attach temporary screenshots for contexts closed before collecting the test trace.
|
||||
if (captureScreenshots) {
|
||||
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)));
|
||||
await this._screenshotRecorder.persistTemporary();
|
||||
await this._pageSnapshotRecorder.persistTemporary();
|
||||
}
|
||||
|
||||
private async _startTraceChunkOnContextCreation(tracing: Tracing) {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ export interface TestServerInterface {
|
|||
workers?: number | string;
|
||||
updateSnapshots?: 'all' | 'changed' | 'missing' | 'none';
|
||||
updateSourceMethod?: 'overwrite' | 'patch' | '3way';
|
||||
pageSnapshot?: 'off' | 'on' | 'only-on-failure';
|
||||
reporters?: string[],
|
||||
trace?: 'on' | 'off';
|
||||
video?: 'on' | 'off';
|
||||
|
|
|
|||
|
|
@ -21,5 +21,9 @@ export interface GitCommitInfo {
|
|||
'revision.subject'?: string;
|
||||
'revision.timestamp'?: number | Date;
|
||||
'revision.link'?: string;
|
||||
'revision.diff'?: string;
|
||||
'pull.link'?: string;
|
||||
'pull.diff'?: string;
|
||||
'pull.base'?: string;
|
||||
'ci.link'?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,13 +33,9 @@ export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerP
|
|||
|
||||
setup: async (config: FullConfig, configDir: string) => {
|
||||
const fromEnv = linksFromEnv();
|
||||
const fromCLI = await gitStatusFromCLI(options?.directory || configDir);
|
||||
const info = { ...fromEnv, ...fromCLI };
|
||||
if (info['revision.timestamp'] instanceof Date)
|
||||
info['revision.timestamp'] = info['revision.timestamp'].getTime();
|
||||
|
||||
const fromCLI = await gitStatusFromCLI(options?.directory || configDir, fromEnv);
|
||||
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;
|
||||
}
|
||||
|
||||
function linksFromEnv(): Pick<GitCommitInfo, 'revision.link' | 'ci.link'> {
|
||||
const out: { 'revision.link'?: string; 'ci.link'?: string; } = {};
|
||||
function linksFromEnv() {
|
||||
const out: Partial<GitCommitInfo> = {};
|
||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
||||
if (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}`;
|
||||
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}`;
|
||||
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;
|
||||
}
|
||||
|
||||
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 { code, stdout } = await spawnAsync(
|
||||
const commitInfoResult = await spawnAsync(
|
||||
'git',
|
||||
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
|
||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||
);
|
||||
if (code)
|
||||
if (commitInfoResult.code)
|
||||
return;
|
||||
const showOutput = stdout.trim();
|
||||
const showOutput = commitInfoResult.stdout.trim();
|
||||
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
|
||||
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
||||
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
||||
|
||||
return {
|
||||
const result: GitCommitInfo = {
|
||||
'revision.id': id,
|
||||
'revision.author': author,
|
||||
'revision.email': email,
|
||||
'revision.subject': subject,
|
||||
'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 } : {}),
|
||||
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
||||
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
||||
_pageSnapshot: params.pageSnapshot,
|
||||
},
|
||||
...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}),
|
||||
...(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>;
|
||||
fetchResponseBody(params: APIRequestContextFetchResponseBodyParams, metadata?: CallMetadata): Promise<APIRequestContextFetchResponseBodyResult>;
|
||||
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>;
|
||||
dispose(params: APIRequestContextDisposeParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeResult>;
|
||||
}
|
||||
|
|
@ -405,8 +405,12 @@ export type APIRequestContextFetchLogOptions = {
|
|||
export type APIRequestContextFetchLogResult = {
|
||||
log: string[],
|
||||
};
|
||||
export type APIRequestContextStorageStateParams = {};
|
||||
export type APIRequestContextStorageStateOptions = {};
|
||||
export type APIRequestContextStorageStateParams = {
|
||||
indexedDB?: boolean,
|
||||
};
|
||||
export type APIRequestContextStorageStateOptions = {
|
||||
indexedDB?: boolean,
|
||||
};
|
||||
export type APIRequestContextStorageStateResult = {
|
||||
cookies: NetworkCookie[],
|
||||
origins: OriginStorage[],
|
||||
|
|
@ -1688,7 +1692,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
|
|||
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
|
||||
setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetWebSocketInterceptionPatternsResult>;
|
||||
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>;
|
||||
enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise<BrowserContextEnableRecorderResult>;
|
||||
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
||||
|
|
@ -1921,8 +1925,12 @@ export type BrowserContextSetOfflineOptions = {
|
|||
|
||||
};
|
||||
export type BrowserContextSetOfflineResult = void;
|
||||
export type BrowserContextStorageStateParams = {};
|
||||
export type BrowserContextStorageStateOptions = {};
|
||||
export type BrowserContextStorageStateParams = {
|
||||
indexedDB?: boolean,
|
||||
};
|
||||
export type BrowserContextStorageStateOptions = {
|
||||
indexedDB?: boolean,
|
||||
};
|
||||
export type BrowserContextStorageStateResult = {
|
||||
cookies: NetworkCookie[],
|
||||
origins: OriginStorage[],
|
||||
|
|
|
|||
|
|
@ -376,6 +376,8 @@ APIRequestContext:
|
|||
items: string
|
||||
|
||||
storageState:
|
||||
parameters:
|
||||
indexedDB: boolean?
|
||||
returns:
|
||||
cookies:
|
||||
type: array
|
||||
|
|
@ -1285,6 +1287,8 @@ BrowserContext:
|
|||
offline: boolean
|
||||
|
||||
storageState:
|
||||
parameters:
|
||||
indexedDB: boolean?
|
||||
returns:
|
||||
cookies:
|
||||
type: array
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@
|
|||
"@web/*": ["../web/src/*"],
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "../web/src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,14 +120,16 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
|||
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
||||
if (!snapshotServer)
|
||||
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/')) {
|
||||
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
||||
if (!snapshotServer)
|
||||
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)
|
||||
response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests');
|
||||
return response;
|
||||
|
|
@ -137,7 +139,8 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
|||
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
||||
if (!snapshotServer)
|
||||
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/')) {
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ export class SnapshotServer {
|
|||
this._resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
serveSnapshot(pathname: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
|
||||
const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams);
|
||||
serveSnapshot(pageOrFrameId: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
|
||||
const snapshot = this._snapshot(pageOrFrameId, searchParams);
|
||||
if (!snapshot)
|
||||
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' } });
|
||||
}
|
||||
|
||||
async serveClosestScreenshot(pathname: string, searchParams: URLSearchParams): Promise<Response> {
|
||||
const snapshot = this._snapshot(pathname.substring('/closest-screenshot'.length), searchParams);
|
||||
async serveClosestScreenshot(pageOrFrameId: string, searchParams: URLSearchParams): Promise<Response> {
|
||||
const snapshot = this._snapshot(pageOrFrameId, searchParams);
|
||||
const sha1 = snapshot?.closestScreenshot();
|
||||
if (!sha1)
|
||||
return new Response(null, { status: 404 });
|
||||
return new Response(await this._resourceLoader(sha1));
|
||||
}
|
||||
|
||||
serveSnapshotInfo(pathname: string, searchParams: URLSearchParams): Response {
|
||||
const snapshot = this._snapshot(pathname.substring('/snapshotInfo'.length), searchParams);
|
||||
serveSnapshotInfo(pageOrFrameId: string, searchParams: URLSearchParams): Response {
|
||||
const snapshot = this._snapshot(pageOrFrameId, searchParams);
|
||||
return this._respondWithJson(snapshot ? {
|
||||
viewport: snapshot.viewport(),
|
||||
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')!;
|
||||
return this._snapshotStorage.snapshotByName(pathname.slice(1), name);
|
||||
return this._snapshotStorage.snapshotByName(pageOrFrameId, name);
|
||||
}
|
||||
|
||||
private _respondWithJson(object: any): Response {
|
||||
|
|
|
|||
|
|
@ -20,19 +20,19 @@ import type { PageEntry } from '../types/entries';
|
|||
import { LRUCache } from './lruCache';
|
||||
|
||||
export class SnapshotStorage {
|
||||
private _resources: ResourceSnapshot[] = [];
|
||||
private _frameSnapshots = new Map<string, {
|
||||
raw: FrameSnapshot[],
|
||||
renderers: SnapshotRenderer[]
|
||||
renderers: SnapshotRenderer[],
|
||||
}>();
|
||||
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);
|
||||
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)
|
||||
override.url = rewriteURLForCustomProtocol(override.url);
|
||||
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
|
||||
|
|
@ -46,7 +46,8 @@ export class SnapshotStorage {
|
|||
this._frameSnapshots.set(snapshot.pageId, frameSnapshots);
|
||||
}
|
||||
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);
|
||||
return renderer;
|
||||
}
|
||||
|
|
@ -62,6 +63,16 @@ export class SnapshotStorage {
|
|||
|
||||
finalize() {
|
||||
// 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._snapshotStorage!.finalize();
|
||||
this._snapshotStorage.finalize();
|
||||
}
|
||||
|
||||
async hasEntry(filename: string): Promise<boolean> {
|
||||
|
|
@ -153,5 +153,6 @@ function createEmptyContext(): ContextEntry {
|
|||
errors: [],
|
||||
stdio: [],
|
||||
hasSource: false,
|
||||
contextId: '',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export class TraceModernizer {
|
|||
contextEntry.sdkLanguage = event.sdkLanguage;
|
||||
contextEntry.options = event.options;
|
||||
contextEntry.testIdAttributeName = event.testIdAttributeName;
|
||||
contextEntry.contextId = event.contextId ?? '';
|
||||
break;
|
||||
}
|
||||
case 'screencast-frame': {
|
||||
|
|
@ -156,11 +157,11 @@ export class TraceModernizer {
|
|||
break;
|
||||
}
|
||||
case 'resource-snapshot':
|
||||
this._snapshotStorage.addResource(event.snapshot);
|
||||
this._snapshotStorage.addResource(this._contextEntry.contextId, event.snapshot);
|
||||
contextEntry.resources.push(event.snapshot);
|
||||
break;
|
||||
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;
|
||||
}
|
||||
// Make sure there is a page entry for each page, even without screencast frames,
|
||||
|
|
@ -388,12 +389,13 @@ export class TraceModernizer {
|
|||
wallTime: 0,
|
||||
monotonicTime: 0,
|
||||
sdkLanguage: 'javascript',
|
||||
contextId: '',
|
||||
};
|
||||
result.push(event);
|
||||
}
|
||||
for (const event of events) {
|
||||
if (event.type === 'context-options') {
|
||||
result.push({ ...event, monotonicTime: 0, origin: 'library' });
|
||||
result.push({ ...event, monotonicTime: 0, origin: 'library', contextId: '' });
|
||||
continue;
|
||||
}
|
||||
// Take wall and monotonic time from the first event.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export type ContextEntry = {
|
|||
stdio: trace.StdioTraceEvent[];
|
||||
errors: trace.ErrorTraceEvent[];
|
||||
hasSource: boolean;
|
||||
contextId: string;
|
||||
};
|
||||
|
||||
export type PageEntry = {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@
|
|||
},
|
||||
"useUnknownInCatchVariables": false,
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "../web/src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export type ContextCreatedTraceEvent = {
|
|||
options: BrowserContextEventOptions,
|
||||
sdkLanguage?: Language,
|
||||
testIdAttributeName?: string,
|
||||
contextId?: string,
|
||||
};
|
||||
|
||||
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",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
*/
|
||||
|
||||
import { defineConfig, devices } from '@playwright/experimental-ct-react';
|
||||
import { resolve } from 'path';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: 'tests',
|
||||
|
|
@ -30,7 +31,7 @@ export default defineConfig({
|
|||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
'@': path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"name": "ct-vue-vite",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
*/
|
||||
|
||||
import { defineConfig, devices } from '@playwright/experimental-ct-vue';
|
||||
import { resolve } from 'path';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: 'tests',
|
||||
|
|
@ -27,7 +28,7 @@ export default defineConfig({
|
|||
ctViteConfig: {
|
||||
resolve: {
|
||||
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 = {}) {
|
||||
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.
|
||||
// Otherwise, we get `Error: spawn ENAMETOOLONG` on Windows.
|
||||
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');
|
||||
});
|
||||
|
||||
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;
|
||||
const url = new URL(server.EMPTY_PAGE);
|
||||
url.searchParams.set('str', 's');
|
||||
|
|
@ -1200,6 +1200,7 @@ it('should accept bool and numeric params', async ({ page, server }) => {
|
|||
'num': 10,
|
||||
'bool': true,
|
||||
'bool2': false,
|
||||
'none': undefined,
|
||||
}
|
||||
});
|
||||
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('bool')).toEqual('true');
|
||||
expect(params.get('bool2')).toEqual('false');
|
||||
expect(params.has('none')).toBe(false);
|
||||
});
|
||||
|
||||
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> }>({
|
||||
reusedContext: async ({ browserType, browser }, use) => {
|
||||
await use(async () => {
|
||||
const defaultContextOptions = (browserType as any)._defaultContextOptions;
|
||||
const defaultContextOptions = (browserType as any)._playwright._defaultContextOptions;
|
||||
const context = await (browser as any)._newContextForReuse(defaultContextOptions);
|
||||
return context;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,15 +90,19 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
|
|||
await new Promise((resolve, reject) => {
|
||||
const openRequest = indexedDB.open('db', 42);
|
||||
openRequest.onupgradeneeded = () => {
|
||||
openRequest.result.createObjectStore('store');
|
||||
|
||||
openRequest.result.createObjectStore('store', { keyPath: 'name' });
|
||||
openRequest.result.createObjectStore('store2');
|
||||
};
|
||||
openRequest.onsuccess = () => {
|
||||
const request = openRequest.result.transaction('store', 'readwrite')
|
||||
const transaction = openRequest.result.transaction(['store', 'store2'], 'readwrite');
|
||||
transaction
|
||||
.objectStore('store')
|
||||
.put({ name: 'foo', date: new Date(0) }, 'bar');
|
||||
request.addEventListener('success', resolve);
|
||||
request.addEventListener('error', reject);
|
||||
.put({ name: 'foo', date: new Date(0) });
|
||||
transaction
|
||||
.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' });
|
||||
const cookie = await page2.evaluate('document.cookie');
|
||||
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);
|
||||
openRequest.addEventListener('success', () => {
|
||||
const db = openRequest.result;
|
||||
const transaction = db.transaction('store', 'readonly');
|
||||
const getRequest = transaction.objectStore('store').get('bar');
|
||||
getRequest.addEventListener('success', () => resolve(getRequest.result));
|
||||
getRequest.addEventListener('error', () => reject(getRequest.error));
|
||||
const transaction = db.transaction(['store', 'store2'], 'readonly');
|
||||
const request1 = transaction.objectStore('store').get('foo');
|
||||
const request2 = transaction.objectStore('store2').get('foo');
|
||||
|
||||
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));
|
||||
}));
|
||||
expect(idbValue).toEqual({ name: 'foo', date: new Date(0) });
|
||||
expect(idbValues).toEqual([
|
||||
{ name: 'foo', date: new Date(0) },
|
||||
'bar'
|
||||
]);
|
||||
await context2.close();
|
||||
});
|
||||
|
||||
|
|
@ -436,4 +447,6 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
|
|||
- listitem:
|
||||
- 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.skip(!!channel, 'We skip browser download when testing a channel');
|
||||
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();
|
||||
expect(fs.existsSync(executablePath)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const test = playwrightTest.extend<ExtraFixtures>({
|
|||
await use(async (wsEndpoint, options = {}, redirectPortForTest): Promise<Browser> => {
|
||||
(options as any).__testHookRedirectPortForwarding = redirectPortForTest;
|
||||
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,
|
||||
};
|
||||
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.skip(channel === 'chromium-headless-shell', 'shell is never headed');
|
||||
|
||||
const headless = (browserType as any)._defaultLaunchOptions.headless;
|
||||
(browserType as any)._defaultLaunchOptions.headless = false;
|
||||
const headless = (browserType as any)._playwright._defaultLaunchOptions.headless;
|
||||
(browserType as any)._playwright._defaultLaunchOptions.headless = false;
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint());
|
||||
const browserContext = await browser.newContext();
|
||||
|
|
@ -182,7 +182,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
|||
// @ts-ignore
|
||||
await page.pause({ __testHookKeepTestTimeout: true });
|
||||
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 }) => {
|
||||
|
|
@ -599,7 +599,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
|||
const browser = await browserType.connect({
|
||||
wsEndpoint: remoteServer.wsEndpoint(),
|
||||
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();
|
||||
|
|
@ -630,14 +630,14 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
|||
|
||||
test('should filter launch options', async ({ connect, startRemoteServer, server, browserType }, testInfo) => {
|
||||
const tracesDir = testInfo.outputPath('traces');
|
||||
const oldTracesDir = (browserType as any)._defaultLaunchOptions.tracesDir;
|
||||
(browserType as any)._defaultLaunchOptions.tracesDir = tracesDir;
|
||||
const oldTracesDir = (browserType as any)._playwright._defaultTracesDir;
|
||||
(browserType as any)._playwright._defaultTracesDir = tracesDir;
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint());
|
||||
const page = await browser.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await browser.close();
|
||||
(browserType as any)._defaultLaunchOptions.tracesDir = oldTracesDir;
|
||||
(browserType as any)._playwright._defaultTracesDir = oldTracesDir;
|
||||
expect(fs.existsSync(tracesDir)).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const test = baseTest.extend<Fixtures>({
|
|||
await use(async () => {
|
||||
const browser = await browserType.connect(wsEndpoint, {
|
||||
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',
|
||||
},
|
||||
}) as BrowserWithReuse;
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ it('should have passed URL when launching with ignoreDefaultArgs: true', async (
|
|||
it.skip(mode !== 'default');
|
||||
|
||||
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 = {
|
||||
args: browserName === 'firefox' ? [...args, '-new-tab', server.EMPTY_PAGE] : [...args, server.EMPTY_PAGE],
|
||||
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 }) => {
|
||||
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);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
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) => {
|
||||
const oldTracesDir = (browserType as any)._defaultLaunchOptions.tracesDir;
|
||||
(browserType as any)._defaultLaunchOptions.tracesDir = undefined;
|
||||
const oldTracesDir = (browserType as any)._playwright._defaultTracesDir;
|
||||
(browserType as any)._playwright._defaultTracesDir = undefined;
|
||||
const browser = await browserType.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.context().tracing.start();
|
||||
|
|
@ -394,7 +394,7 @@ test('should survive browser.close with auto-created traces dir', async ({ brows
|
|||
]);
|
||||
|
||||
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 }) => {
|
||||
|
|
|
|||
|
|
@ -420,3 +420,71 @@ test('should take screenshot when page is closed in afterEach', async ({ runInli
|
|||
expect(result.failed).toBe(1);
|
||||
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(['config', '--local', 'user.email', 'shakespeare@example.local']);
|
||||
await execGit(['config', '--local', 'user.name', 'William']);
|
||||
await execGit(['add', 'playwright.config.ts']);
|
||||
await execGit(['commit', '-m', 'init']);
|
||||
await execGit(['add', '*.ts']);
|
||||
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_SERVER_URL: 'https://playwright.dev',
|
||||
GITHUB_SHA: 'example-sha',
|
||||
GITHUB_REF_NAME: '42/merge',
|
||||
GITHUB_BASE_REF: 'HEAD~1',
|
||||
});
|
||||
|
||||
await showReport();
|
||||
|
|
@ -1231,7 +1235,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||
- 'link "chore(html): make this test look nice"'
|
||||
- text: /^William <shakespeare@example.local> on/
|
||||
- link "logs"
|
||||
- link "Logs"
|
||||
- link "Pull Request"
|
||||
- link /^[a-f0-9]{7}$/
|
||||
- text: 'foo: value1 bar: {"prop":"value2"} baz: ["value3",123]'
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -137,29 +137,55 @@ class JSLintingService extends LintingService {
|
|||
'vue-router',
|
||||
'experimental-ct',
|
||||
];
|
||||
constructor() {
|
||||
super();
|
||||
this.eslint = new ESLint({
|
||||
overrideConfigFile: path.join(PROJECT_DIR, '.eslintrc.js'),
|
||||
useEslintrc: false,
|
||||
|
||||
async _init() {
|
||||
if (this._eslint)
|
||||
return this._eslint;
|
||||
|
||||
const { fixupConfigRules } = await import('@eslint/compat');
|
||||
const { FlatCompat } = await import('@eslint/eslintrc');
|
||||
// @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: {
|
||||
plugins: ['react'],
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
settings: {
|
||||
react: { version: 'detect', }
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
],
|
||||
rules: {
|
||||
languageOptions: {
|
||||
// @ts-ignore
|
||||
parser: await import('@typescript-eslint/parser'),
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: /** @type {any}*/({
|
||||
...baseRules,
|
||||
'notice/notice': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'max-len': ['error', { code: 100 }],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'eol-last': 'off',
|
||||
},
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
}),
|
||||
}
|
||||
});
|
||||
return this._eslint;
|
||||
}
|
||||
|
||||
supports(codeLang) {
|
||||
|
|
@ -171,9 +197,10 @@ class JSLintingService extends LintingService {
|
|||
* @returns {Promise<LintResult>}
|
||||
*/
|
||||
async _lintSnippet(snippet) {
|
||||
const eslint = await this._init();
|
||||
if (this._knownBadSnippets.some(s => snippet.code.includes(s)))
|
||||
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)
|
||||
return { status: 'ok' };
|
||||
const result = results[0];
|
||||
|
|
|
|||
|
|
@ -94,6 +94,18 @@ Example:
|
|||
console.log('\nUpdating browser version in browsers.json...');
|
||||
for (const descriptor of descriptors)
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue