{"id":19448317,"url":"https://github.com/csbun/zhagana","last_synced_at":"2026-04-11T21:03:53.724Z","repository":{"id":150165570,"uuid":"306313899","full_name":"csbun/zhagana","owner":"csbun","description":"Step by step guide to use Playwright","archived":false,"fork":false,"pushed_at":"2020-11-28T06:43:39.000Z","size":19521,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-07T23:21:35.732Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/csbun.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-22T11:23:15.000Z","updated_at":"2024-03-10T15:45:47.000Z","dependencies_parsed_at":"2023-04-25T01:32:23.946Z","dependency_job_id":null,"html_url":"https://github.com/csbun/zhagana","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/csbun%2Fzhagana","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/csbun%2Fzhagana/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/csbun%2Fzhagana/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/csbun%2Fzhagana/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/csbun","download_url":"https://codeload.github.com/csbun/zhagana/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240636664,"owners_count":19832922,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-10T16:26:01.648Z","updated_at":"2026-04-11T21:03:53.689Z","avatar_url":"https://github.com/csbun.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ZhaGaNa\n\nStep by step guide to use [Playwright][pw].\n\n## Project Setup\n\nCreate a as simple as we can project manually:\n\n```sh\nmkdir zhagana\ncd zhagana\nnpm init\n```\n\nInstall [Playwright][pw] and [TypeScript][ts] via npm, which may take some minutes to download browser binaries:\n\n```sh\nnpm i playwright\nnpm i -D typescript\n```\n\n### TypeScript Configuration\n\nPlaywright for JavaScript and TypeScript is generally available. But we still need some configuration for TypeScript. Create a `tsconfig.json` file with the following content:\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"module\": \"commonjs\",\n    \"outDir\": \"build\",\n    \"sourceMap\": true\n  }\n}\n```\n\n### VS Code Launcher and Debuger\n\nClick the **RUN** button on the left menu then **create a launch.json**. Select **Node.js** from the drop down if you have other debugger extensions.\n\n![vscode-debuger](resources/vscode-debuger.png)\n\nMake sure you have `\"preLaunchTask\": \"tsc: build - tsconfig.json\"` and `\"outFiles\": [\"${workspaceFolder}/build/**/*.js\"]` in _launch.json_.\n\n```json\n{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"Launch Program\",\n      \"skipFiles\": [\n        \"\u003cnode_internals\u003e/**\"\n      ],\n      \"program\": \"${workspaceFolder}/index.ts\",\n      \"preLaunchTask\": \"tsc: build - tsconfig.json\",\n      \"outFiles\": [\"${workspaceFolder}/build/**/*.js\"]\n    }\n  ]\n}\n```\n\n## Coding\n\n### Screenshot\n\nWe will start by taking a screenshot of the page. This is code [from their documentation](https://playwright.dev/#version=v1.5.1\u0026path=docs%2Fintro.md\u0026q=first-script), but transfer into TypeScript\n\n```ts\nimport { webkit } from 'playwright'\n\n(async () =\u003e {\n  const browser = await webkit.launch();\n  const page = await browser.newPage();\n  await page.goto('http://whatsmyuseragent.org/');\n  await page.screenshot({ path: `out/whatsmyuseragent.png` });\n  await browser.close();\n})();\n```\n\nPress `F5` to run our project, and we will get the _out/whatsmyuseragent.png_ file like this\n\n![whatsmyuseragent](resources/whatsmyuseragent.jpg)\n\nNow, let's make it happen in 3 browsers:\n\n```ts\nimport { Browser, BrowserType, chromium, firefox, webkit } from 'playwright'\n\nasync function screenshot(browserType: BrowserType\u003cBrowser\u003e) {\n  // use `browserType` from arguments instead of hardcode\n  const browser = await browserType.launch();\n  const page = await browser.newPage();\n  await page.goto('http://whatsmyuseragent.org/');\n  await page.screenshot({ path: `out/ua-${browserType.name()}.png` });\n  await browser.close();\n}\n\n(async () =\u003e {\n  // 3 different kind of browsers\n  const BROWSER_TYPES = [\n    chromium,\n    firefox,\n    webkit\n  ]\n  // make screenshot all together\n  await Promise.all(BROWSER_TYPES.map((browserType) =\u003e {\n    return screenshot(browserType);\n  }));\n})();\n```\n\nHere we use the `screenshot` function to take the place of main function and use `Promise.all` to handle 3 browsers in parallel. After a few seconds, we will get 3 screenshots:\n\n- _out/ua-chromium.png_ with `HeadlessChrome`\n- _out/ua-firefox.png_ with `Firefox`\n- _out/ua-webkit.png_ with `AppleWebKit ... Safari`\n\n### Emulation - Mobile Device\n\nNext step, we will simulate browser behavior on a mobile device and navigate to [Google Maps](https://www.google.com/maps).\n\n```ts\nimport { Browser, BrowserType, devices, chromium, firefox, webkit } from 'playwright'\n\nasync function screenshot(browserType: BrowserType\u003cBrowser\u003e) {\n  // use `browserType` from arguments instead of hardcode\n  const browser = await browserType.launch();\n  // simulate browser behavior on a mobile device\n  const iphone = devices['iPhone X'];\n  const context = await browser.newContext({ ...iphone });\n  // open web page\n  const page = await context.newPage();\n  await page.goto('https://www.google.com/maps');  \n  // take screenshot\n  await page.screenshot({ path: `out/map-${browserType.name()}.png` });\n  await browser.close();\n}\n```\n\nSince firefox does not support mobile, we reduce our browsers to chromium and webkit only:\n\n```ts\n(async () =\u003e {\n  // firefox does not support mobile\n  const BROWSER_TYPES = [ chromium, webkit ]\n  // make screenshot all together\n  await Promise.all(BROWSER_TYPES.map((browserType) =\u003e {\n\n    return screenshot(browserType);\n  }));\n})();\n```\n\n`F5` again we will get 2 _png_ file in _out_ directory:\n\n| chromium | webkit |\n|:---:|:---:|\n| ![chromium](resources/map-chromium-1.png) | ![webkit](resources/map-webkit-1.png) |\n\nMaps came out, but seems not complete loaded. So we need `.waitForNavigation()` after `page.goto()`:\n\n```ts\nawait page.goto('https://www.google.com/maps');\nawait page.waitForNavigation();\nawait page.screenshot({ path: `out/map-${browserType.name()}.png` });\n```\n\nBut, wait... there is a blocker comes up: Google Maps want us to download App but we just want to **STAY ON WEB**.\n\n\u003cimg alt=\"webkit\" src=\"resources/map-webkit-pop.png\" width=\"300px\" /\u003e\n\n### Input - Mouse Click\n\nFrom devtools we can get the selector of this promo: `.ml-promotion-nonlu-blocking-promo`, use `page.waitForSelector()` instead of `page.waitForNavigation()` to catch the promotion:\n\n![Devtools - Pop](resources/devtools-map-pop.png)\n\n```ts\nawait page.goto('https://www.google.com/maps');\nawait page.waitForSelector('.ml-promotion-nonlu-blocking-promo');\n```\n\nSo let's click the **STAY ON WEB** button on the page! From devtools we can also get the selector of this button: `button.ml-promotion-action-button.ml-promotion-no-button`, use `page.click()` to trigger the click event:\n\n![Devtools - Button](resources/devtools-map-pop-button.png)\n\n```ts\n// click STAY ON WEB\nawait page.click('button.ml-promotion-action-button.ml-promotion-no-button');\n```\n\nAs the invisible animation last for 0.3s, we need to wait for more than 300ms after button clicked, before we capture the screenshot.\n\n![Devtools - Pop off](resources/devtools-map-pop-off.png)\n\n```ts\n// wait for more than 300 millisecond for browser to response with the events\nawait page.waitForTimeout(400);\nawait page.screenshot({ path: `out/map-${browserType.name\n```\n\n### Emulation - Geolocation\n\nNow we have the map in our current location (may be base on IP address) but we also have the ability to simulate to a different place. We can \"fly\" to town _Tewo_ by\nreating a context with \"geolocation\" permissions granted:\n\n```ts\nconst context = await browser.newContext({\n  ...iphone,\n  geolocation: {\n      longitude: 103.2199128,\n      latitude: 34.0556586,\n  },\n  permissions: ['geolocation'],\n});\n```\n\nIf you don't konw the longitude and latitude of your \"perfect place\", just search it in Google Maps then you can get it from the browser URL.\n\n![search-location](resources/devtools-map-search-location.png)\n\nClick the **Your Location** button to navigate to our emulated geolocation.\n\n![Your Location](resources/devtools-map-your-location.png)\n\n```ts\n// click `your location` to navi to current location\nawait page.click('button.ml-button-my-location-fab');\n// As I can not find any event which means relocat finished,\n// so we need to wait for some seconds for Google Maps to load resources\nawait page.waitForTimeout(500);\n```\n\nRe-run our project we will find us located in _Tewo Post Bureau_.\n\n\u003cimg alt=\"Tewo\" src=\"resources/map-chromium-tewo.png\" width=\"300px\" /\u003e\n\n### Input - Text Input\n\nAfter these simulations, we can start to control the page with more playwright APIs, just like what we click the page just now.\n\nFirst, fill in the search bar with our target place, like _Zhagana_.\n\n![Devtools - Click search box](resources/devtools-map-search-click.png)\n\n![Devtools - Search box input](resources/devtools-map-search-input.png)\n\n```ts\nawait page.click('div.ml-searchbox-button-textarea');\nawait page.waitForSelector('#ml-searchboxinput');\n// fill in content\nawait page.fill('#ml-searchboxinput', 'Zhagana');\n```\n\nSecond, press `Enter` to search.\n\n```ts\n// press Enter to start searching\nawait page.press('#ml-searchboxinput', 'Enter');\n```\n\nAfter that, we will get the target place with a red point, and there should be a **Directions** button at the bottom of the page.\n\n![Devtools - Directions](resources/devtools-map-directions.png)\n\nThird, click **Directions** and google will provide us the navigation route.\n\n```ts\n// click Directions\nconst directionsSelector = 'button[jsaction=\"pane.placeActions.directions\"]'\nawait page.waitForSelector(directionsSelector);\nawait page.click(directionsSelector)\n```\n\nPut them all together, with output path string as a result.\n\n```ts\nasync function screenshot(browserType: BrowserType\u003cBrowser\u003e): Promise\u003cstring\u003e {\n  // use `browserType` from arguments instead of hardcode\n  const browser = await browserType.launch();\n  // simulate browser behavior on a mobile device\n  const iphone = devices['iPhone X']\n  const context = await browser.newContext({\n    ...iphone,\n    geolocation: {\n      longitude: 103.2199128,\n      latitude: 34.0556586,\n    },\n    permissions: ['geolocation'],\n  });\n  // open web page\n  const page = await context.newPage();\n  await page.goto('https://www.google.com/maps');\n  // await page.waitForNavigation();\n\n  await page.waitForSelector('.ml-promotion-on-screen');\n  // click STAY ON WEB\n  await page.click('button.ml-promotion-action-button.ml-promotion-no-button');\n\n  // click `your location` to navi to current location\n  await page.click('button.ml-button-my-location-fab');\n  \n  // click to trigger input field\n  await page.click('div.ml-searchbox-button-textarea');\n  await page.waitForSelector('#ml-searchboxinput');\n  // fill in content\n  await page.fill('#ml-searchboxinput', 'Zhagana');\n  // press Enter to start searching\n  await page.press('#ml-searchboxinput', 'Enter');\n\n  // click Directions\n  const directionsSelector = 'button[jsaction=\"pane.placeActions.directions\"]'\n  await page.waitForSelector(directionsSelector);\n  await page.click(directionsSelector);\n  // wait for result\n  // As I can not find any event which means direction finished,\n  // so we need to wait for some seconds for Google Maps to load resources\n  await page.waitForTimeout(2000);\n\n  // take screenshot, output path string as a result.\n  const outputPath = `out/map-${browserType.name()}.png`;\n  await page.screenshot({ path: outputPath });\n  await browser.close();\n  return outputPath;\n}\n```\n\nOkay! Here comes out the two maps screenshots:\n\n| chromium | webkit |\n|:---:|:---:|\n| ![chromium](resources/map-chromium-navigate.png) | ![webkit](resources/map-webkit-navigate.png) |\n\n### Image Diff\n\nThe 2 screenshots look exactly the same, but we still want to use some tools to check. [Pixelmatch](https://github.com/mapbox/pixelmatch) is a simple and fast JavaScript pixel-level image comparison library. Create a function to compare two file A and B.\n\n```ts\nasync function diff(fileA: string, fileB: string) {\n  // read the 2 different PNG file\n  const mapChromium = PNG.sync.read(fs.readFileSync(fileA));\n  const mapWebkit = PNG.sync.read(fs.readFileSync(fileB));\n  // init the diff image buffer\n  const { width, height } = mapChromium;\n  const diffImg = new PNG({ width, height });\n  // pixel diff\n  pixelmatch(\n    mapChromium.data,\n    mapWebkit.data,\n    diffImg.data,\n    width,\n    height,\n    { threshold: 0.1 }\n  );\n  // print out the diff image\n  fs.writeFileSync('out/map-diff.png', PNG.sync.write(diffImg));\n}\n```\n\nAnd call this function after we generated the two screenshots:\n\n```ts\n\n(async () =\u003e {\n  const BROWSER_TYPES = [ chromium, webkit ];\n  // make screenshot all together\n  const maps = await Promise.all(BROWSER_TYPES.map((browserType) =\u003e {\n    return screenshot(browserType);\n  }));\n\n  await diff(maps[0], maps[1]);\n})();\n```\n\nBingo! Google Maps did a great job in the two different browser with almost the same behavior. The only different are font weight and also the navigate route weight.\n\n\u003cimg alt=\"Maps diff\" src=\"resources/map-diff.png\" width=\"300px\" /\u003e\n\n## Postscript\n\nZhagana is a wonderful place in Tiewu County, Gannan (Tibetan Autonomous Prefecture), Gansu province, China. Zhagana means “Rock Box” in Tibetan language, which is fitting as it is surrounded by large rocky spires on all sides.\n\n![Zhagana](resources/IMG_4721.jpg)\n\n[pw]: https://playwright.dev\n[ts]: https://www.typescriptlang.org\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcsbun%2Fzhagana","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcsbun%2Fzhagana","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcsbun%2Fzhagana/lists"}