|  | 
|  | 1 | +const base = require("@playwright/test"); | 
|  | 2 | +const cp = require("child_process"); | 
|  | 3 | +const { _android } = require("playwright"); | 
|  | 4 | +const clientPlaywrightVersion = cp | 
|  | 5 | +  .execSync("npx playwright --version") | 
|  | 6 | +  .toString() | 
|  | 7 | +  .trim() | 
|  | 8 | +  .split(" ")[1]; | 
|  | 9 | +const BrowserStackLocal = require("browserstack-local"); | 
|  | 10 | +const util = require("util"); | 
|  | 11 | + | 
|  | 12 | +// BrowserStack Specific Capabilities. | 
|  | 13 | +// Set 'browserstack.local:true For Local testing | 
|  | 14 | +const caps = { | 
|  | 15 | +  osVersion: "13.0", | 
|  | 16 | +  deviceName: "Samsung Galaxy S23", // "Samsung Galaxy S22 Ultra", "Google Pixel 7 Pro", "OnePlus 9", etc. | 
|  | 17 | +  browserName: "chrome", | 
|  | 18 | +  realMobile: "true", | 
|  | 19 | +  name: "My android playwright test", | 
|  | 20 | +  build: "playwright-build-1", | 
|  | 21 | +  "browserstack.username": process.env.BROWSERSTACK_USERNAME || "<USERNAME>", | 
|  | 22 | +  "browserstack.accessKey": | 
|  | 23 | +    process.env.BROWSERSTACK_ACCESS_KEY || "<ACCESS_KEY>", | 
|  | 24 | +  "browserstack.local": process.env.BROWSERSTACK_LOCAL || false, | 
|  | 25 | +}; | 
|  | 26 | + | 
|  | 27 | +exports.bsLocal = new BrowserStackLocal.Local(); | 
|  | 28 | + | 
|  | 29 | +// replace YOUR_ACCESS_KEY with your key. You can also set an environment variable - "BROWSERSTACK_ACCESS_KEY". | 
|  | 30 | +exports.BS_LOCAL_ARGS = { | 
|  | 31 | +  key: process.env.BROWSERSTACK_ACCESS_KEY || "ACCESSKEY", | 
|  | 32 | +}; | 
|  | 33 | + | 
|  | 34 | +// Patching the capabilities dynamically according to the project name. | 
|  | 35 | +const patchMobileCaps = (name, title) => { | 
|  | 36 | +  let combination = name.split(/@browserstack/)[0]; | 
|  | 37 | +  let [browerCaps, osCaps] = combination.split(/:/); | 
|  | 38 | +  let [browser, deviceName] = browerCaps.split(/@/); | 
|  | 39 | +  let osCapsSplit = osCaps.split(/ /); | 
|  | 40 | +  let os = osCapsSplit.shift(); | 
|  | 41 | +  let osVersion = osCapsSplit.join(" "); | 
|  | 42 | +  caps.browser = browser ? browser : "chrome"; | 
|  | 43 | +  caps.deviceName = deviceName ? deviceName : "Samsung Galaxy S22 Ultra"; | 
|  | 44 | +  caps.osVersion = osVersion ? osVersion : "12.0"; | 
|  | 45 | +  caps.name = title; | 
|  | 46 | +  caps.realMobile = "true"; | 
|  | 47 | +}; | 
|  | 48 | + | 
|  | 49 | +const patchCaps = (name, title) => { | 
|  | 50 | +  let combination = name.split(/@browserstack/)[0]; | 
|  | 51 | +  let [browerCaps, osCaps] = combination.split(/:/); | 
|  | 52 | +  let [browser, browser_version] = browerCaps.split(/@/); | 
|  | 53 | +  let osCapsSplit = osCaps.split(/ /); | 
|  | 54 | +  let os = osCapsSplit.shift(); | 
|  | 55 | +  let os_version = osCapsSplit.join(" "); | 
|  | 56 | +  caps.browser = browser ? browser : "chrome"; | 
|  | 57 | +  caps.browser_version = browser_version ? browser_version : "latest"; | 
|  | 58 | +  caps.os = os ? os : "osx"; | 
|  | 59 | +  caps.os_version = os_version ? os_version : "catalina"; | 
|  | 60 | +  caps.name = title; | 
|  | 61 | +}; | 
|  | 62 | + | 
|  | 63 | +const isHash = (entity) => | 
|  | 64 | +  Boolean(entity && typeof entity === "object" && !Array.isArray(entity)); | 
|  | 65 | +const nestedKeyValue = (hash, keys) => | 
|  | 66 | +  keys.reduce((hash, key) => (isHash(hash) ? hash[key] : undefined), hash); | 
|  | 67 | +const isUndefined = (val) => val === undefined || val === null || val === ""; | 
|  | 68 | +const evaluateSessionStatus = (status) => { | 
|  | 69 | +  if (!isUndefined(status)) { | 
|  | 70 | +    status = status.toLowerCase(); | 
|  | 71 | +  } | 
|  | 72 | +  if (status === "passed") { | 
|  | 73 | +    return "passed"; | 
|  | 74 | +  } else if (status === "failed" || status === "timedout") { | 
|  | 75 | +    return "failed"; | 
|  | 76 | +  } else { | 
|  | 77 | +    return ""; | 
|  | 78 | +  } | 
|  | 79 | +}; | 
|  | 80 | + | 
|  | 81 | +exports.test = base.test.extend({ | 
|  | 82 | +  page: async ({ page, playwright }, use, testInfo) => { | 
|  | 83 | +    if (testInfo.project.name.match(/browserstack/)) { | 
|  | 84 | +      let vBrowser, vContext, vDevice; | 
|  | 85 | +      const isMobile = testInfo.project.name.match(/browserstack-mobile/); | 
|  | 86 | +      if (isMobile) { | 
|  | 87 | +        patchMobileCaps( | 
|  | 88 | +          testInfo.project.name, | 
|  | 89 | +          `${testInfo.file} - ${testInfo.title}` | 
|  | 90 | +        ); | 
|  | 91 | +        vDevice = await playwright._android.connect( | 
|  | 92 | +          `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent( | 
|  | 93 | +            JSON.stringify(caps) | 
|  | 94 | +          )}` | 
|  | 95 | +        ); | 
|  | 96 | +        await vDevice.shell("am force-stop com.android.chrome"); | 
|  | 97 | +        vContext = await vDevice.launchBrowser(); | 
|  | 98 | +      } else { | 
|  | 99 | +        patchCaps(testInfo.project.name, `${testInfo.title}`); | 
|  | 100 | +        delete caps.osVersion; | 
|  | 101 | +        delete caps.deviceName; | 
|  | 102 | +        delete caps.realMobile; | 
|  | 103 | +        vBrowser = await playwright.chromium.connect({ | 
|  | 104 | +          wsEndpoint: | 
|  | 105 | +            `wss://cdp.browserstack.com/playwright?caps=` + | 
|  | 106 | +            `${encodeURIComponent(JSON.stringify(caps))}`, | 
|  | 107 | +        }); | 
|  | 108 | +        vContext = await vBrowser.newContext(testInfo.project.use); | 
|  | 109 | +      } | 
|  | 110 | +      const vPage = await vContext.newPage(); | 
|  | 111 | +      await use(vPage); | 
|  | 112 | + | 
|  | 113 | +      await vPage.close(); | 
|  | 114 | + | 
|  | 115 | +      if (isMobile) { | 
|  | 116 | +        await vDevice.close(); | 
|  | 117 | +      } else { | 
|  | 118 | +        await vBrowser.close(); | 
|  | 119 | +      } | 
|  | 120 | +    } else { | 
|  | 121 | +      use(page); | 
|  | 122 | +    } | 
|  | 123 | +  }, | 
|  | 124 | + | 
|  | 125 | +  beforeEach: [ | 
|  | 126 | +    async ({ page }, use) => { | 
|  | 127 | +      await page | 
|  | 128 | +        .context() | 
|  | 129 | +        .tracing.start({ screenshots: true, snapshots: true, sources: true }); | 
|  | 130 | +      await use(); | 
|  | 131 | +    }, | 
|  | 132 | +    { auto: true }, | 
|  | 133 | +  ], | 
|  | 134 | + | 
|  | 135 | +  afterEach: [ | 
|  | 136 | +    async ({ page }, use, testInfo) => { | 
|  | 137 | +      await use(); | 
|  | 138 | +      if (testInfo.status == "failed") { | 
|  | 139 | +        await page | 
|  | 140 | +          .context() | 
|  | 141 | +          .tracing.stop({ path: `${testInfo.outputDir}/trace.zip` }); | 
|  | 142 | +        await page.screenshot({ path: `${testInfo.outputDir}/screenshot.png` }); | 
|  | 143 | +        await testInfo.attach("screenshot", { | 
|  | 144 | +          path: `${testInfo.outputDir}/screenshot.png`, | 
|  | 145 | +          contentType: "image/png", | 
|  | 146 | +        }); | 
|  | 147 | +        await testInfo.attach("trace", { | 
|  | 148 | +          path: `${testInfo.outputDir}/trace.zip`, | 
|  | 149 | +          contentType: "application/zip", | 
|  | 150 | +        }); | 
|  | 151 | +      } | 
|  | 152 | +    }, | 
|  | 153 | +    { auto: true }, | 
|  | 154 | +  ], | 
|  | 155 | +}); | 
0 commit comments