2021-06-03 08:07:55 -07:00
|
|
|
/**
|
|
|
|
* 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 * as fs from 'fs';
|
2021-06-08 11:22:07 -07:00
|
|
|
import * as path from 'path';
|
2021-06-15 10:06:49 -07:00
|
|
|
import * as os from 'os';
|
2021-06-23 11:08:35 +02:00
|
|
|
import type { LaunchOptions, BrowserContextOptions, Page } from '../../types/types';
|
2021-06-17 15:09:38 -07:00
|
|
|
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, FullConfig, TestInfo } from '../../types/test';
|
2021-06-06 20:18:47 -07:00
|
|
|
import { rootTestType } from './testType';
|
2021-06-15 10:06:49 -07:00
|
|
|
import { createGuid, removeFolders } from '../utils/utils';
|
2021-06-06 20:18:47 -07:00
|
|
|
export { expect } from './expect';
|
|
|
|
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
2021-06-15 10:06:49 -07:00
|
|
|
|
|
|
|
const artifactsFolder = path.join(os.tmpdir(), 'pwt-' + createGuid());
|
|
|
|
|
2021-06-06 20:18:47 -07:00
|
|
|
export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>({
|
2021-06-04 20:54:58 -07:00
|
|
|
defaultBrowserType: [ 'chromium', { scope: 'worker' } ],
|
|
|
|
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ],
|
2021-06-03 08:07:55 -07:00
|
|
|
playwright: [ require('../inprocess'), { scope: 'worker' } ],
|
|
|
|
headless: [ undefined, { scope: 'worker' } ],
|
|
|
|
channel: [ undefined, { scope: 'worker' } ],
|
|
|
|
launchOptions: [ {}, { scope: 'worker' } ],
|
|
|
|
|
2021-06-11 16:19:50 -07:00
|
|
|
browser: [ async ({ playwright, browserName, headless, channel, launchOptions }, use) => {
|
2021-06-03 08:07:55 -07:00
|
|
|
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
|
|
|
|
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
|
|
|
|
const options: LaunchOptions = {
|
|
|
|
handleSIGINT: false,
|
2021-06-24 23:28:01 -07:00
|
|
|
timeout: 0,
|
2021-06-03 08:07:55 -07:00
|
|
|
...launchOptions,
|
|
|
|
};
|
|
|
|
if (headless !== undefined)
|
|
|
|
options.headless = headless;
|
|
|
|
if (channel !== undefined)
|
|
|
|
options.channel = channel;
|
|
|
|
const browser = await playwright[browserName].launch(options);
|
|
|
|
await use(browser);
|
|
|
|
await browser.close();
|
2021-06-15 10:06:49 -07:00
|
|
|
await removeFolders([artifactsFolder]);
|
2021-06-03 08:07:55 -07:00
|
|
|
}, { scope: 'worker' } ],
|
|
|
|
|
|
|
|
screenshot: 'off',
|
|
|
|
video: 'off',
|
2021-06-08 11:22:07 -07:00
|
|
|
trace: 'off',
|
2021-06-03 08:07:55 -07:00
|
|
|
acceptDownloads: undefined,
|
|
|
|
bypassCSP: undefined,
|
|
|
|
colorScheme: undefined,
|
|
|
|
deviceScaleFactor: undefined,
|
|
|
|
extraHTTPHeaders: undefined,
|
|
|
|
geolocation: undefined,
|
|
|
|
hasTouch: undefined,
|
|
|
|
httpCredentials: undefined,
|
|
|
|
ignoreHTTPSErrors: undefined,
|
|
|
|
isMobile: undefined,
|
|
|
|
javaScriptEnabled: undefined,
|
|
|
|
locale: undefined,
|
|
|
|
offline: undefined,
|
|
|
|
permissions: undefined,
|
|
|
|
proxy: undefined,
|
|
|
|
storageState: undefined,
|
|
|
|
timezoneId: undefined,
|
|
|
|
userAgent: undefined,
|
|
|
|
viewport: undefined,
|
|
|
|
contextOptions: {},
|
|
|
|
|
2021-06-11 16:19:50 -07:00
|
|
|
context: async ({ browser, screenshot, trace, video, acceptDownloads, bypassCSP, colorScheme, deviceScaleFactor, extraHTTPHeaders, hasTouch, geolocation, httpCredentials, ignoreHTTPSErrors, isMobile, javaScriptEnabled, locale, offline, permissions, proxy, storageState, viewport, timezoneId, userAgent, contextOptions }, use, testInfo) => {
|
2021-06-04 20:54:58 -07:00
|
|
|
testInfo.snapshotSuffix = process.platform;
|
2021-06-03 08:07:55 -07:00
|
|
|
if (process.env.PWDEBUG)
|
|
|
|
testInfo.setTimeout(0);
|
|
|
|
|
2021-06-16 16:05:30 -07:00
|
|
|
let videoMode = typeof video === 'string' ? video : video.mode;
|
|
|
|
if (videoMode === 'retry-with-video')
|
|
|
|
videoMode = 'on-first-retry';
|
|
|
|
if (trace === 'retry-with-trace')
|
|
|
|
trace = 'on-first-retry';
|
|
|
|
|
|
|
|
const captureVideo = (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
|
|
|
const captureTrace = (trace === 'on' || trace === 'retain-on-failure' || (trace === 'on-first-retry' && testInfo.retry === 1));
|
|
|
|
|
2021-06-15 10:06:49 -07:00
|
|
|
let recordVideoDir: string | null = null;
|
2021-06-16 07:51:54 -07:00
|
|
|
const recordVideoSize = typeof video === 'string' ? undefined : video.size;
|
2021-06-16 16:05:30 -07:00
|
|
|
if (captureVideo) {
|
2021-06-15 10:06:49 -07:00
|
|
|
await fs.promises.mkdir(artifactsFolder, { recursive: true });
|
|
|
|
recordVideoDir = artifactsFolder;
|
|
|
|
}
|
|
|
|
|
2021-06-03 08:07:55 -07:00
|
|
|
const options: BrowserContextOptions = {
|
2021-06-16 07:51:54 -07:00
|
|
|
recordVideo: recordVideoDir ? { dir: recordVideoDir, size: recordVideoSize } : undefined,
|
2021-06-03 08:07:55 -07:00
|
|
|
...contextOptions,
|
|
|
|
};
|
|
|
|
if (acceptDownloads !== undefined)
|
|
|
|
options.acceptDownloads = acceptDownloads;
|
|
|
|
if (bypassCSP !== undefined)
|
|
|
|
options.bypassCSP = bypassCSP;
|
|
|
|
if (colorScheme !== undefined)
|
|
|
|
options.colorScheme = colorScheme;
|
|
|
|
if (deviceScaleFactor !== undefined)
|
|
|
|
options.deviceScaleFactor = deviceScaleFactor;
|
|
|
|
if (extraHTTPHeaders !== undefined)
|
|
|
|
options.extraHTTPHeaders = extraHTTPHeaders;
|
|
|
|
if (geolocation !== undefined)
|
|
|
|
options.geolocation = geolocation;
|
|
|
|
if (hasTouch !== undefined)
|
|
|
|
options.hasTouch = hasTouch;
|
|
|
|
if (httpCredentials !== undefined)
|
|
|
|
options.httpCredentials = httpCredentials;
|
|
|
|
if (ignoreHTTPSErrors !== undefined)
|
|
|
|
options.ignoreHTTPSErrors = ignoreHTTPSErrors;
|
|
|
|
if (isMobile !== undefined)
|
|
|
|
options.isMobile = isMobile;
|
|
|
|
if (javaScriptEnabled !== undefined)
|
|
|
|
options.javaScriptEnabled = javaScriptEnabled;
|
|
|
|
if (locale !== undefined)
|
|
|
|
options.locale = locale;
|
|
|
|
if (offline !== undefined)
|
|
|
|
options.offline = offline;
|
|
|
|
if (permissions !== undefined)
|
|
|
|
options.permissions = permissions;
|
|
|
|
if (proxy !== undefined)
|
|
|
|
options.proxy = proxy;
|
|
|
|
if (storageState !== undefined)
|
|
|
|
options.storageState = storageState;
|
|
|
|
if (timezoneId !== undefined)
|
|
|
|
options.timezoneId = timezoneId;
|
|
|
|
if (userAgent !== undefined)
|
|
|
|
options.userAgent = userAgent;
|
|
|
|
if (viewport !== undefined)
|
|
|
|
options.viewport = viewport;
|
|
|
|
|
|
|
|
const context = await browser.newContext(options);
|
2021-06-24 23:28:01 -07:00
|
|
|
context.setDefaultTimeout(0);
|
2021-06-03 08:07:55 -07:00
|
|
|
const allPages: Page[] = [];
|
|
|
|
context.on('page', page => allPages.push(page));
|
|
|
|
|
2021-06-16 16:05:30 -07:00
|
|
|
if (captureTrace) {
|
2021-06-08 11:22:07 -07:00
|
|
|
const name = path.relative(testInfo.project.outputDir, testInfo.outputDir).replace(/[\/\\]/g, '-');
|
|
|
|
await context.tracing.start({ name, screenshots: true, snapshots: true });
|
|
|
|
}
|
|
|
|
|
2021-06-03 08:07:55 -07:00
|
|
|
await use(context);
|
|
|
|
|
|
|
|
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
2021-06-08 11:22:07 -07:00
|
|
|
|
2021-06-16 16:05:30 -07:00
|
|
|
const preserveTrace = captureTrace && (trace === 'on' || (testFailed && trace === 'retain-on-failure') || (trace === 'on-first-retry' && testInfo.retry === 1));
|
|
|
|
if (preserveTrace) {
|
2021-06-08 11:22:07 -07:00
|
|
|
const tracePath = testInfo.outputPath(`trace.zip`);
|
|
|
|
await context.tracing.stop({ path: tracePath });
|
2021-06-16 16:05:30 -07:00
|
|
|
} else if (captureTrace) {
|
2021-06-08 11:22:07 -07:00
|
|
|
await context.tracing.stop();
|
|
|
|
}
|
|
|
|
|
2021-06-16 16:05:30 -07:00
|
|
|
const captureScreenshots = (screenshot === 'on' || (screenshot === 'only-on-failure' && testFailed));
|
|
|
|
if (captureScreenshots) {
|
2021-06-03 08:07:55 -07:00
|
|
|
await Promise.all(allPages.map((page, index) => {
|
|
|
|
const screenshotPath = testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${++index}.png`);
|
|
|
|
return page.screenshot({ timeout: 5000, path: screenshotPath }).catch(e => {});
|
|
|
|
}));
|
|
|
|
}
|
2021-06-17 15:09:38 -07:00
|
|
|
|
|
|
|
const prependToError = testInfo.status === 'timedOut' ? formatPendingCalls((context as any)._connection.pendingProtocolCalls(), testInfo) : '';
|
2021-06-03 08:07:55 -07:00
|
|
|
await context.close();
|
2021-06-17 15:09:38 -07:00
|
|
|
if (prependToError) {
|
|
|
|
if (!testInfo.error) {
|
|
|
|
testInfo.error = { value: prependToError };
|
|
|
|
} else if (testInfo.error.message) {
|
|
|
|
testInfo.error.message = prependToError + testInfo.error.message;
|
|
|
|
if (testInfo.error.stack)
|
|
|
|
testInfo.error.stack = prependToError + testInfo.error.stack;
|
|
|
|
}
|
|
|
|
}
|
2021-06-03 08:07:55 -07:00
|
|
|
|
2021-06-16 16:05:30 -07:00
|
|
|
const preserveVideo = captureVideo && (videoMode === 'on' || (testFailed && videoMode === 'retain-on-failure') || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
|
|
|
if (preserveVideo) {
|
2021-06-03 08:07:55 -07:00
|
|
|
await Promise.all(allPages.map(async page => {
|
2021-06-16 07:51:54 -07:00
|
|
|
const v = page.video();
|
|
|
|
if (!v)
|
2021-06-03 08:07:55 -07:00
|
|
|
return;
|
2021-06-10 22:23:02 -07:00
|
|
|
try {
|
2021-06-16 07:51:54 -07:00
|
|
|
const videoPath = await v.path();
|
2021-06-15 10:06:49 -07:00
|
|
|
const fileName = path.basename(videoPath);
|
2021-06-16 07:51:54 -07:00
|
|
|
await v.saveAs(testInfo.outputPath(fileName));
|
2021-06-10 22:23:02 -07:00
|
|
|
} catch (e) {
|
2021-06-15 10:06:49 -07:00
|
|
|
// Silent catch empty videos.
|
2021-06-10 22:23:02 -07:00
|
|
|
}
|
2021-06-03 08:07:55 -07:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
page: async ({ context }, use) => {
|
|
|
|
await use(await context.newPage());
|
|
|
|
},
|
|
|
|
});
|
|
|
|
export default test;
|
2021-06-17 15:09:38 -07:00
|
|
|
|
|
|
|
function formatPendingCalls(calls: ProtocolCall[], testInfo: TestInfo) {
|
|
|
|
if (!calls.length)
|
|
|
|
return '';
|
|
|
|
return 'Pending operations:\n' + calls.map(call => {
|
|
|
|
const frame = call.stack && call.stack[0] ? formatStackFrame(testInfo.config, call.stack[0]) : '<unknown>';
|
|
|
|
return ` - ${call.apiName} at ${frame}\n`;
|
|
|
|
}).join('') + '\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
function formatStackFrame(config: FullConfig, frame: StackFrame) {
|
|
|
|
const file = path.relative(config.rootDir, frame.file) || path.basename(frame.file);
|
|
|
|
return `${file}:${frame.line || 1}:${frame.column || 1}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
type StackFrame = {
|
|
|
|
file: string,
|
|
|
|
line?: number,
|
|
|
|
column?: number,
|
|
|
|
function?: string,
|
|
|
|
};
|
|
|
|
|
|
|
|
type ProtocolCall = {
|
|
|
|
stack?: StackFrame[],
|
|
|
|
apiName?: string,
|
|
|
|
};
|