UNPKG

testable-playwright-test

Version:

Playwright fixture to run your tests against Testable Cloud

238 lines (218 loc) 8.68 kB
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;