testable-playwright-test
Version:
Playwright fixture to run your tests against Testable Cloud
238 lines (218 loc) • 8.68 kB
JavaScript
const base = require('@playwright/test');
const playwrightVersion = require('@playwright/test/package.json').version;
const { URLSearchParams } = require('url');
const { join } = require('path');
const Screenshots = process.env.TESTABLE_SCREENSHOTS === 'true';
const ScreenshotTrigger = process.env.TESTABLE_SCREENSHOT_TRIGGER;
function toSuiteName(testInfo) {
let answer = `${testInfo.project.name || 'chromium'}`;
for (let i = 0; i < testInfo.titlePath.length - 1; i++)
if (testInfo.titlePath[i])
answer += ` -> ${testInfo.titlePath[i]}`;
if (testInfo.titlePath.length <= 1)
answer += ' -> Playwright Tests';
return answer;
}
function toFilePath(file) {
const cwd = process.cwd();
return file.indexOf(cwd) === 0 ? file.substring(cwd.length + 1) : file;
}
function printLocation(location) {
return location ? ` - ${toFilePath(location.file)}${location.line ? `:${location.line}` : ''}${location.column ? `:${location.column}` : ''}` : '';
}
function getStepTitle(step) {
return `${step.title}${printLocation(step.location)}`;
}
function objectContains(baseObject, comparisonObject) {
for (var property in comparisonObject) {
if (comparisonObject.hasOwnProperty(property)) {
if (typeof comparisonObject[property] === "object" && comparisonObject[property] !== null) {
if (!baseObject.hasOwnProperty(property) || typeof baseObject[property] !== 'object' || baseObject[property] === null) {
return false;
}
if (!objectContains(baseObject[property], comparisonObject[property])) {
return false;
}
} else if (!baseObject.hasOwnProperty(property) || baseObject[property] !== comparisonObject[property]) {
return false;
}
}
}
return true;
}
function findDeviceName(use) {
for (const [key, value] of Object.entries(base.devices)) {
const device = base.devices[key];
if (objectContains(use, device))
return key;
}
}
function toBrowserInfo(channel, browserType) {
if (channel) {
switch (channel) {
case 'chrome':
return { browserType: 'chrome', browserVersion: 'latest' };
case 'chrome-beta':
return { browserType: 'chrome', browserVersion: 'beta' };
case 'msedge':
return { browserType: 'edge', browserVersion: 'latest' };
case 'msedge-beta':
return { browserType: 'edge', browserVersion: 'beta' };
}
} else {
return { browserType: browserType || 'chromium', browserVersion: 'latest' };
}
}
function addSteps(startTime, stepInfos) {
if (stepInfos && stepInfos.length) {
let currentStartTime = startTime;
const now = Date.now();
return stepInfos.map((stepInfo) => {
const step = {
name: getStepTitle(stepInfo),
started: currentStartTime,
finished: stepInfo.endWallTime || now,
duration: (stepInfo.endWallTime || now) - currentStartTime,
state: stepInfo.error ? 'failed' : 'passed',
steps: addSteps(currentStartTime, stepInfo.steps)
};
if (stepInfo.error) {
step.errorType = 'Error';
step.error = stepInfo.error.message;
step.errorTrace = stepInfo.error.stack;
}
currentStartTime = stepInfo.endWallTime;
return step;
});
}
return [];
}
function getConnectOptions(testId, config, project, options) {
options = options || {};
if (!process.env.IS_TESTABLE && !options.key)
return null; // if we're running outside of Testable and not pointing at Testable, then do not use connect
options.playwright = true;
if (testId)
options.testId = testId;
const use = project?.use;
if (!options.device && use) {
options.device = JSON.stringify({
displaySize: 'customDevice',
userAgent: use.userAgent,
width: use.screen?.width || use.viewport?.width || 1920,
height: use.screen?.height || use.viewport?.height || 1080,
name: findDeviceName(use) || 'Custom Device',
scaleFactor: use.deviceScaleFactor || 1,
isMobile: use.isMobile || false,
isTouch: use.hasTouch || false,
isLandscape: false
});
}
if (use && typeof use.headless === 'boolean')
options.headless = `${use.headless}`;
if (options.key) {
options.playwrightVersion = config?.version || playwrightVersion;
const browserInfo = toBrowserInfo(use?.channel, use?.defaultBrowserType);
options.browserName = browserInfo.browserType;
options.browserVersion = browserInfo.browserVersion;
} else {
if (config?.version)
options.launchVersion = config.version;
options.userOptions = use?.launchOptions || {};
if (use?.channel)
options.userOptions.channel = use.channel;
options.browserType = use?.defaultBrowserType || 'chromium';
}
const serverUrl = getServerUrl(options);
options.userOptions = Buffer.from(JSON.stringify(options.userOptions || {})).toString('base64');
const params = new URLSearchParams(options).toString();
return {
wsEndpoint: `${serverUrl}/?${params}`,
timeout: 0
};
}
function createFixture(fixtureOptions) {
return base.test.extend({
testId: [ async ({}, use) => {
const testId = process.env.TESTABLE_TEST_ID;
await use(testId);
}, { scope: 'worker', auto: true } ],
connectOptions: [ async ({ testId }, use, workerInfo) => {
const connectOptions = getConnectOptions(testId, workerInfo?.config,
workerInfo?.project, fixtureOptions);
await use(connectOptions);
}, { scope: 'worker', option: true } ],
testReporter: [ async ({ page }, use, testInfo) => {
const now = Date.now();
const options = fixtureOptions || {};
const isRemote = !process.env.IS_TESTABLE && options.key;
if (isRemote) {
await page.evaluate(function testable_assertion_start() { },
{ suite: toSuiteName(testInfo), name: testInfo.title });
}
await use('testReporter');
const failed = testInfo.status !== testInfo.expectedStatus;
if (process.env.IS_TESTABLE) {
const screenshot = Screenshots && (ScreenshotTrigger === 'afterEvery' || failed && ScreenshotTrigger === 'afterFailed');
// attach screenshot so reporter can send it to Testable
if (screenshot) {
const screenshotContents = await page.screenshot();
await testInfo.attach(`${toSuiteName(testInfo)}.${testInfo.title}.png`,
{ body: screenshotContents, contentType: 'image/png' });
}
} else if (isRemote) {
const screenshot = options.screenshot === 'afterEvery' || failed && options.screenshot === 'afterFailed';
const data = {
suite: toSuiteName(testInfo),
name: testInfo.title,
state: failed ? 'failed' : 'passed',
duration: testInfo.duration,
screenshot
};
if (testInfo.error) {
data.errorType = 'Error';
data.error = testInfo.error.message;
data.errorTrace = testInfo.error.stack;
}
data.steps = addSteps(testInfo._startWallTime || now, testInfo._steps);
await page.evaluate(function testable_assertion_finish() { }, data);
}
}, { scope: 'test', auto: true } ]
});
}
function getTestableSetupFile() {
return join(__dirname, 'global-setup.js');
}
function getTestableTeardownFile() {
return join(__dirname, 'global-teardown.js');
}
function getServerUrl(options) {
return options.serverUrl || process.env.TESTABLE_WS_PROXY || 'wss://playwright.testable.io';
}
function defineConfig(config, options) {
options = options || {};
const isTestableRemote = !process.env.IS_TESTABLE && options.key;
if (isTestableRemote) {
process.env.TESTABLE_SERVER_URL = getServerUrl(options);
process.env.TESTABLE_KEY = options.key;
const testableGlobalTeardown = getTestableTeardownFile();
if (!config.globalTeardown)
config.globalTeardown = [ testableGlobalTeardown ];
else if (typeof config.globalTeardown === 'string' && config.globalTeardown.length > 0)
config.globalTeardown = [ config.globalTeardown, testableGlobalTeardown ];
else
config.globalTeardown.push(testableGlobalTeardown);
}
const testableGlobalSetup = getTestableSetupFile();
if (!config.globalSetup)
config.globalSetup = [ testableGlobalSetup ];
else if (typeof config.globalSetup === 'string' && config.globalSetup.length > 0)
config.globalSetup = [ config.globalSetup, testableGlobalSetup ];
else
config.globalSetup.push(testableGlobalSetup);
const test = createFixture(options);
base.test = test;
return base.defineConfig(config);
}
exports.createFixture = createFixture;
exports.defineConfig = defineConfig;