├── .github └── workflows │ └── main.yml ├── .gitignore ├── Jenkinsfile ├── LICENSE ├── PreserveAuthenticatedState └── canva.test.ts ├── README.md ├── UIverification └── basicVerification.test.ts ├── advanceSelect ├── bootstrap.test.ts └── index.html ├── allText ├── index.html ├── main.css └── typesOfText.test.ts ├── android └── mobile.test.ts ├── annotation └── annotation.test.ts ├── apitest └── service-now.test.ts ├── azure-pipelines-1.yml ├── azure-pipelines.yml ├── baseUrlDemo ├── baseUrl.test.ts └── baseUrl.test2.ts ├── booksAPI.json ├── chromeExtension └── dictionary.test.ts ├── clickAndHold └── clickhold.test.ts ├── clipboard └── clip.test.ts ├── customReport ├── myReporter.ts └── report.test.ts ├── customWaits ├── index.html ├── waitForAlert.test.ts ├── waitForTextChange.ts └── waitForTitle.test.ts ├── data └── login.cred.json ├── destructing └── destructing.js ├── device.png ├── downloads └── download.test.ts ├── electron ├── Prices.exe └── electron.test.ts ├── eng.traineddata ├── fixtures ├── basePages.ts ├── myFixtures.ts ├── mytest.test.ts └── test.ts ├── frames └── frame.test.ts ├── generateReport.bat ├── github.config.ts ├── harDemo └── trackRequest.test.ts ├── helper └── globalsetup.ts ├── highlight └── highlight.test.ts ├── img.png ├── locatorAPI ├── locatorIndetail.test.ts └── locatorsVsElementHandle.test.ts ├── locators └── github.test.ts ├── logger ├── Logger.ts └── playLogger.test.ts ├── network ├── apiresponse.test.ts ├── auth.test.ts ├── blocker.test.ts └── mock-api-response.test.ts ├── package-lock.json ├── package.json ├── parameterized-test └── computerDB.test.ts ├── playwright.config.ts ├── pom └── letcodemodules │ └── credentials │ ├── Header.page.ts │ ├── Login.page.ts │ └── common.page.ts ├── reportDemo ├── clip.test.ts ├── github.test.ts └── service-now.test.ts ├── shadow-dom └── shadow.test.ts ├── slider └── slider.test.ts ├── tags └── tags.test.ts ├── tesseractdemo └── justdial.test.ts ├── test-1.spec.ts ├── test-runner.code-workspace ├── test ├── first.test.ts ├── smoke │ └── cms │ │ └── pom.test.ts └── tc001.test.ts.bak ├── timeout_demo └── waitfor.test.ts ├── tracing └── tracing.test.ts ├── tsconfig.json ├── v1.5-releases ├── drag.tets.ts ├── mousewheel.test.ts └── myAwesome.test.ts ├── visual-comparsion ├── visual.test.ts └── visual.test.ts-snapshots │ ├── letcode-chromium-win32.png │ └── snapshot-name-chromium-win32.png └── webscraping ├── amazonprice.test.ts ├── auth.json └── youtube.test.ts /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # on: 2 | # push: 3 | # branches: 4 | # - main 5 | 6 | # jobs: 7 | # e2e-tests: 8 | # runs-on: windows-latest # or macos-latest, ubuntu-latest 9 | # environment: cred 10 | # steps: 11 | # - uses: actions/checkout@v2 12 | 13 | # - uses: actions/setup-node@v1 14 | 15 | # - uses: microsoft/playwright-github-action@v1 16 | 17 | # - name: Install dependencies 18 | # run: npm install 19 | 20 | # - name: Run tests 21 | # run: npm run github-action 22 | # env: 23 | # MY_EMAIL: ${{ secrets.MY_EMAIL }} 24 | # MY_PASS: ${{ secrets.MY_PASS }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test-results 3 | allure-results 4 | my-ext 5 | playwright-report 6 | test-result.json -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { docker { image 'mcr.microsoft.com/playwright:v1.21.0-focal' } } 3 | stages { 4 | stage('e2e-tests') { 5 | steps { 6 | sh 'npm install' 7 | sh 'npm run test' 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Koushik Chatterjee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PreserveAuthenticatedState/canva.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("canva login", async ({ browser }) => { 4 | 5 | const context = await browser.newContext({ 6 | storageState: "./auth.json" 7 | }) 8 | const page = await context.newPage(); 9 | const ctxt = page.context(); 10 | ctxt.storageState() 11 | await page.goto("https://www.canva.com/"); 12 | await page.waitForTimeout(5000); 13 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playwright-Test-Runner 2 | ## What is Playwright? 3 | The playwright is a Node.js library to automate Chromium, Firefox, and WebKit with a single API. Playwright is built to enable cross-browser web testing. 4 | 5 | Playwright by Microsoft did start as a fork of Puppeteer 6 | Puppeteer is a node library to automate the chromium browsers with the JavaScript API 7 | ### Capabilities: 8 | * It spans multiple pages, domains, and iframes 9 | * Intercept network activity for stubbing and mocking network requests 10 | * Emulate mobile devices, geolocation, permissions 11 | * Native input events for mouse and keyboard 12 | * Upload & download support 13 | 14 | Playwright enables fast, reliable, and capable automation across all modern browsers 15 | 16 | ### Support for all browsers 17 | * Test on Chromium, Firefox, and WebKit 18 | * Test for mobile (device emulation) 19 | * Headless and headful 20 | 21 | ### Fast and reliable execution 22 | * Auto-wait APIs (clicks, types, etc) 23 | * Timeout-free automation 24 | * Lean parallelization with browser contexts 25 | * Wide variety of selectors (locators) & shadow-dom support 26 | * Can handle single page application 27 | 28 | ## Youtube Tutorial 29 | * Please follow the sequence to enhance your learning! 30 | 31 | [What is Playwright? | Playwright with Typescript & Jest - Part 1](https://youtu.be/zY-IoTYcbWs) 32 | 33 | [Playwright Jest Config & Launch Browser | Playwright - Part 2](https://youtu.be/DbdqflN3dJ4) 34 | 35 | [First Script - Auto Waits | Playwright - Part 3](https://youtu.be/9xEzNdG4XaQ) 36 | 37 | [Codeless Automation With PlayWright | Playwright - Part 4](https://youtu.be/gb43GiWwQKg) 38 | 39 | [Save Test Execution In Video | Playwright - Part 5](https://youtu.be/0125rwgsBP8) 40 | 41 | [How to upload files | Playwright - Part 6](https://youtu.be/e8jfjV71E6Q) 42 | 43 | [Handling different types of inputs | Playwright - Part 7](https://youtu.be/Slv5fuTrIZg) 44 | 45 | [Handling different types of alerts | Playwright - Part 8](https://youtu.be/RzBlwacFIl0) 46 | 47 | [Handling Select/DropDown | Playwright - Part 9](https://youtu.be/IubdSQFOdiU) 48 | 49 | [Window Handling | Playwright - Part 10](https://youtu.be/DyHQ3G442jY) 50 | 51 | [Frames | Playwright - Part 11](https://youtu.be/Vqm-8G81W8w) 52 | 53 | [Find Multiple Elements - part 12](https://youtu.be/54OwsiRa_eE) 54 | 55 | [How To Take Screenshot - part 13](https://youtu.be/G650JxukN1A) 56 | 57 | [How To Run In Local Browser | Playwright - Part 14](https://youtu.be/5LrRFHI81o4) 58 | 59 | [Drag and Drop | Playwright - Part 15](https://youtu.be/0wFkhkdcT8A) 60 | 61 | [Page Object Model | Playwright - Part 16](https://youtu.be/WSd6-X-n6P8) 62 | 63 | [POM Enhancement & JSON | Playwright - Part 17](https://youtu.be/00xGOpnOzds) 64 | 65 | [POM Enhancement & JSON | Playwright - Part 18](https://youtu.be/w05KGL8G0f4) 66 | 67 | [Jest Allure Report | Playwright - Part 19](https://youtu.be/tjpSkaBq9c0) 68 | 69 | [Jest Allure Report | Playwright - Part 20](https://youtu.be/xffrNccLIso) 70 | 71 | [Skip on failure | Playwright - Part 21](https://youtu.be/4-IBKtbAxlg) 72 | 73 | [Playwright Test Runner | Playwright - Part 22](https://youtu.be/zyJHd-4_4Lk) 74 | 75 | [Playwright Test Runner | Commands | Playwright Tutorial - Part 23](https://youtu.be/_gnb7TAQ8sQ) 76 | 77 | [Screenshot & Video On Test Failure | Playwright Tutorial - Part 24](https://youtu.be/P9VARCLnhKM) 78 | 79 | [Test Group & Hooks | Playwright Tutorial - Part 25](https://youtu.be/DHsAm12trBA) 80 | 81 | [Playwright Test Runner Group Disadvantage | Playwright tutorial - Part 26](https://youtu.be/zvAJZVIfxfk) 82 | 83 | [How To Handle Shadow DOM | Playwright tutorial - Part 27](https://youtu.be/4v8iPJH8_hg) 84 | 85 | [Playwright Visual Comparisons | Playwright part - 28](https://youtu.be/kyAeH-7lAL4) 86 | 87 | [Playwright Relative Locator | Playwright - part 29](https://youtu.be/bQjsXcxGjcg) 88 | 89 | [Playwright Github Action | Playwright - part 30](https://youtu.be/gjHEApRdFV4) 90 | 91 | [Playwright Skip Login | Playwright Tutorial part - 31](https://youtu.be/WHyQsX0w_5g) 92 | 93 | [HTTP Authentication | Playwright Tutorial - part 32](https://youtu.be/d80tgo0lnGs) 94 | 95 | [Playwright Trace Viewer | Playwright Tutorial - part 33](https://youtu.be/jY3CWJQ4V1I) 96 | 97 | [Playwright Fixtures | Playwright Tutorial - part 34](https://youtu.be/3gmRLLT_hx0) 98 | 99 | [Playwright Fixtures 2| Playwright Tutorial - part 35](https://youtu.be/XAAYP9PXToY) 100 | 101 | [Playwright UI Verifications | Playwright Tutorial - part 36](https://youtu.be/340d_Kkl9Eg) 102 | 103 | [What's new in Playwright? | Playwright Tutorial - Part 37](https://youtu.be/3IexgqtblT4) 104 | 105 | [Playwright Allure Report | Playwright Tutorial - Part 38](https://youtu.be/Pa7_klzkCXU) 106 | 107 | [Playwright Base URL | Playwright Tutorial - Part 39](https://youtu.be/w3lKsUCxeM8) 108 | 109 | [Tags in Test | Playwright Tutorial - Part 40](https://youtu.be/ZfTrdhKXAgo) 110 | 111 | [Calculate Youtube Playlist Duration | Web Scraping | Playwright Tutorial - Part 41](https://youtu.be/rUH1demFjQY) 112 | 113 | [How To Debug Playwright In VsCode | Playwright Tutorial - Part 42](https://youtu.be/Ink1oOqygWU) 114 | 115 | [Test Annotations | Playwright Tutorial - Part 43](https://youtu.be/l61cgSImhpU) 116 | 117 | [How To Wait For API Response | Playwright Tutorial - Part 44](https://youtu.be/MK0O8s3NBA4) 118 | 119 | [Playwright Version 1.14 | Playwright Tutorial - Part 45](https://youtu.be/xRxFGEOq95M) 120 | 121 | [Playwright Test Runner Skip on Failure | Playwright Tutorial - Part 46](https://youtu.be/q8oZD5uO6_s) 122 | 123 | [Element Handle VS Locator API | Playwright Tutorial - Part 47](https://youtu.be/3Vsy2uSCo_Y) 124 | 125 | [InnerHTML vs InnerText vs TextContent | Playwright Tutorial - Part 48](https://youtu.be/MxAZiAbv45Q) 126 | 127 | [Playwright Android Automation | Playwright Tutorial Part - 49](https://youtu.be/Nte3PIffyYk) 128 | 129 | [Web Scrapping Using Playwright | Github Copilot | Playwright Tutorial Part 50](https://youtu.be/heGLd50G_zA) 130 | 131 | [Winston Logger | Playwright Tutorial Part 51](https://youtu.be/HtVJhuKv2zA) 132 | 133 | [Playwright Logger | Read console logs | Playwright Tutorial - Part 52](https://youtu.be/KbZZpwH6eOI) 134 | 135 | [Playwright locator API In Detail | Playwright Tutorial - Part 53](https://youtu.be/cvLaBBfuYmA) 136 | 137 | [Playwright Explicit wait| Playwright Tutorial Part 54]() 138 | 139 | [Playwright Custom Report | Playwright Tutorial Part 55](https://youtu.be/t-KsH5p60sk) 140 | 141 | [Get Started With Playwright Framework | Playwright Tutorial Part 56](https://youtu.be/ELAp41NV13E) 142 | 143 | [Parallel Tests - Mouse Wheel Control | Playwright Tutorial Part 57](https://youtu.be/UXj0LTBff7Y) 144 | 145 | [Access Clipboard URL and Open in New Tab | Playwright Tutorial Part 58](https://youtu.be/r8O_myjoW98) 146 | 147 | [Network - Abort Request | Playwright Tutorial Part 59](https://youtu.be/jIEAtdVV1j4) 148 | 149 | [Click & Hold | Playwright Tutorial Part 60](https://youtu.be/0SruLQy2pgA) 150 | 151 | [Playwright API Testing | Playwright Tutorial Part 61](https://youtu.be/deEK0lHrC-w) 152 | 153 | [See the moves - Slider | Playwright Tutorial Part 62](https://youtu.be/OKsPOxNYQWI) 154 | 155 | [Zip Report | Global TearDown | Playwright Tutorial Part 63](https://youtu.be/vMTBAbaLaf4) 156 | 157 | [New Frame Locator Functions | Playwright Tutorial Part 64](https://youtu.be/8P72y8ZNmVY) 158 | 159 | [How To Get CSS Value | Playwright Tutorial Part 65](https://youtu.be/eI_AaU4kJXU) 160 | A 161 | [Tesseract JS | Extract Text From Image | Playwright Tutorial Part 66](https://youtu.be/9kqEYQ-Pc-8) 162 | 163 | [Playwright is a game changer | Playwright Tutorial part - 67](https://youtu.be/EWXW2z0U3OQ) 164 | 165 | [How to download file & attach to report | Playwright Tutorial part - 68](https://youtu.be/xQxan2SlnKA) 166 | 167 | [Locator - Has & Has Text | Playwright Tutorial part - 69](https://youtu.be/1RUMsrYXUcA) 168 | 169 | [Playwright Hard & Soft Assert, Highlight Locators | Playwright tutorial - part 70](https://youtu.be/i2iLTjdw2y0) 170 | 171 | [POM Clean Code | Playwright Tutorial part 71](https://youtu.be/Vm870yqU7Wc) 172 | 173 | [Maximize Window | Playwright Tutorial - Part 72](https://youtu.be/GWzN9EYSNPI) 174 | 175 | [Playwright Installation - 2022](https://youtu.be/AdFgipIPAlg) 176 | 177 | [How To Test Mobile & Desktop Browser? Playwright tutorial - Part 73](https://youtu.be/NBHDp-QvGBQ) 178 | 179 | [Playwright + Azure Pipeline | Playwright Tutorial - Part 74](https://youtu.be/RCzXuCt8Lng) 180 | 181 | [Parametrize Tests | Playwright Tutorial - Part 75](https://youtu.be/5F-DTlyyMAA) 182 | 183 | [Playwright Framework #1 | Base functions | Playwright Tutorial - Part 76](https://youtu.be/orWd3b6zqHI) 184 | 185 | [Migrate from Jest to Playwright Test Runner | Playwright Tutorial - part 77](https://youtu.be/qoG5gidhj_A) 186 | 187 | [WaitFor an Element | Playwright Tutorial - Part 78](https://youtu.be/vTTwL6AZ6l0) 188 | 189 | [Network Replay | HAR | Playwright Tutorial - Part 79](https://youtu.be/uGS55H2HsoI) 190 | 191 | [Playwright Framework #2 | Execute in multiple environment | Playwright Tutorial - Part 80](https://youtu.be/GQStVI5qbLI) 192 | 193 | [Mocking test data | Playwright part 82](https://youtu.be/c7zCQ8FKih4) 194 | -------------------------------------------------------------------------------- /UIverification/basicVerification.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("Basic UI verification", async ({ page }) => { 4 | 5 | // isDisabled()? 6 | await page.goto("https://letcode.in/edit"); 7 | expect(await page.isDisabled("#noEdit")).toBe(false); 8 | 9 | // isEditable()? 10 | const edit = await page.isEditable("#dontwrite"); 11 | console.log("is edit? " + edit); 12 | expect(edit).not.toBe(true); 13 | 14 | 15 | // isEnabled()? 16 | await page.goto("https://letcode.in/buttons"); 17 | const ele = await page.$("#isDisabled"); 18 | console.log(await ele?.isEnabled()); 19 | 20 | // isHidden()? 21 | // isVisible()? 22 | console.log(await ele?.isVisible()); 23 | 24 | // checkbox/radio check/uncheck 25 | // checkbox/radio isChecked? 26 | await page.goto("https://letcode.in/radio"); 27 | await page.waitForSelector("input:below(:text('Find if the checkbox is selected?'))") 28 | const checkBox = await page.$("input:below(:text('Find if the checkbox is selected?'))") 29 | if (checkBox) { 30 | expect(await checkBox.isChecked()).toBe(true); 31 | await checkBox.uncheck(); 32 | } 33 | else { throw new Error("Elementnd"); } 34 | }) 35 | 36 | test("color", async ({ page }) => { 37 | await page.goto("https://letcode.in/buttons"); 38 | const btn = page.locator("#home"); 39 | const color = await btn.evaluate((ele) => { 40 | return window.getComputedStyle(ele).getPropertyValue("background-color") 41 | }); 42 | console.log(color); 43 | expect(color).toBe("rgb(250, 124, 145)"); 44 | 45 | 46 | }) 47 | -------------------------------------------------------------------------------- /advanceSelect/bootstrap.test.ts: -------------------------------------------------------------------------------- 1 | // https://zetcode.com/javascript/regex/ - reference to learn Regex 2 | import { Page, test } from "@playwright/test"; 3 | 4 | test.use({ 5 | viewport: { 6 | height: 824, 7 | width: 1536 8 | } 9 | }) 10 | test("Select from bootstrap dropdown", async ({ page }) => { 11 | 12 | await page.goto("file:///Y:/my-code-base/Playwright-Test-Runner/advanceSelect/index.html"); 13 | // await page.locator(".selectpicker") 14 | // .selectOption({ 15 | // label: "Playwright" 16 | // }) 17 | await selectOption(page, /^Playwright$/g); 18 | await selectOption(page, /^Cypress$/g); 19 | await page.waitForTimeout(5000) 20 | }) 21 | 22 | async function selectOption(page: Page, data: RegExp) { 23 | await page.locator(".filter-option").click(); 24 | await page.locator(".dropdown-menu") 25 | .locator("li", { 26 | has: page.locator("a span"), 27 | hasText: data 28 | }).click(); 29 | } 30 | -------------------------------------------------------------------------------- /advanceSelect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /allText/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Different text property 9 | 10 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |

Different Types Of Text Properties

22 |

Playwright Automation

23 | 24 | 25 | 26 |
27 |
28 |

29 |
30 |
31 |
32 |
33 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /allText/main.css: -------------------------------------------------------------------------------- 1 | /*! Spectre.css v0.5.9 | MIT License | github.com/picturepan2/spectre */ 2 | /* Manually forked from Normalize.css */ 3 | /* normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 4 | /** 1. Change the default font family in all browsers (opinionated). 2. Correct the line height in all browsers. 3. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS. */ 5 | /* Document ========================================================================== */ 6 | html { 7 | font-family: sans-serif; /* 1 */ 8 | -webkit-text-size-adjust: 100%; /* 3 */ 9 | -ms-text-size-adjust: 100%; /* 3 */ 10 | } 11 | 12 | /* Sections ========================================================================== */ 13 | /** Remove the margin in all browsers (opinionated). */ 14 | body { 15 | margin: 0; 16 | } 17 | 18 | /** Add the correct display in IE 9-. */ 19 | article, 20 | aside, 21 | footer, 22 | header, 23 | nav, 24 | section { 25 | display: block; 26 | } 27 | 28 | /** Correct the font size and margin on `h1` elements within `section` and `article` contexts in Chrome, Firefox, and Safari. */ 29 | h1 { 30 | font-size: 2em; 31 | margin: .67em 0; 32 | } 33 | 34 | /* Grouping content ========================================================================== */ 35 | /** Add the correct display in IE 9-. 1. Add the correct display in IE. */ 36 | figcaption, 37 | figure, 38 | main { 39 | /* 1 */ display: block; 40 | } 41 | 42 | /** Add the correct margin in IE 8 (removed). */ 43 | /** 1. Add the correct box sizing in Firefox. 2. Show the overflow in Edge and IE. */ 44 | hr { 45 | box-sizing: content-box; /* 1 */ 46 | height: 0; /* 1 */ 47 | overflow: visible; /* 2 */ 48 | } 49 | 50 | /** 1. Correct the inheritance and scaling of font size in all browsers. (removed) 2. Correct the odd `em` font sizing in all browsers. */ 51 | /* Text-level semantics ========================================================================== */ 52 | /** 1. Remove the gray background on active links in IE 10. 2. Remove gaps in links underline in iOS 8+ and Safari 8+. */ 53 | a { 54 | background-color: transparent; /* 1 */ 55 | -webkit-text-decoration-skip: objects; /* 2 */ 56 | } 57 | 58 | /** Remove the outline on focused links when they are also active or hovered in all browsers (opinionated). */ 59 | a:active, 60 | a:hover { 61 | outline-width: 0; 62 | } 63 | 64 | /** Modify default styling of address. */ 65 | address { 66 | font-style: normal; 67 | } 68 | 69 | /** 1. Remove the bottom border in Firefox 39-. 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. (removed) */ 70 | /** Prevent the duplicate application of `bolder` by the next rule in Safari 6. */ 71 | b, 72 | strong { 73 | font-weight: inherit; 74 | } 75 | 76 | /** Add the correct font weight in Chrome, Edge, and Safari. */ 77 | b, 78 | strong { 79 | font-weight: bolder; 80 | } 81 | 82 | /** 1. Correct the inheritance and scaling of font size in all browsers. 2. Correct the odd `em` font sizing in all browsers. */ 83 | code, 84 | kbd, 85 | pre, 86 | samp { 87 | font-family: "SF Mono", "Segoe UI Mono", "Roboto Mono", Menlo, Courier, monospace; /* 1 (changed) */ 88 | font-size: 1em; /* 2 */ 89 | } 90 | 91 | /** Add the correct font style in Android 4.3-. */ 92 | dfn { 93 | font-style: italic; 94 | } 95 | 96 | /** Add the correct background and color in IE 9-. (Removed) */ 97 | /** Add the correct font size in all browsers. */ 98 | small { 99 | font-size: 80%; 100 | font-weight: 400; /* (added) */ 101 | } 102 | 103 | /** Prevent `sub` and `sup` elements from affecting the line height in all browsers. */ 104 | sub, 105 | sup { 106 | font-size: 75%; 107 | line-height: 0; 108 | position: relative; 109 | vertical-align: baseline; 110 | } 111 | 112 | sub { 113 | bottom: -.25em; 114 | } 115 | 116 | sup { 117 | top: -.5em; 118 | } 119 | 120 | /* Embedded content ========================================================================== */ 121 | /** Add the correct display in IE 9-. */ 122 | audio, 123 | video { 124 | display: inline-block; 125 | } 126 | 127 | /** Add the correct display in iOS 4-7. */ 128 | audio:not([controls]) { 129 | display: none; 130 | height: 0; 131 | } 132 | 133 | /** Remove the border on images inside links in IE 10-. */ 134 | img { 135 | border-style: none; 136 | } 137 | 138 | /** Hide the overflow in IE. */ 139 | svg:not(:root) { 140 | overflow: hidden; 141 | } 142 | 143 | /* Forms ========================================================================== */ 144 | /** 1. Change the font styles in all browsers (opinionated). 2. Remove the margin in Firefox and Safari. */ 145 | button, 146 | input, 147 | optgroup, 148 | select, 149 | textarea { 150 | font-family: inherit; /* 1 (changed) */ 151 | font-size: inherit; /* 1 (changed) */ 152 | line-height: inherit; /* 1 (changed) */ 153 | margin: 0; /* 2 */ 154 | } 155 | 156 | /** Show the overflow in IE. 1. Show the overflow in Edge. */ 157 | button, 158 | input { 159 | /* 1 */ overflow: visible; 160 | } 161 | 162 | /** Remove the inheritance of text transform in Edge, Firefox, and IE. 1. Remove the inheritance of text transform in Firefox. */ 163 | button, 164 | select { 165 | /* 1 */ text-transform: none; 166 | } 167 | 168 | /** 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` controls in Android 4. 2. Correct the inability to style clickable types in iOS and Safari. */ 169 | button, 170 | html [type="button"], 171 | [type="reset"], 172 | [type="submit"] { 173 | -webkit-appearance: button; /* 2 */ 174 | } 175 | 176 | /** Remove the inner border and padding in Firefox. */ 177 | button::-moz-focus-inner, 178 | [type="button"]::-moz-focus-inner, 179 | [type="reset"]::-moz-focus-inner, 180 | [type="submit"]::-moz-focus-inner { 181 | border-style: none; 182 | padding: 0; 183 | } 184 | 185 | /** Restore the focus styles unset by the previous rule (removed). */ 186 | /** Change the border, margin, and padding in all browsers (opinionated) (changed). */ 187 | fieldset { 188 | border: 0; 189 | margin: 0; 190 | padding: 0; 191 | } 192 | 193 | /** 1. Correct the text wrapping in Edge and IE. 2. Correct the color inheritance from `fieldset` elements in IE. 3. Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. */ 194 | legend { 195 | box-sizing: border-box; /* 1 */ 196 | color: inherit; /* 2 */ 197 | display: table; /* 1 */ 198 | max-width: 100%; /* 1 */ 199 | padding: 0; /* 3 */ 200 | white-space: normal; /* 1 */ 201 | } 202 | 203 | /** 1. Add the correct display in IE 9-. 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. */ 204 | progress { 205 | display: inline-block; /* 1 */ 206 | vertical-align: baseline; /* 2 */ 207 | } 208 | 209 | /** Remove the default vertical scrollbar in IE. */ 210 | textarea { 211 | overflow: auto; 212 | } 213 | 214 | /** 1. Add the correct box sizing in IE 10-. 2. Remove the padding in IE 10-. */ 215 | [type="checkbox"], 216 | [type="radio"] { 217 | box-sizing: border-box; /* 1 */ 218 | padding: 0; /* 2 */ 219 | } 220 | 221 | /** Correct the cursor style of increment and decrement buttons in Chrome. */ 222 | [type="number"]::-webkit-inner-spin-button, 223 | [type="number"]::-webkit-outer-spin-button { 224 | height: auto; 225 | } 226 | 227 | /** 1. Correct the odd appearance in Chrome and Safari. 2. Correct the outline style in Safari. */ 228 | [type="search"] { 229 | -webkit-appearance: textfield; /* 1 */ 230 | outline-offset: -2px; /* 2 */ 231 | } 232 | 233 | /** Remove the inner padding and cancel buttons in Chrome and Safari on macOS. */ 234 | [type="search"]::-webkit-search-cancel-button, 235 | [type="search"]::-webkit-search-decoration { 236 | -webkit-appearance: none; 237 | } 238 | 239 | /** 1. Correct the inability to style clickable types in iOS and Safari. 2. Change font properties to `inherit` in Safari. */ 240 | ::-webkit-file-upload-button { 241 | -webkit-appearance: button; /* 1 */ 242 | font: inherit; /* 2 */ 243 | } 244 | 245 | /* Interactive ========================================================================== */ 246 | /* Add the correct display in IE 9-. 1. Add the correct display in Edge, IE, and Firefox. */ 247 | details, 248 | menu { 249 | display: block; 250 | } 251 | 252 | /* Add the correct display in all browsers. */ 253 | summary { 254 | display: list-item; 255 | outline: none; 256 | } 257 | 258 | /* Scripting ========================================================================== */ 259 | /** Add the correct display in IE 9-. */ 260 | canvas { 261 | display: inline-block; 262 | } 263 | 264 | /** Add the correct display in IE. */ 265 | template { 266 | display: none; 267 | } 268 | 269 | /* Hidden ========================================================================== */ 270 | /** Add the correct display in IE 10-. */ 271 | [hidden] { 272 | display: none; 273 | } 274 | 275 | *, 276 | *::before, 277 | *::after { 278 | box-sizing: inherit; 279 | } 280 | 281 | html { 282 | box-sizing: border-box; 283 | font-size: 20px; 284 | line-height: 1.5; 285 | -webkit-tap-highlight-color: transparent; 286 | } 287 | 288 | body { 289 | background: #fff; 290 | color: #3b4351; 291 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; 292 | font-size: .8rem; 293 | overflow-x: hidden; 294 | text-rendering: optimizeLegibility; 295 | } 296 | 297 | a { 298 | color: #8a4d76; 299 | outline: none; 300 | text-decoration: none; 301 | } 302 | 303 | a:focus { 304 | box-shadow: 0 0 0 .1rem rgba(138, 77, 118, .2); 305 | } 306 | 307 | a:focus, 308 | a:hover, 309 | a:active, 310 | a.active { 311 | color: #693b5a; 312 | text-decoration: underline; 313 | } 314 | 315 | a:visited { 316 | color: #a86291; 317 | } 318 | 319 | h1, 320 | h2, 321 | h3, 322 | h4, 323 | h5, 324 | h6 { 325 | color: inherit; 326 | font-weight: 500; 327 | line-height: 1.2; 328 | margin-bottom: .5em; 329 | margin-top: 0; 330 | } 331 | 332 | .h1, 333 | .h2, 334 | .h3, 335 | .h4, 336 | .h5, 337 | .h6 { 338 | font-weight: 500; 339 | } 340 | 341 | h1, 342 | .h1 { 343 | font-size: 2rem; 344 | } 345 | 346 | h2, 347 | .h2 { 348 | font-size: 1.6rem; 349 | } 350 | 351 | h3, 352 | .h3 { 353 | font-size: 1.4rem; 354 | } 355 | 356 | h4, 357 | .h4 { 358 | font-size: 1.2rem; 359 | } 360 | 361 | h5, 362 | .h5 { 363 | font-size: 1rem; 364 | } 365 | 366 | h6, 367 | .h6 { 368 | font-size: .8rem; 369 | } 370 | 371 | p { 372 | margin: 0 0 1.2rem; 373 | } 374 | 375 | a, 376 | ins, 377 | u { 378 | text-decoration-skip: ink edges; 379 | } 380 | 381 | abbr[title] { 382 | border-bottom: .05rem dotted; 383 | cursor: help; 384 | text-decoration: none; 385 | } 386 | 387 | kbd { 388 | background: #303742; 389 | border-radius: .1rem; 390 | color: #fff; 391 | font-size: .7rem; 392 | line-height: 1.25; 393 | padding: .1rem .2rem; 394 | } 395 | 396 | mark { 397 | background: #ffe9b3; 398 | border-bottom: .05rem solid #ffd367; 399 | border-radius: .1rem; 400 | color: #3b4351; 401 | padding: .05rem .1rem 0; 402 | } 403 | 404 | blockquote { 405 | border-left: .1rem solid #dadee4; 406 | margin-left: 0; 407 | padding: .4rem .8rem; 408 | } 409 | 410 | blockquote p:last-child { 411 | margin-bottom: 0; 412 | } 413 | 414 | ul, 415 | ol { 416 | margin: .8rem 0 .8rem .8rem; 417 | padding: 0; 418 | } 419 | 420 | ul ul, 421 | ul ol, 422 | ol ul, 423 | ol ol { 424 | margin: .8rem 0 .8rem .8rem; 425 | } 426 | 427 | ul li, 428 | ol li { 429 | margin-top: .4rem; 430 | } 431 | 432 | ul { 433 | list-style: disc inside; 434 | } 435 | 436 | ul ul { 437 | list-style-type: circle; 438 | } 439 | 440 | ol { 441 | list-style: decimal inside; 442 | } 443 | 444 | ol ol { 445 | list-style-type: lower-alpha; 446 | } 447 | 448 | dl dt { 449 | font-weight: bold; 450 | } 451 | 452 | dl dd { 453 | margin: .4rem 0 .8rem 0; 454 | } 455 | 456 | html:lang(zh), 457 | html:lang(zh-Hans), 458 | .lang-zh, 459 | .lang-zh-hans { 460 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", sans-serif; 461 | } 462 | 463 | html:lang(zh-Hant), 464 | .lang-zh-hant { 465 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang TC", "Hiragino Sans CNS", "Microsoft JhengHei", "Helvetica Neue", sans-serif; 466 | } 467 | 468 | html:lang(ja), 469 | .lang-ja { 470 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Hiragino Sans", "Hiragino Kaku Gothic Pro", "Yu Gothic", YuGothic, Meiryo, "Helvetica Neue", sans-serif; 471 | } 472 | 473 | html:lang(ko), 474 | .lang-ko { 475 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Malgun Gothic", "Helvetica Neue", sans-serif; 476 | } 477 | 478 | :lang(zh) ins, 479 | :lang(zh) u, 480 | :lang(ja) ins, 481 | :lang(ja) u, 482 | .lang-cjk ins, 483 | .lang-cjk u { 484 | border-bottom: .05rem solid; 485 | text-decoration: none; 486 | } 487 | 488 | :lang(zh) del + del, 489 | :lang(zh) del + s, 490 | :lang(zh) ins + ins, 491 | :lang(zh) ins + u, 492 | :lang(zh) s + del, 493 | :lang(zh) s + s, 494 | :lang(zh) u + ins, 495 | :lang(zh) u + u, 496 | :lang(ja) del + del, 497 | :lang(ja) del + s, 498 | :lang(ja) ins + ins, 499 | :lang(ja) ins + u, 500 | :lang(ja) s + del, 501 | :lang(ja) s + s, 502 | :lang(ja) u + ins, 503 | :lang(ja) u + u, 504 | .lang-cjk del + del, 505 | .lang-cjk del + s, 506 | .lang-cjk ins + ins, 507 | .lang-cjk ins + u, 508 | .lang-cjk s + del, 509 | .lang-cjk s + s, 510 | .lang-cjk u + ins, 511 | .lang-cjk u + u { 512 | margin-left: .125em; 513 | } 514 | 515 | .table { 516 | border-collapse: collapse; 517 | border-spacing: 0; 518 | text-align: left; 519 | width: 100%; 520 | } 521 | 522 | .table.table-striped tbody tr:nth-of-type(odd) { 523 | background: #f7f8f9; 524 | } 525 | 526 | .table tbody tr.active, 527 | .table.table-striped tbody tr.active { 528 | background: #eef0f3; 529 | } 530 | 531 | .table.table-hover tbody tr:hover { 532 | background: #eef0f3; 533 | } 534 | 535 | .table.table-scroll { 536 | display: block; 537 | overflow-x: auto; 538 | padding-bottom: .75rem; 539 | white-space: nowrap; 540 | } 541 | 542 | .table td, 543 | .table th { 544 | border-bottom: .05rem solid #dadee4; 545 | padding: .6rem .4rem; 546 | } 547 | 548 | .table th { 549 | border-bottom-width: .1rem; 550 | } 551 | 552 | .btn { 553 | -webkit-appearance: none; 554 | -moz-appearance: none; 555 | appearance: none; 556 | background: #fff; 557 | border: .05rem solid #8a4d76; 558 | border-radius: .1rem; 559 | color: #8a4d76; 560 | cursor: pointer; 561 | display: inline-block; 562 | font-size: .8rem; 563 | height: 1.8rem; 564 | line-height: 1.2rem; 565 | outline: none; 566 | padding: .25rem .4rem; 567 | text-align: center; 568 | text-decoration: none; 569 | transition: background .2s, border .2s, box-shadow .2s, color .2s; 570 | -webkit-user-select: none; 571 | -ms-user-select: none; 572 | user-select: none; 573 | vertical-align: middle; 574 | white-space: nowrap; 575 | } 576 | 577 | .btn:focus { 578 | box-shadow: 0 0 0 .1rem rgba(138, 77, 118, .2); 579 | } 580 | 581 | .btn:focus, 582 | .btn:hover { 583 | background: #dabcd0; 584 | border-color: #80486e; 585 | text-decoration: none; 586 | } 587 | 588 | .btn:active, 589 | .btn.active { 590 | background: #80486e; 591 | border-color: #703e60; 592 | color: #fff; 593 | text-decoration: none; 594 | } 595 | 596 | .btn:active.loading::after, 597 | .btn.active.loading::after { 598 | border-bottom-color: #fff; 599 | border-left-color: #fff; 600 | } 601 | 602 | .btn[disabled], 603 | .btn:disabled, 604 | .btn.disabled { 605 | cursor: default; 606 | opacity: .5; 607 | pointer-events: none; 608 | } 609 | 610 | .btn.btn-primary { 611 | background: #8a4d76; 612 | border-color: #80486e; 613 | color: #fff; 614 | } 615 | 616 | .btn.btn-primary:focus, 617 | .btn.btn-primary:hover { 618 | background: #7a4468; 619 | border-color: #703e60; 620 | color: #fff; 621 | } 622 | 623 | .btn.btn-primary:active, 624 | .btn.btn-primary.active { 625 | background: #734062; 626 | border-color: #693b5a; 627 | color: #fff; 628 | } 629 | 630 | .btn.btn-primary.loading::after { 631 | border-bottom-color: #fff; 632 | border-left-color: #fff; 633 | } 634 | 635 | .btn.btn-success { 636 | background: #32b643; 637 | border-color: #2faa3f; 638 | color: #fff; 639 | } 640 | 641 | .btn.btn-success:focus { 642 | box-shadow: 0 0 0 .1rem rgba(50, 182, 67, .2); 643 | } 644 | 645 | .btn.btn-success:focus, 646 | .btn.btn-success:hover { 647 | background: #30ae40; 648 | border-color: #2da23c; 649 | color: #fff; 650 | } 651 | 652 | .btn.btn-success:active, 653 | .btn.btn-success.active { 654 | background: #2a9a39; 655 | border-color: #278e34; 656 | color: #fff; 657 | } 658 | 659 | .btn.btn-success.loading::after { 660 | border-bottom-color: #fff; 661 | border-left-color: #fff; 662 | } 663 | 664 | .btn.btn-error { 665 | background: #e85600; 666 | border-color: #d95000; 667 | color: #fff; 668 | } 669 | 670 | .btn.btn-error:focus { 671 | box-shadow: 0 0 0 .1rem rgba(232, 86, 0, .2); 672 | } 673 | 674 | .btn.btn-error:focus, 675 | .btn.btn-error:hover { 676 | background: #de5200; 677 | border-color: #cf4d00; 678 | color: #fff; 679 | } 680 | 681 | .btn.btn-error:active, 682 | .btn.btn-error.active { 683 | background: #c44900; 684 | border-color: #b54300; 685 | color: #fff; 686 | } 687 | 688 | .btn.btn-error.loading::after { 689 | border-bottom-color: #fff; 690 | border-left-color: #fff; 691 | } 692 | 693 | .btn.btn-link { 694 | background: transparent; 695 | border-color: transparent; 696 | color: #8a4d76; 697 | } 698 | 699 | .btn.btn-link:focus, 700 | .btn.btn-link:hover, 701 | .btn.btn-link:active, 702 | .btn.btn-link.active { 703 | color: #693b5a; 704 | } 705 | 706 | .btn.btn-sm { 707 | font-size: .7rem; 708 | height: 1.4rem; 709 | padding: .05rem .3rem; 710 | } 711 | 712 | .btn.btn-lg { 713 | font-size: .9rem; 714 | height: 2rem; 715 | padding: .35rem .6rem; 716 | } 717 | 718 | .btn.btn-block { 719 | display: block; 720 | width: 100%; 721 | } 722 | 723 | .btn.btn-action { 724 | padding-left: 0; 725 | padding-right: 0; 726 | width: 1.8rem; 727 | } 728 | 729 | .btn.btn-action.btn-sm { 730 | width: 1.4rem; 731 | } 732 | 733 | .btn.btn-action.btn-lg { 734 | width: 2rem; 735 | } 736 | 737 | .btn.btn-clear { 738 | background: transparent; 739 | border: 0; 740 | color: currentColor; 741 | height: 1rem; 742 | line-height: .8rem; 743 | margin-left: .2rem; 744 | margin-right: -2px; 745 | opacity: 1; 746 | padding: .1rem; 747 | text-decoration: none; 748 | width: 1rem; 749 | } 750 | 751 | .btn.btn-clear:focus, 752 | .btn.btn-clear:hover { 753 | background: rgba(247, 248, 249, .5); 754 | opacity: .95; 755 | } 756 | 757 | .btn.btn-clear::before { 758 | content: "\2715"; 759 | } 760 | 761 | .btn-group { 762 | display: -ms-inline-flexbox; 763 | display: inline-flex; 764 | -ms-flex-wrap: wrap; 765 | flex-wrap: wrap; 766 | } 767 | 768 | .btn-group .btn { 769 | -ms-flex: 1 0 auto; 770 | flex: 1 0 auto; 771 | } 772 | 773 | .btn-group .btn:first-child:not(:last-child) { 774 | border-bottom-right-radius: 0; 775 | border-top-right-radius: 0; 776 | } 777 | 778 | .btn-group .btn:not(:first-child):not(:last-child) { 779 | border-radius: 0; 780 | margin-left: -.05rem; 781 | } 782 | 783 | .btn-group .btn:last-child:not(:first-child) { 784 | border-bottom-left-radius: 0; 785 | border-top-left-radius: 0; 786 | margin-left: -.05rem; 787 | } 788 | 789 | .btn-group .btn:focus, 790 | .btn-group .btn:hover, 791 | .btn-group .btn:active, 792 | .btn-group .btn.active { 793 | z-index: 1; 794 | } 795 | 796 | .btn-group.btn-group-block { 797 | display: -ms-flexbox; 798 | display: flex; 799 | } 800 | 801 | .btn-group.btn-group-block .btn { 802 | -ms-flex: 1 0 0; 803 | flex: 1 0 0; 804 | } 805 | 806 | .form-group:not(:last-child) { 807 | margin-bottom: .4rem; 808 | } 809 | 810 | fieldset { 811 | margin-bottom: .8rem; 812 | } 813 | 814 | legend { 815 | font-size: .9rem; 816 | font-weight: 500; 817 | margin-bottom: .8rem; 818 | } 819 | 820 | .form-label { 821 | display: block; 822 | line-height: 1.2rem; 823 | padding: .3rem 0; 824 | } 825 | 826 | .form-label.label-sm { 827 | font-size: .7rem; 828 | padding: .1rem 0; 829 | } 830 | 831 | .form-label.label-lg { 832 | font-size: .9rem; 833 | padding: .4rem 0; 834 | } 835 | 836 | .form-input { 837 | -webkit-appearance: none; 838 | -moz-appearance: none; 839 | appearance: none; 840 | background: #fff; 841 | background-image: none; 842 | border: .05rem solid #bcc3ce; 843 | border-radius: .1rem; 844 | color: #3b4351; 845 | display: block; 846 | font-size: .8rem; 847 | height: 1.8rem; 848 | line-height: 1.2rem; 849 | max-width: 100%; 850 | outline: none; 851 | padding: .25rem .4rem; 852 | position: relative; 853 | transition: background .2s, border .2s, box-shadow .2s, color .2s; 854 | width: 100%; 855 | } 856 | 857 | .form-input:focus { 858 | border-color: #8a4d76; 859 | box-shadow: 0 0 0 .1rem rgba(138, 77, 118, .2); 860 | } 861 | 862 | .form-input:-ms-input-placeholder { 863 | color: #bcc3ce; 864 | } 865 | 866 | .form-input::-ms-input-placeholder { 867 | color: #bcc3ce; 868 | } 869 | 870 | .form-input::placeholder { 871 | color: #bcc3ce; 872 | } 873 | 874 | .form-input.input-sm { 875 | font-size: .7rem; 876 | height: 1.4rem; 877 | padding: .05rem .3rem; 878 | } 879 | 880 | .form-input.input-lg { 881 | font-size: .9rem; 882 | height: 2rem; 883 | padding: .35rem .6rem; 884 | } 885 | 886 | .form-input.input-inline { 887 | display: inline-block; 888 | vertical-align: middle; 889 | width: auto; 890 | } 891 | 892 | .form-input[type="file"] { 893 | height: auto; 894 | } 895 | 896 | textarea.form-input, 897 | textarea.form-input.input-lg, 898 | textarea.form-input.input-sm { 899 | height: auto; 900 | } 901 | 902 | .form-input-hint { 903 | color: #bcc3ce; 904 | font-size: .7rem; 905 | margin-top: .2rem; 906 | } 907 | 908 | .has-success .form-input-hint, 909 | .is-success + .form-input-hint { 910 | color: #32b643; 911 | } 912 | 913 | .has-error .form-input-hint, 914 | .is-error + .form-input-hint { 915 | color: #e85600; 916 | } 917 | 918 | .form-select { 919 | -webkit-appearance: none; 920 | -moz-appearance: none; 921 | appearance: none; 922 | background: #fff; 923 | border: .05rem solid #bcc3ce; 924 | border-radius: .1rem; 925 | color: inherit; 926 | font-size: .8rem; 927 | height: 1.8rem; 928 | line-height: 1.2rem; 929 | outline: none; 930 | padding: .25rem .4rem; 931 | vertical-align: middle; 932 | width: 100%; 933 | } 934 | 935 | .form-select:focus { 936 | border-color: #8a4d76; 937 | box-shadow: 0 0 0 .1rem rgba(138, 77, 118, .2); 938 | } 939 | 940 | .form-select::-ms-expand { 941 | display: none; 942 | } 943 | 944 | .form-select.select-sm { 945 | font-size: .7rem; 946 | height: 1.4rem; 947 | padding: .05rem 1.1rem .05rem .3rem; 948 | } 949 | 950 | .form-select.select-lg { 951 | font-size: .9rem; 952 | height: 2rem; 953 | padding: .35rem 1.4rem .35rem .6rem; 954 | } 955 | 956 | .form-select[size], 957 | .form-select[multiple] { 958 | height: auto; 959 | padding: .25rem .4rem; 960 | } 961 | 962 | .form-select[size] option, 963 | .form-select[multiple] option { 964 | padding: .1rem .2rem; 965 | } 966 | 967 | .form-select:not([multiple]):not([size]) { 968 | background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%204%205'%3E%3Cpath%20fill='%23667189'%20d='M2%200L0%202h4zm0%205L0%203h4z'/%3E%3C/svg%3E") no-repeat right .35rem center/.4rem .5rem; 969 | padding-right: 1.2rem; 970 | } 971 | 972 | .has-icon-left, 973 | .has-icon-right { 974 | position: relative; 975 | } 976 | 977 | .has-icon-left .form-icon, 978 | .has-icon-right .form-icon { 979 | height: .8rem; 980 | margin: 0 .25rem; 981 | position: absolute; 982 | top: 50%; 983 | transform: translateY(-50%); 984 | width: .8rem; 985 | z-index: 2; 986 | } 987 | 988 | .has-icon-left .form-icon { 989 | left: .05rem; 990 | } 991 | 992 | .has-icon-left .form-input { 993 | padding-left: 1.3rem; 994 | } 995 | 996 | .has-icon-right .form-icon { 997 | right: .05rem; 998 | } 999 | 1000 | .has-icon-right .form-input { 1001 | padding-right: 1.3rem; 1002 | } 1003 | 1004 | .form-checkbox, 1005 | .form-radio, 1006 | .form-switch { 1007 | display: block; 1008 | line-height: 1.2rem; 1009 | margin: .2rem 0; 1010 | min-height: 1.4rem; 1011 | padding: .1rem .4rem .1rem 1.2rem; 1012 | position: relative; 1013 | } 1014 | 1015 | .form-checkbox input, 1016 | .form-radio input, 1017 | .form-switch input { 1018 | clip: rect(0, 0, 0, 0); 1019 | height: 1px; 1020 | margin: -1px; 1021 | overflow: hidden; 1022 | position: absolute; 1023 | width: 1px; 1024 | } 1025 | 1026 | .form-checkbox input:focus + .form-icon, 1027 | .form-radio input:focus + .form-icon, 1028 | .form-switch input:focus + .form-icon { 1029 | border-color: #8a4d76; 1030 | box-shadow: 0 0 0 .1rem rgba(138, 77, 118, .2); 1031 | } 1032 | 1033 | .form-checkbox input:checked + .form-icon, 1034 | .form-radio input:checked + .form-icon, 1035 | .form-switch input:checked + .form-icon { 1036 | background: #8a4d76; 1037 | border-color: #8a4d76; 1038 | } 1039 | 1040 | .form-checkbox .form-icon, 1041 | .form-radio .form-icon, 1042 | .form-switch .form-icon { 1043 | border: .05rem solid #bcc3ce; 1044 | cursor: pointer; 1045 | display: inline-block; 1046 | position: absolute; 1047 | transition: background .2s, border .2s, box-shadow .2s, color .2s; 1048 | } 1049 | 1050 | .form-checkbox.input-sm, 1051 | .form-radio.input-sm, 1052 | .form-switch.input-sm { 1053 | font-size: .7rem; 1054 | margin: 0; 1055 | } 1056 | 1057 | .form-checkbox.input-lg, 1058 | .form-radio.input-lg, 1059 | .form-switch.input-lg { 1060 | font-size: .9rem; 1061 | margin: .3rem 0; 1062 | } 1063 | 1064 | .form-checkbox .form-icon, 1065 | .form-radio .form-icon { 1066 | background: #fff; 1067 | height: .8rem; 1068 | left: 0; 1069 | top: .3rem; 1070 | width: .8rem; 1071 | } 1072 | 1073 | .form-checkbox input:active + .form-icon, 1074 | .form-radio input:active + .form-icon { 1075 | background: #eef0f3; 1076 | } 1077 | 1078 | .form-checkbox .form-icon { 1079 | border-radius: .1rem; 1080 | } 1081 | 1082 | .form-checkbox input:checked + .form-icon::before { 1083 | background-clip: padding-box; 1084 | border: .1rem solid #fff; 1085 | border-left-width: 0; 1086 | border-top-width: 0; 1087 | content: ""; 1088 | height: 9px; 1089 | left: 50%; 1090 | margin-left: -3px; 1091 | margin-top: -6px; 1092 | position: absolute; 1093 | top: 50%; 1094 | transform: rotate(45deg); 1095 | width: 6px; 1096 | } 1097 | 1098 | .form-checkbox input:indeterminate + .form-icon { 1099 | background: #8a4d76; 1100 | border-color: #8a4d76; 1101 | } 1102 | 1103 | .form-checkbox input:indeterminate + .form-icon::before { 1104 | background: #fff; 1105 | content: ""; 1106 | height: 2px; 1107 | left: 50%; 1108 | margin-left: -5px; 1109 | margin-top: -1px; 1110 | position: absolute; 1111 | top: 50%; 1112 | width: 10px; 1113 | } 1114 | 1115 | .form-radio .form-icon { 1116 | border-radius: 50%; 1117 | } 1118 | 1119 | .form-radio input:checked + .form-icon::before { 1120 | background: #fff; 1121 | border-radius: 50%; 1122 | content: ""; 1123 | height: 6px; 1124 | left: 50%; 1125 | position: absolute; 1126 | top: 50%; 1127 | transform: translate(-50%, -50%); 1128 | width: 6px; 1129 | } 1130 | 1131 | .form-switch { 1132 | padding-left: 2rem; 1133 | } 1134 | 1135 | .form-switch .form-icon { 1136 | background: #bcc3ce; 1137 | background-clip: padding-box; 1138 | border-radius: .45rem; 1139 | height: .9rem; 1140 | left: 0; 1141 | top: .25rem; 1142 | width: 1.6rem; 1143 | } 1144 | 1145 | .form-switch .form-icon::before { 1146 | background: #fff; 1147 | border-radius: 50%; 1148 | content: ""; 1149 | display: block; 1150 | height: .8rem; 1151 | left: 0; 1152 | position: absolute; 1153 | top: 0; 1154 | transition: background .2s, border .2s, box-shadow .2s, color .2s, left .2s; 1155 | width: .8rem; 1156 | } 1157 | 1158 | .form-switch input:checked + .form-icon::before { 1159 | left: 14px; 1160 | } 1161 | 1162 | .form-switch input:active + .form-icon::before { 1163 | background: #f7f8f9; 1164 | } 1165 | 1166 | .input-group { 1167 | display: -ms-flexbox; 1168 | display: flex; 1169 | } 1170 | 1171 | .input-group .input-group-addon { 1172 | background: #f7f8f9; 1173 | border: .05rem solid #bcc3ce; 1174 | border-radius: .1rem; 1175 | line-height: 1.2rem; 1176 | padding: .25rem .4rem; 1177 | white-space: nowrap; 1178 | } 1179 | 1180 | .input-group .input-group-addon.addon-sm { 1181 | font-size: .7rem; 1182 | padding: .05rem .3rem; 1183 | } 1184 | 1185 | .input-group .input-group-addon.addon-lg { 1186 | font-size: .9rem; 1187 | padding: .35rem .6rem; 1188 | } 1189 | 1190 | .input-group .form-input, 1191 | .input-group .form-select { 1192 | -ms-flex: 1 1 auto; 1193 | flex: 1 1 auto; 1194 | width: 1%; 1195 | } 1196 | 1197 | .input-group .input-group-btn { 1198 | z-index: 1; 1199 | } 1200 | 1201 | .input-group .form-input:first-child:not(:last-child), 1202 | .input-group .form-select:first-child:not(:last-child), 1203 | .input-group .input-group-addon:first-child:not(:last-child), 1204 | .input-group .input-group-btn:first-child:not(:last-child) { 1205 | border-bottom-right-radius: 0; 1206 | border-top-right-radius: 0; 1207 | } 1208 | 1209 | .input-group .form-input:not(:first-child):not(:last-child), 1210 | .input-group .form-select:not(:first-child):not(:last-child), 1211 | .input-group .input-group-addon:not(:first-child):not(:last-child), 1212 | .input-group .input-group-btn:not(:first-child):not(:last-child) { 1213 | border-radius: 0; 1214 | margin-left: -.05rem; 1215 | } 1216 | 1217 | .input-group .form-input:last-child:not(:first-child), 1218 | .input-group .form-select:last-child:not(:first-child), 1219 | .input-group .input-group-addon:last-child:not(:first-child), 1220 | .input-group .input-group-btn:last-child:not(:first-child) { 1221 | border-bottom-left-radius: 0; 1222 | border-top-left-radius: 0; 1223 | margin-left: -.05rem; 1224 | } 1225 | 1226 | .input-group .form-input:focus, 1227 | .input-group .form-select:focus, 1228 | .input-group .input-group-addon:focus, 1229 | .input-group .input-group-btn:focus { 1230 | z-index: 2; 1231 | } 1232 | 1233 | .input-group .form-select { 1234 | width: auto; 1235 | } 1236 | 1237 | .input-group.input-inline { 1238 | display: -ms-inline-flexbox; 1239 | display: inline-flex; 1240 | } 1241 | 1242 | .has-success .form-input, 1243 | .form-input.is-success, 1244 | .has-success .form-select, 1245 | .form-select.is-success { 1246 | background: #f9fdfa; 1247 | border-color: #32b643; 1248 | } 1249 | 1250 | .has-success .form-input:focus, 1251 | .form-input.is-success:focus, 1252 | .has-success .form-select:focus, 1253 | .form-select.is-success:focus { 1254 | box-shadow: 0 0 0 .1rem rgba(50, 182, 67, .2); 1255 | } 1256 | 1257 | .has-error .form-input, 1258 | .form-input.is-error, 1259 | .has-error .form-select, 1260 | .form-select.is-error { 1261 | background: #fffaf7; 1262 | border-color: #e85600; 1263 | } 1264 | 1265 | .has-error .form-input:focus, 1266 | .form-input.is-error:focus, 1267 | .has-error .form-select:focus, 1268 | .form-select.is-error:focus { 1269 | box-shadow: 0 0 0 .1rem rgba(232, 86, 0, .2); 1270 | } 1271 | 1272 | .has-error .form-checkbox .form-icon, 1273 | .form-checkbox.is-error .form-icon, 1274 | .has-error .form-radio .form-icon, 1275 | .form-radio.is-error .form-icon, 1276 | .has-error .form-switch .form-icon, 1277 | .form-switch.is-error .form-icon { 1278 | border-color: #e85600; 1279 | } 1280 | 1281 | .has-error .form-checkbox input:checked + .form-icon, 1282 | .form-checkbox.is-error input:checked + .form-icon, 1283 | .has-error .form-radio input:checked + .form-icon, 1284 | .form-radio.is-error input:checked + .form-icon, 1285 | .has-error .form-switch input:checked + .form-icon, 1286 | .form-switch.is-error input:checked + .form-icon { 1287 | background: #e85600; 1288 | border-color: #e85600; 1289 | } 1290 | 1291 | .has-error .form-checkbox input:focus + .form-icon, 1292 | .form-checkbox.is-error input:focus + .form-icon, 1293 | .has-error .form-radio input:focus + .form-icon, 1294 | .form-radio.is-error input:focus + .form-icon, 1295 | .has-error .form-switch input:focus + .form-icon, 1296 | .form-switch.is-error input:focus + .form-icon { 1297 | border-color: #e85600; 1298 | box-shadow: 0 0 0 .1rem rgba(232, 86, 0, .2); 1299 | } 1300 | 1301 | .has-error .form-checkbox input:indeterminate + .form-icon, 1302 | .form-checkbox.is-error input:indeterminate + .form-icon { 1303 | background: #e85600; 1304 | border-color: #e85600; 1305 | } 1306 | 1307 | .form-input:not(:-ms-input-placeholder):invalid { 1308 | border-color: #e85600; 1309 | } 1310 | 1311 | .form-input:not(:placeholder-shown):invalid { 1312 | border-color: #e85600; 1313 | } 1314 | 1315 | .form-input:not(:-ms-input-placeholder):invalid:focus { 1316 | background: #fffaf7; 1317 | box-shadow: 0 0 0 .1rem rgba(232, 86, 0, .2); 1318 | } 1319 | 1320 | .form-input:not(:placeholder-shown):invalid:focus { 1321 | background: #fffaf7; 1322 | box-shadow: 0 0 0 .1rem rgba(232, 86, 0, .2); 1323 | } 1324 | 1325 | .form-input:not(:-ms-input-placeholder):invalid + .form-input-hint { 1326 | color: #e85600; 1327 | } 1328 | 1329 | .form-input:not(:placeholder-shown):invalid + .form-input-hint { 1330 | color: #e85600; 1331 | } 1332 | 1333 | .form-input:disabled, 1334 | .form-input.disabled, 1335 | .form-select:disabled, 1336 | .form-select.disabled { 1337 | background-color: #eef0f3; 1338 | cursor: not-allowed; 1339 | opacity: .5; 1340 | } 1341 | 1342 | .form-input[readonly] { 1343 | background-color: #f7f8f9; 1344 | } 1345 | 1346 | input:disabled + .form-icon, 1347 | input.disabled + .form-icon { 1348 | background: #eef0f3; 1349 | cursor: not-allowed; 1350 | opacity: .5; 1351 | } 1352 | 1353 | .form-switch input:disabled + .form-icon::before, 1354 | .form-switch input.disabled + .form-icon::before { 1355 | background: #fff; 1356 | } 1357 | 1358 | .form-horizontal { 1359 | padding: .4rem 0; 1360 | } 1361 | 1362 | .form-horizontal .form-group { 1363 | display: -ms-flexbox; 1364 | display: flex; 1365 | -ms-flex-wrap: wrap; 1366 | flex-wrap: wrap; 1367 | } 1368 | 1369 | .form-inline { 1370 | display: inline-block; 1371 | } 1372 | 1373 | .label { 1374 | background: #eef0f3; 1375 | border-radius: .1rem; 1376 | color: #455060; 1377 | display: inline-block; 1378 | line-height: 1.25; 1379 | padding: .1rem .2rem; 1380 | } 1381 | 1382 | .label.label-rounded { 1383 | border-radius: 5rem; 1384 | padding-left: .4rem; 1385 | padding-right: .4rem; 1386 | } 1387 | 1388 | .label.label-primary { 1389 | background: #8a4d76; 1390 | color: #fff; 1391 | } 1392 | 1393 | .label.label-secondary { 1394 | background: #dabcd0; 1395 | color: #8a4d76; 1396 | } 1397 | 1398 | .label.label-success { 1399 | background: #32b643; 1400 | color: #fff; 1401 | } 1402 | 1403 | .label.label-warning { 1404 | background: #ffb700; 1405 | color: #fff; 1406 | } 1407 | 1408 | .label.label-error { 1409 | background: #e85600; 1410 | color: #fff; 1411 | } 1412 | 1413 | code { 1414 | background: #fcf2f2; 1415 | border-radius: .1rem; 1416 | color: #d73e48; 1417 | font-size: 85%; 1418 | line-height: 1.25; 1419 | padding: .1rem .2rem; 1420 | } 1421 | 1422 | .code { 1423 | border-radius: .1rem; 1424 | color: #3b4351; 1425 | position: relative; 1426 | } 1427 | 1428 | .code::before { 1429 | color: #bcc3ce; 1430 | content: attr(data-lang); 1431 | font-size: .7rem; 1432 | position: absolute; 1433 | right: .4rem; 1434 | top: .1rem; 1435 | } 1436 | 1437 | .code code { 1438 | background: #f7f8f9; 1439 | color: inherit; 1440 | display: block; 1441 | line-height: 1.5; 1442 | overflow-x: auto; 1443 | padding: 1rem; 1444 | width: 100%; 1445 | } 1446 | 1447 | .container { 1448 | margin-left: auto; 1449 | margin-right: auto; 1450 | padding-left: .4rem; 1451 | padding-right: .4rem; 1452 | width: 100%; 1453 | } 1454 | 1455 | .container.grid-xl { 1456 | max-width: 1296px; 1457 | } 1458 | 1459 | .container.grid-lg { 1460 | max-width: 976px; 1461 | } 1462 | 1463 | .container.grid-md { 1464 | max-width: 856px; 1465 | } 1466 | 1467 | .container.grid-sm { 1468 | max-width: 616px; 1469 | } 1470 | 1471 | .container.grid-xs { 1472 | max-width: 496px; 1473 | } 1474 | 1475 | .show-xs, 1476 | .show-sm, 1477 | .show-md, 1478 | .show-lg, 1479 | .show-xl { 1480 | display: none !important; 1481 | } 1482 | 1483 | .cols, 1484 | .columns { 1485 | display: -ms-flexbox; 1486 | display: flex; 1487 | -ms-flex-wrap: wrap; 1488 | flex-wrap: wrap; 1489 | margin-left: -.4rem; 1490 | margin-right: -.4rem; 1491 | } 1492 | 1493 | .cols.col-gapless, 1494 | .columns.col-gapless { 1495 | margin-left: 0; 1496 | margin-right: 0; 1497 | } 1498 | 1499 | .cols.col-gapless > .column, 1500 | .columns.col-gapless > .column { 1501 | padding-left: 0; 1502 | padding-right: 0; 1503 | } 1504 | 1505 | .cols.col-oneline, 1506 | .columns.col-oneline { 1507 | -ms-flex-wrap: nowrap; 1508 | flex-wrap: nowrap; 1509 | overflow-x: auto; 1510 | } 1511 | 1512 | [class~="col-"], 1513 | .column { 1514 | -ms-flex: 1; 1515 | flex: 1; 1516 | max-width: 100%; 1517 | padding-left: .4rem; 1518 | padding-right: .4rem; 1519 | } 1520 | 1521 | [class~="col-"].col-12, 1522 | [class~="col-"].col-11, 1523 | [class~="col-"].col-10, 1524 | [class~="col-"].col-9, 1525 | [class~="col-"].col-8, 1526 | [class~="col-"].col-7, 1527 | [class~="col-"].col-6, 1528 | [class~="col-"].col-5, 1529 | [class~="col-"].col-4, 1530 | [class~="col-"].col-3, 1531 | [class~="col-"].col-2, 1532 | [class~="col-"].col-1, 1533 | [class~="col-"].col-auto, 1534 | .column.col-12, 1535 | .column.col-11, 1536 | .column.col-10, 1537 | .column.col-9, 1538 | .column.col-8, 1539 | .column.col-7, 1540 | .column.col-6, 1541 | .column.col-5, 1542 | .column.col-4, 1543 | .column.col-3, 1544 | .column.col-2, 1545 | .column.col-1, 1546 | .column.col-auto { 1547 | -ms-flex: none; 1548 | flex: none; 1549 | } 1550 | 1551 | .col-12 { 1552 | width: 100%; 1553 | } 1554 | 1555 | .col-11 { 1556 | width: 91.66666667%; 1557 | } 1558 | 1559 | .col-10 { 1560 | width: 83.33333333%; 1561 | } 1562 | 1563 | .col-9 { 1564 | width: 75%; 1565 | } 1566 | 1567 | .col-8 { 1568 | width: 66.66666667%; 1569 | } 1570 | 1571 | .col-7 { 1572 | width: 58.33333333%; 1573 | } 1574 | 1575 | .col-6 { 1576 | width: 50%; 1577 | } 1578 | 1579 | .col-5 { 1580 | width: 41.66666667%; 1581 | } 1582 | 1583 | .col-4 { 1584 | width: 33.33333333%; 1585 | } 1586 | 1587 | .col-3 { 1588 | width: 25%; 1589 | } 1590 | 1591 | .col-2 { 1592 | width: 16.66666667%; 1593 | } 1594 | 1595 | .col-1 { 1596 | width: 8.33333333%; 1597 | } 1598 | 1599 | .col-auto { 1600 | -ms-flex: 0 0 auto; 1601 | flex: 0 0 auto; 1602 | max-width: none; 1603 | width: auto; 1604 | } 1605 | 1606 | .col-mx-auto { 1607 | margin-left: auto; 1608 | margin-right: auto; 1609 | } 1610 | 1611 | .col-ml-auto { 1612 | margin-left: auto; 1613 | } 1614 | 1615 | .col-mr-auto { 1616 | margin-right: auto; 1617 | } 1618 | 1619 | @media (max-width: 1280px) { 1620 | .col-xl-12, 1621 | .col-xl-11, 1622 | .col-xl-10, 1623 | .col-xl-9, 1624 | .col-xl-8, 1625 | .col-xl-7, 1626 | .col-xl-6, 1627 | .col-xl-5, 1628 | .col-xl-4, 1629 | .col-xl-3, 1630 | .col-xl-2, 1631 | .col-xl-1, 1632 | .col-xl-auto { 1633 | -ms-flex: none; 1634 | flex: none; 1635 | } 1636 | .col-xl-12 { 1637 | width: 100%; 1638 | } 1639 | .col-xl-11 { 1640 | width: 91.66666667%; 1641 | } 1642 | .col-xl-10 { 1643 | width: 83.33333333%; 1644 | } 1645 | .col-xl-9 { 1646 | width: 75%; 1647 | } 1648 | .col-xl-8 { 1649 | width: 66.66666667%; 1650 | } 1651 | .col-xl-7 { 1652 | width: 58.33333333%; 1653 | } 1654 | .col-xl-6 { 1655 | width: 50%; 1656 | } 1657 | .col-xl-5 { 1658 | width: 41.66666667%; 1659 | } 1660 | .col-xl-4 { 1661 | width: 33.33333333%; 1662 | } 1663 | .col-xl-3 { 1664 | width: 25%; 1665 | } 1666 | .col-xl-2 { 1667 | width: 16.66666667%; 1668 | } 1669 | .col-xl-1 { 1670 | width: 8.33333333%; 1671 | } 1672 | .col-xl-auto { 1673 | width: auto; 1674 | } 1675 | .hide-xl { 1676 | display: none !important; 1677 | } 1678 | .show-xl { 1679 | display: block !important; 1680 | } 1681 | } 1682 | 1683 | @media (max-width: 960px) { 1684 | .col-lg-12, 1685 | .col-lg-11, 1686 | .col-lg-10, 1687 | .col-lg-9, 1688 | .col-lg-8, 1689 | .col-lg-7, 1690 | .col-lg-6, 1691 | .col-lg-5, 1692 | .col-lg-4, 1693 | .col-lg-3, 1694 | .col-lg-2, 1695 | .col-lg-1, 1696 | .col-lg-auto { 1697 | -ms-flex: none; 1698 | flex: none; 1699 | } 1700 | .col-lg-12 { 1701 | width: 100%; 1702 | } 1703 | .col-lg-11 { 1704 | width: 91.66666667%; 1705 | } 1706 | .col-lg-10 { 1707 | width: 83.33333333%; 1708 | } 1709 | .col-lg-9 { 1710 | width: 75%; 1711 | } 1712 | .col-lg-8 { 1713 | width: 66.66666667%; 1714 | } 1715 | .col-lg-7 { 1716 | width: 58.33333333%; 1717 | } 1718 | .col-lg-6 { 1719 | width: 50%; 1720 | } 1721 | .col-lg-5 { 1722 | width: 41.66666667%; 1723 | } 1724 | .col-lg-4 { 1725 | width: 33.33333333%; 1726 | } 1727 | .col-lg-3 { 1728 | width: 25%; 1729 | } 1730 | .col-lg-2 { 1731 | width: 16.66666667%; 1732 | } 1733 | .col-lg-1 { 1734 | width: 8.33333333%; 1735 | } 1736 | .col-lg-auto { 1737 | width: auto; 1738 | } 1739 | .hide-lg { 1740 | display: none !important; 1741 | } 1742 | .show-lg { 1743 | display: block !important; 1744 | } 1745 | } 1746 | 1747 | @media (max-width: 840px) { 1748 | .col-md-12, 1749 | .col-md-11, 1750 | .col-md-10, 1751 | .col-md-9, 1752 | .col-md-8, 1753 | .col-md-7, 1754 | .col-md-6, 1755 | .col-md-5, 1756 | .col-md-4, 1757 | .col-md-3, 1758 | .col-md-2, 1759 | .col-md-1, 1760 | .col-md-auto { 1761 | -ms-flex: none; 1762 | flex: none; 1763 | } 1764 | .col-md-12 { 1765 | width: 100%; 1766 | } 1767 | .col-md-11 { 1768 | width: 91.66666667%; 1769 | } 1770 | .col-md-10 { 1771 | width: 83.33333333%; 1772 | } 1773 | .col-md-9 { 1774 | width: 75%; 1775 | } 1776 | .col-md-8 { 1777 | width: 66.66666667%; 1778 | } 1779 | .col-md-7 { 1780 | width: 58.33333333%; 1781 | } 1782 | .col-md-6 { 1783 | width: 50%; 1784 | } 1785 | .col-md-5 { 1786 | width: 41.66666667%; 1787 | } 1788 | .col-md-4 { 1789 | width: 33.33333333%; 1790 | } 1791 | .col-md-3 { 1792 | width: 25%; 1793 | } 1794 | .col-md-2 { 1795 | width: 16.66666667%; 1796 | } 1797 | .col-md-1 { 1798 | width: 8.33333333%; 1799 | } 1800 | .col-md-auto { 1801 | width: auto; 1802 | } 1803 | .hide-md { 1804 | display: none !important; 1805 | } 1806 | .show-md { 1807 | display: block !important; 1808 | } 1809 | } 1810 | 1811 | @media (max-width: 600px) { 1812 | .col-sm-12, 1813 | .col-sm-11, 1814 | .col-sm-10, 1815 | .col-sm-9, 1816 | .col-sm-8, 1817 | .col-sm-7, 1818 | .col-sm-6, 1819 | .col-sm-5, 1820 | .col-sm-4, 1821 | .col-sm-3, 1822 | .col-sm-2, 1823 | .col-sm-1, 1824 | .col-sm-auto { 1825 | -ms-flex: none; 1826 | flex: none; 1827 | } 1828 | .col-sm-12 { 1829 | width: 100%; 1830 | } 1831 | .col-sm-11 { 1832 | width: 91.66666667%; 1833 | } 1834 | .col-sm-10 { 1835 | width: 83.33333333%; 1836 | } 1837 | .col-sm-9 { 1838 | width: 75%; 1839 | } 1840 | .col-sm-8 { 1841 | width: 66.66666667%; 1842 | } 1843 | .col-sm-7 { 1844 | width: 58.33333333%; 1845 | } 1846 | .col-sm-6 { 1847 | width: 50%; 1848 | } 1849 | .col-sm-5 { 1850 | width: 41.66666667%; 1851 | } 1852 | .col-sm-4 { 1853 | width: 33.33333333%; 1854 | } 1855 | .col-sm-3 { 1856 | width: 25%; 1857 | } 1858 | .col-sm-2 { 1859 | width: 16.66666667%; 1860 | } 1861 | .col-sm-1 { 1862 | width: 8.33333333%; 1863 | } 1864 | .col-sm-auto { 1865 | width: auto; 1866 | } 1867 | .hide-sm { 1868 | display: none !important; 1869 | } 1870 | .show-sm { 1871 | display: block !important; 1872 | } 1873 | } 1874 | 1875 | @media (max-width: 480px) { 1876 | .col-xs-12, 1877 | .col-xs-11, 1878 | .col-xs-10, 1879 | .col-xs-9, 1880 | .col-xs-8, 1881 | .col-xs-7, 1882 | .col-xs-6, 1883 | .col-xs-5, 1884 | .col-xs-4, 1885 | .col-xs-3, 1886 | .col-xs-2, 1887 | .col-xs-1, 1888 | .col-xs-auto { 1889 | -ms-flex: none; 1890 | flex: none; 1891 | } 1892 | .col-xs-12 { 1893 | width: 100%; 1894 | } 1895 | .col-xs-11 { 1896 | width: 91.66666667%; 1897 | } 1898 | .col-xs-10 { 1899 | width: 83.33333333%; 1900 | } 1901 | .col-xs-9 { 1902 | width: 75%; 1903 | } 1904 | .col-xs-8 { 1905 | width: 66.66666667%; 1906 | } 1907 | .col-xs-7 { 1908 | width: 58.33333333%; 1909 | } 1910 | .col-xs-6 { 1911 | width: 50%; 1912 | } 1913 | .col-xs-5 { 1914 | width: 41.66666667%; 1915 | } 1916 | .col-xs-4 { 1917 | width: 33.33333333%; 1918 | } 1919 | .col-xs-3 { 1920 | width: 25%; 1921 | } 1922 | .col-xs-2 { 1923 | width: 16.66666667%; 1924 | } 1925 | .col-xs-1 { 1926 | width: 8.33333333%; 1927 | } 1928 | .col-xs-auto { 1929 | width: auto; 1930 | } 1931 | .hide-xs { 1932 | display: none !important; 1933 | } 1934 | .show-xs { 1935 | display: block !important; 1936 | } 1937 | } 1938 | 1939 | .hero { 1940 | display: -ms-flexbox; 1941 | display: flex; 1942 | -ms-flex-direction: column; 1943 | flex-direction: column; 1944 | -ms-flex-pack: justify; 1945 | justify-content: space-between; 1946 | padding-bottom: 4rem; 1947 | padding-top: 4rem; 1948 | } 1949 | 1950 | .hero.hero-sm { 1951 | padding-bottom: 2rem; 1952 | padding-top: 2rem; 1953 | } 1954 | 1955 | .hero.hero-lg { 1956 | padding-bottom: 8rem; 1957 | padding-top: 8rem; 1958 | } 1959 | 1960 | .hero .hero-body { 1961 | padding: .4rem; 1962 | } 1963 | 1964 | .navbar { 1965 | align-items: stretch; 1966 | display: -ms-flexbox; 1967 | display: flex; 1968 | -ms-flex-align: stretch; 1969 | -ms-flex-pack: justify; 1970 | -ms-flex-wrap: wrap; 1971 | flex-wrap: wrap; 1972 | justify-content: space-between; 1973 | } 1974 | 1975 | .navbar .navbar-section { 1976 | align-items: center; 1977 | display: -ms-flexbox; 1978 | display: flex; 1979 | -ms-flex: 1 0 0; 1980 | flex: 1 0 0; 1981 | -ms-flex-align: center; 1982 | } 1983 | 1984 | .navbar .navbar-section:not(:first-child):last-child { 1985 | -ms-flex-pack: end; 1986 | justify-content: flex-end; 1987 | } 1988 | 1989 | .navbar .navbar-center { 1990 | align-items: center; 1991 | display: -ms-flexbox; 1992 | display: flex; 1993 | -ms-flex: 0 0 auto; 1994 | flex: 0 0 auto; 1995 | -ms-flex-align: center; 1996 | } 1997 | 1998 | .navbar .navbar-brand { 1999 | font-size: .9rem; 2000 | text-decoration: none; 2001 | } 2002 | 2003 | .badge { 2004 | position: relative; 2005 | white-space: nowrap; 2006 | } 2007 | 2008 | .badge[data-badge]::after, 2009 | .badge:not([data-badge])::after { 2010 | background: #8a4d76; 2011 | background-clip: padding-box; 2012 | border-radius: .5rem; 2013 | box-shadow: 0 0 0 .1rem #fff; 2014 | color: #fff; 2015 | content: attr(data-badge); 2016 | display: inline-block; 2017 | transform: translate(-.05rem, -.5rem); 2018 | } 2019 | 2020 | .badge[data-badge]::after { 2021 | font-size: .7rem; 2022 | height: .9rem; 2023 | line-height: 1; 2024 | min-width: .9rem; 2025 | padding: .1rem .2rem; 2026 | text-align: center; 2027 | white-space: nowrap; 2028 | } 2029 | 2030 | .badge:not([data-badge])::after, 2031 | .badge[data-badge=""]::after { 2032 | height: 6px; 2033 | min-width: 6px; 2034 | padding: 0; 2035 | width: 6px; 2036 | } 2037 | 2038 | .badge.btn::after { 2039 | position: absolute; 2040 | right: 0; 2041 | top: 0; 2042 | transform: translate(50%, -50%); 2043 | } 2044 | 2045 | .badge.avatar::after { 2046 | position: absolute; 2047 | right: 14.64%; 2048 | top: 14.64%; 2049 | transform: translate(50%, -50%); 2050 | z-index: 100; 2051 | } 2052 | 2053 | .card { 2054 | background: #fff; 2055 | border: .05rem solid #dadee4; 2056 | border-radius: .1rem; 2057 | display: -ms-flexbox; 2058 | display: flex; 2059 | -ms-flex-direction: column; 2060 | flex-direction: column; 2061 | } 2062 | 2063 | .card .card-header, 2064 | .card .card-body, 2065 | .card .card-footer { 2066 | padding: .8rem; 2067 | padding-bottom: 0; 2068 | } 2069 | 2070 | .card .card-header:last-child, 2071 | .card .card-body:last-child, 2072 | .card .card-footer:last-child { 2073 | padding-bottom: .8rem; 2074 | } 2075 | 2076 | .card .card-body { 2077 | -ms-flex: 1 1 auto; 2078 | flex: 1 1 auto; 2079 | } 2080 | 2081 | .card .card-image { 2082 | padding-top: .8rem; 2083 | } 2084 | 2085 | .card .card-image:first-child { 2086 | padding-top: 0; 2087 | } 2088 | 2089 | .card .card-image:first-child img { 2090 | border-top-left-radius: .1rem; 2091 | border-top-right-radius: .1rem; 2092 | } 2093 | 2094 | .card .card-image:last-child img { 2095 | border-bottom-left-radius: .1rem; 2096 | border-bottom-right-radius: .1rem; 2097 | } 2098 | 2099 | .chip { 2100 | align-items: center; 2101 | background: #eef0f3; 2102 | border-radius: 5rem; 2103 | display: -ms-inline-flexbox; 2104 | display: inline-flex; 2105 | -ms-flex-align: center; 2106 | font-size: 90%; 2107 | height: 1.2rem; 2108 | line-height: .8rem; 2109 | margin: .1rem; 2110 | max-width: 320px; 2111 | overflow: hidden; 2112 | padding: .2rem .4rem; 2113 | text-decoration: none; 2114 | text-overflow: ellipsis; 2115 | vertical-align: middle; 2116 | white-space: nowrap; 2117 | } 2118 | 2119 | .chip.active { 2120 | background: #8a4d76; 2121 | color: #fff; 2122 | } 2123 | 2124 | .chip .avatar { 2125 | margin-left: -.4rem; 2126 | margin-right: .2rem; 2127 | } 2128 | 2129 | .chip .btn-clear { 2130 | border-radius: 50%; 2131 | transform: scale(.75); 2132 | } 2133 | 2134 | .dropdown { 2135 | display: inline-block; 2136 | position: relative; 2137 | } 2138 | 2139 | .dropdown .menu { 2140 | animation: slide-down .15s ease 1; 2141 | display: none; 2142 | left: 0; 2143 | max-height: 50vh; 2144 | overflow-y: auto; 2145 | position: absolute; 2146 | top: 100%; 2147 | } 2148 | 2149 | .dropdown.dropdown-right .menu { 2150 | left: auto; 2151 | right: 0; 2152 | } 2153 | 2154 | .dropdown.active .menu, 2155 | .dropdown .dropdown-toggle:focus + .menu, 2156 | .dropdown .menu:hover { 2157 | display: block; 2158 | } 2159 | 2160 | .dropdown .btn-group .dropdown-toggle:nth-last-child(2) { 2161 | border-bottom-right-radius: .1rem; 2162 | border-top-right-radius: .1rem; 2163 | } 2164 | 2165 | .empty { 2166 | background: #f7f8f9; 2167 | border-radius: .1rem; 2168 | color: #66758c; 2169 | padding: 3.2rem 1.6rem; 2170 | text-align: center; 2171 | } 2172 | 2173 | .empty .empty-icon { 2174 | margin-bottom: .8rem; 2175 | } 2176 | 2177 | .empty .empty-title, 2178 | .empty .empty-subtitle { 2179 | margin: .4rem auto; 2180 | } 2181 | 2182 | .empty .empty-action { 2183 | margin-top: .8rem; 2184 | } 2185 | 2186 | .tab { 2187 | align-items: center; 2188 | border-bottom: .05rem solid #dadee4; 2189 | display: -ms-flexbox; 2190 | display: flex; 2191 | -ms-flex-align: center; 2192 | -ms-flex-wrap: wrap; 2193 | flex-wrap: wrap; 2194 | list-style: none; 2195 | margin: .2rem 0 .15rem 0; 2196 | } 2197 | 2198 | .tab .tab-item { 2199 | margin-top: 0; 2200 | } 2201 | 2202 | .tab .tab-item a { 2203 | border-bottom: .1rem solid transparent; 2204 | color: inherit; 2205 | display: block; 2206 | margin: 0 .4rem 0 0; 2207 | padding: .4rem .2rem .3rem .2rem; 2208 | text-decoration: none; 2209 | } 2210 | 2211 | .tab .tab-item a:focus, 2212 | .tab .tab-item a:hover { 2213 | color: #8a4d76; 2214 | } 2215 | 2216 | .tab .tab-item.active a, 2217 | .tab .tab-item a.active { 2218 | border-bottom-color: #8a4d76; 2219 | color: #8a4d76; 2220 | } 2221 | 2222 | .tab .tab-item.tab-action { 2223 | -ms-flex: 1 0 auto; 2224 | flex: 1 0 auto; 2225 | text-align: right; 2226 | } 2227 | 2228 | .tab .tab-item .btn-clear { 2229 | margin-top: -.2rem; 2230 | } 2231 | 2232 | .tab.tab-block .tab-item { 2233 | -ms-flex: 1 0 0; 2234 | flex: 1 0 0; 2235 | text-align: center; 2236 | } 2237 | 2238 | .tab.tab-block .tab-item a { 2239 | margin: 0; 2240 | } 2241 | 2242 | .tab.tab-block .tab-item .badge[data-badge]::after { 2243 | position: absolute; 2244 | right: .1rem; 2245 | top: .1rem; 2246 | transform: translate(0, 0); 2247 | } 2248 | 2249 | .tab:not(.tab-block) .badge { 2250 | padding-right: 0; 2251 | } 2252 | 2253 | .toast { 2254 | background: rgba(48, 55, 66, .95); 2255 | border: .05rem solid #303742; 2256 | border-color: #303742; 2257 | border-radius: .1rem; 2258 | color: #fff; 2259 | display: block; 2260 | padding: .4rem; 2261 | width: 100%; 2262 | } 2263 | 2264 | .toast.toast-primary { 2265 | background: rgba(138, 77, 118, .95); 2266 | border-color: #8a4d76; 2267 | } 2268 | 2269 | .toast.toast-success { 2270 | background: rgba(50, 182, 67, .95); 2271 | border-color: #32b643; 2272 | } 2273 | 2274 | .toast.toast-warning { 2275 | background: rgba(255, 183, 0, .95); 2276 | border-color: #ffb700; 2277 | } 2278 | 2279 | .toast.toast-error { 2280 | background: rgba(232, 86, 0, .95); 2281 | border-color: #e85600; 2282 | } 2283 | 2284 | .toast a { 2285 | color: #fff; 2286 | text-decoration: underline; 2287 | } 2288 | 2289 | .toast a:focus, 2290 | .toast a:hover, 2291 | .toast a:active, 2292 | .toast a.active { 2293 | opacity: .75; 2294 | } 2295 | 2296 | .toast .btn-clear { 2297 | margin: .1rem; 2298 | } 2299 | 2300 | .toast p:last-child { 2301 | margin-bottom: 0; 2302 | } 2303 | 2304 | .tooltip { 2305 | position: relative; 2306 | } 2307 | 2308 | .tooltip::after { 2309 | background: rgba(48, 55, 66, .95); 2310 | border-radius: .1rem; 2311 | bottom: 100%; 2312 | color: #fff; 2313 | content: attr(data-tooltip); 2314 | display: block; 2315 | font-size: .7rem; 2316 | left: 50%; 2317 | max-width: 320px; 2318 | opacity: 0; 2319 | overflow: hidden; 2320 | padding: .2rem .4rem; 2321 | pointer-events: none; 2322 | position: absolute; 2323 | text-overflow: ellipsis; 2324 | transform: translate(-50%, .4rem); 2325 | transition: opacity .2s, transform .2s; 2326 | white-space: pre; 2327 | z-index: 300; 2328 | } 2329 | 2330 | .tooltip:focus::after, 2331 | .tooltip:hover::after { 2332 | opacity: 1; 2333 | transform: translate(-50%, -.2rem); 2334 | } 2335 | 2336 | .tooltip[disabled], 2337 | .tooltip.disabled { 2338 | pointer-events: auto; 2339 | } 2340 | 2341 | .tooltip.tooltip-right::after { 2342 | bottom: 50%; 2343 | left: 100%; 2344 | transform: translate(-.2rem, 50%); 2345 | } 2346 | 2347 | .tooltip.tooltip-right:focus::after, 2348 | .tooltip.tooltip-right:hover::after { 2349 | transform: translate(.2rem, 50%); 2350 | } 2351 | 2352 | .tooltip.tooltip-bottom::after { 2353 | bottom: auto; 2354 | top: 100%; 2355 | transform: translate(-50%, -.4rem); 2356 | } 2357 | 2358 | .tooltip.tooltip-bottom:focus::after, 2359 | .tooltip.tooltip-bottom:hover::after { 2360 | transform: translate(-50%, .2rem); 2361 | } 2362 | 2363 | .tooltip.tooltip-left::after { 2364 | bottom: 50%; 2365 | left: auto; 2366 | right: 100%; 2367 | transform: translate(.4rem, 50%); 2368 | } 2369 | 2370 | .tooltip.tooltip-left:focus::after, 2371 | .tooltip.tooltip-left:hover::after { 2372 | transform: translate(-.2rem, 50%); 2373 | } 2374 | 2375 | @keyframes loading { 2376 | 0% { 2377 | transform: rotate(0deg); 2378 | } 2379 | 100% { 2380 | transform: rotate(360deg); 2381 | } 2382 | } 2383 | 2384 | @keyframes slide-down { 2385 | 0% { 2386 | opacity: 0; 2387 | transform: translateY(-1.6rem); 2388 | } 2389 | 100% { 2390 | opacity: 1; 2391 | transform: translateY(0); 2392 | } 2393 | } 2394 | 2395 | .text-primary { 2396 | color: #8a4d76 !important; 2397 | } 2398 | 2399 | a.text-primary:focus, 2400 | a.text-primary:hover { 2401 | color: #7a4468; 2402 | } 2403 | 2404 | a.text-primary:visited { 2405 | color: #9a5684; 2406 | } 2407 | 2408 | .text-secondary { 2409 | color: #d4b3c9 !important; 2410 | } 2411 | 2412 | a.text-secondary:focus, 2413 | a.text-secondary:hover { 2414 | color: #cba2be; 2415 | } 2416 | 2417 | a.text-secondary:visited { 2418 | color: #ddc3d5; 2419 | } 2420 | 2421 | .text-gray { 2422 | color: #bcc3ce !important; 2423 | } 2424 | 2425 | a.text-gray:focus, 2426 | a.text-gray:hover { 2427 | color: #adb6c4; 2428 | } 2429 | 2430 | a.text-gray:visited { 2431 | color: #cbd0d9; 2432 | } 2433 | 2434 | .text-light { 2435 | color: #fff !important; 2436 | } 2437 | 2438 | a.text-light:focus, 2439 | a.text-light:hover { 2440 | color: #f2f2f2; 2441 | } 2442 | 2443 | a.text-light:visited { 2444 | color: white; 2445 | } 2446 | 2447 | .text-dark { 2448 | color: #3b4351 !important; 2449 | } 2450 | 2451 | a.text-dark:focus, 2452 | a.text-dark:hover { 2453 | color: #303742; 2454 | } 2455 | 2456 | a.text-dark:visited { 2457 | color: #455060; 2458 | } 2459 | 2460 | .text-success { 2461 | color: #32b643 !important; 2462 | } 2463 | 2464 | a.text-success:focus, 2465 | a.text-success:hover { 2466 | color: #2da23c; 2467 | } 2468 | 2469 | a.text-success:visited { 2470 | color: #39c94b; 2471 | } 2472 | 2473 | .text-warning { 2474 | color: #ffb700 !important; 2475 | } 2476 | 2477 | a.text-warning:focus, 2478 | a.text-warning:hover { 2479 | color: #e6a500; 2480 | } 2481 | 2482 | a.text-warning:visited { 2483 | color: #ffbe1a; 2484 | } 2485 | 2486 | .text-error { 2487 | color: #e85600 !important; 2488 | } 2489 | 2490 | a.text-error:focus, 2491 | a.text-error:hover { 2492 | color: #cf4d00; 2493 | } 2494 | 2495 | a.text-error:visited { 2496 | color: #ff6003; 2497 | } 2498 | 2499 | .bg-primary { 2500 | background: #8a4d76 !important; 2501 | color: #fff; 2502 | } 2503 | 2504 | .bg-secondary { 2505 | background: #dabcd0 !important; 2506 | } 2507 | 2508 | .bg-dark { 2509 | background: #303742 !important; 2510 | color: #fff; 2511 | } 2512 | 2513 | .bg-gray { 2514 | background: #f7f8f9 !important; 2515 | } 2516 | 2517 | .bg-success { 2518 | background: #32b643 !important; 2519 | color: #fff; 2520 | } 2521 | 2522 | .bg-warning { 2523 | background: #ffb700 !important; 2524 | color: #fff; 2525 | } 2526 | 2527 | .bg-error { 2528 | background: #e85600 !important; 2529 | color: #fff; 2530 | } 2531 | 2532 | .c-hand { 2533 | cursor: pointer; 2534 | } 2535 | 2536 | .c-move { 2537 | cursor: move; 2538 | } 2539 | 2540 | .c-zoom-in { 2541 | cursor: zoom-in; 2542 | } 2543 | 2544 | .c-zoom-out { 2545 | cursor: zoom-out; 2546 | } 2547 | 2548 | .c-not-allowed { 2549 | cursor: not-allowed; 2550 | } 2551 | 2552 | .c-auto { 2553 | cursor: auto; 2554 | } 2555 | 2556 | .d-block { 2557 | display: block; 2558 | } 2559 | 2560 | .d-inline { 2561 | display: inline; 2562 | } 2563 | 2564 | .d-inline-block { 2565 | display: inline-block; 2566 | } 2567 | 2568 | .d-flex { 2569 | display: -ms-flexbox; 2570 | display: flex; 2571 | } 2572 | 2573 | .d-inline-flex { 2574 | display: -ms-inline-flexbox; 2575 | display: inline-flex; 2576 | } 2577 | 2578 | .d-none, 2579 | .d-hide { 2580 | display: none !important; 2581 | } 2582 | 2583 | .d-visible { 2584 | visibility: visible; 2585 | } 2586 | 2587 | .d-invisible { 2588 | visibility: hidden; 2589 | } 2590 | 2591 | .text-hide { 2592 | background: transparent; 2593 | border: 0; 2594 | color: transparent; 2595 | font-size: 0; 2596 | line-height: 0; 2597 | text-shadow: none; 2598 | } 2599 | 2600 | .text-assistive { 2601 | border: 0; 2602 | clip: rect(0, 0, 0, 0); 2603 | height: 1px; 2604 | margin: -1px; 2605 | overflow: hidden; 2606 | padding: 0; 2607 | position: absolute; 2608 | width: 1px; 2609 | } 2610 | 2611 | .divider, 2612 | .divider-vert { 2613 | display: block; 2614 | position: relative; 2615 | } 2616 | 2617 | .divider[data-content]::after, 2618 | .divider-vert[data-content]::after { 2619 | background: #fff; 2620 | color: #bcc3ce; 2621 | content: attr(data-content); 2622 | display: inline-block; 2623 | font-size: .7rem; 2624 | padding: 0 .4rem; 2625 | transform: translateY(-.65rem); 2626 | } 2627 | 2628 | .divider { 2629 | border-top: .05rem solid #f1f3f5; 2630 | height: .05rem; 2631 | margin: .4rem 0; 2632 | } 2633 | 2634 | .divider[data-content] { 2635 | margin: .8rem 0; 2636 | } 2637 | 2638 | .divider-vert { 2639 | display: block; 2640 | padding: .8rem; 2641 | } 2642 | 2643 | .divider-vert::before { 2644 | border-left: .05rem solid #dadee4; 2645 | bottom: .4rem; 2646 | content: ""; 2647 | display: block; 2648 | left: 50%; 2649 | position: absolute; 2650 | top: .4rem; 2651 | transform: translateX(-50%); 2652 | } 2653 | 2654 | .divider-vert[data-content]::after { 2655 | left: 50%; 2656 | padding: .2rem 0; 2657 | position: absolute; 2658 | top: 50%; 2659 | transform: translate(-50%, -50%); 2660 | } 2661 | 2662 | .loading { 2663 | color: transparent !important; 2664 | min-height: .8rem; 2665 | pointer-events: none; 2666 | position: relative; 2667 | } 2668 | 2669 | .loading::after { 2670 | animation: loading 500ms infinite linear; 2671 | background: transparent; 2672 | border: .1rem solid #8a4d76; 2673 | border-radius: 50%; 2674 | border-right-color: transparent; 2675 | border-top-color: transparent; 2676 | content: ""; 2677 | display: block; 2678 | height: .8rem; 2679 | left: 50%; 2680 | margin-left: -.4rem; 2681 | margin-top: -.4rem; 2682 | opacity: 1; 2683 | padding: 0; 2684 | position: absolute; 2685 | top: 50%; 2686 | width: .8rem; 2687 | z-index: 1; 2688 | } 2689 | 2690 | .loading.loading-lg { 2691 | min-height: 2rem; 2692 | } 2693 | 2694 | .loading.loading-lg::after { 2695 | height: 1.6rem; 2696 | margin-left: -.8rem; 2697 | margin-top: -.8rem; 2698 | width: 1.6rem; 2699 | } 2700 | 2701 | .clearfix::after { 2702 | clear: both; 2703 | content: ""; 2704 | display: table; 2705 | } 2706 | 2707 | .float-left { 2708 | float: left !important; 2709 | } 2710 | 2711 | .float-right { 2712 | float: right !important; 2713 | } 2714 | 2715 | .p-relative { 2716 | position: relative !important; 2717 | } 2718 | 2719 | .p-absolute { 2720 | position: absolute !important; 2721 | } 2722 | 2723 | .p-fixed { 2724 | position: fixed !important; 2725 | } 2726 | 2727 | .p-sticky { 2728 | position: -webkit-sticky !important; 2729 | position: sticky !important; 2730 | } 2731 | 2732 | .p-centered { 2733 | display: block; 2734 | float: none; 2735 | margin-left: auto; 2736 | margin-right: auto; 2737 | } 2738 | 2739 | .flex-centered { 2740 | align-items: center; 2741 | display: -ms-flexbox; 2742 | display: flex; 2743 | -ms-flex-align: center; 2744 | -ms-flex-pack: center; 2745 | justify-content: center; 2746 | } 2747 | 2748 | .m-0 { 2749 | margin: 0 !important; 2750 | } 2751 | 2752 | .mb-0 { 2753 | margin-bottom: 0 !important; 2754 | } 2755 | 2756 | .ml-0 { 2757 | margin-left: 0 !important; 2758 | } 2759 | 2760 | .mr-0 { 2761 | margin-right: 0 !important; 2762 | } 2763 | 2764 | .mt-0 { 2765 | margin-top: 0 !important; 2766 | } 2767 | 2768 | .mx-0 { 2769 | margin-left: 0 !important; 2770 | margin-right: 0 !important; 2771 | } 2772 | 2773 | .my-0 { 2774 | margin-bottom: 0 !important; 2775 | margin-top: 0 !important; 2776 | } 2777 | 2778 | .m-1 { 2779 | margin: .2rem !important; 2780 | } 2781 | 2782 | .mb-1 { 2783 | margin-bottom: .2rem !important; 2784 | } 2785 | 2786 | .ml-1 { 2787 | margin-left: .2rem !important; 2788 | } 2789 | 2790 | .mr-1 { 2791 | margin-right: .2rem !important; 2792 | } 2793 | 2794 | .mt-1 { 2795 | margin-top: .2rem !important; 2796 | } 2797 | 2798 | .mx-1 { 2799 | margin-left: .2rem !important; 2800 | margin-right: .2rem !important; 2801 | } 2802 | 2803 | .my-1 { 2804 | margin-bottom: .2rem !important; 2805 | margin-top: .2rem !important; 2806 | } 2807 | 2808 | .m-2 { 2809 | margin: .4rem !important; 2810 | } 2811 | 2812 | .mb-2 { 2813 | margin-bottom: .4rem !important; 2814 | } 2815 | 2816 | .ml-2 { 2817 | margin-left: .4rem !important; 2818 | } 2819 | 2820 | .mr-2 { 2821 | margin-right: .4rem !important; 2822 | } 2823 | 2824 | .mt-2 { 2825 | margin-top: .4rem !important; 2826 | } 2827 | 2828 | .mx-2 { 2829 | margin-left: .4rem !important; 2830 | margin-right: .4rem !important; 2831 | } 2832 | 2833 | .my-2 { 2834 | margin-bottom: .4rem !important; 2835 | margin-top: .4rem !important; 2836 | } 2837 | 2838 | .p-0 { 2839 | padding: 0 !important; 2840 | } 2841 | 2842 | .pb-0 { 2843 | padding-bottom: 0 !important; 2844 | } 2845 | 2846 | .pl-0 { 2847 | padding-left: 0 !important; 2848 | } 2849 | 2850 | .pr-0 { 2851 | padding-right: 0 !important; 2852 | } 2853 | 2854 | .pt-0 { 2855 | padding-top: 0 !important; 2856 | } 2857 | 2858 | .px-0 { 2859 | padding-left: 0 !important; 2860 | padding-right: 0 !important; 2861 | } 2862 | 2863 | .py-0 { 2864 | padding-bottom: 0 !important; 2865 | padding-top: 0 !important; 2866 | } 2867 | 2868 | .p-1 { 2869 | padding: .2rem !important; 2870 | } 2871 | 2872 | .pb-1 { 2873 | padding-bottom: .2rem !important; 2874 | } 2875 | 2876 | .pl-1 { 2877 | padding-left: .2rem !important; 2878 | } 2879 | 2880 | .pr-1 { 2881 | padding-right: .2rem !important; 2882 | } 2883 | 2884 | .pt-1 { 2885 | padding-top: .2rem !important; 2886 | } 2887 | 2888 | .px-1 { 2889 | padding-left: .2rem !important; 2890 | padding-right: .2rem !important; 2891 | } 2892 | 2893 | .py-1 { 2894 | padding-bottom: .2rem !important; 2895 | padding-top: .2rem !important; 2896 | } 2897 | 2898 | .p-2 { 2899 | padding: .4rem !important; 2900 | } 2901 | 2902 | .pb-2 { 2903 | padding-bottom: .4rem !important; 2904 | } 2905 | 2906 | .pl-2 { 2907 | padding-left: .4rem !important; 2908 | } 2909 | 2910 | .pr-2 { 2911 | padding-right: .4rem !important; 2912 | } 2913 | 2914 | .pt-2 { 2915 | padding-top: .4rem !important; 2916 | } 2917 | 2918 | .px-2 { 2919 | padding-left: .4rem !important; 2920 | padding-right: .4rem !important; 2921 | } 2922 | 2923 | .py-2 { 2924 | padding-bottom: .4rem !important; 2925 | padding-top: .4rem !important; 2926 | } 2927 | 2928 | .s-rounded { 2929 | border-radius: .1rem; 2930 | } 2931 | 2932 | .s-circle { 2933 | border-radius: 50%; 2934 | } 2935 | 2936 | .text-left { 2937 | text-align: left; 2938 | } 2939 | 2940 | .text-right { 2941 | text-align: right; 2942 | } 2943 | 2944 | .text-center { 2945 | text-align: center; 2946 | } 2947 | 2948 | .text-justify { 2949 | text-align: justify; 2950 | } 2951 | 2952 | .text-lowercase { 2953 | text-transform: lowercase; 2954 | } 2955 | 2956 | .text-uppercase { 2957 | text-transform: uppercase; 2958 | } 2959 | 2960 | .text-capitalize { 2961 | text-transform: capitalize; 2962 | } 2963 | 2964 | .text-normal { 2965 | font-weight: normal; 2966 | } 2967 | 2968 | .text-bold { 2969 | font-weight: bold; 2970 | } 2971 | 2972 | .text-italic { 2973 | font-style: italic; 2974 | } 2975 | 2976 | .text-large { 2977 | font-size: 1.2em; 2978 | } 2979 | 2980 | .text-small { 2981 | font-size: .9em; 2982 | } 2983 | 2984 | .text-tiny { 2985 | font-size: .8em; 2986 | } 2987 | 2988 | .text-muted { 2989 | opacity: .8; 2990 | } 2991 | 2992 | .text-ellipsis { 2993 | overflow: hidden; 2994 | text-overflow: ellipsis; 2995 | white-space: nowrap; 2996 | } 2997 | 2998 | .text-clip { 2999 | overflow: hidden; 3000 | text-overflow: clip; 3001 | white-space: nowrap; 3002 | } 3003 | 3004 | .text-break { 3005 | -webkit-hyphens: auto; 3006 | -ms-hyphens: auto; 3007 | hyphens: auto; 3008 | word-break: break-word; 3009 | word-wrap: break-word; 3010 | } -------------------------------------------------------------------------------- /allText/typesOfText.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("Type of text", async ({ page }) => { 4 | await page.goto("http://127.0.0.1:5500/allText/index.html"); 5 | await test.step("Inner HTML", async () => { 6 | await page.click("#innerHTML"); 7 | let txt = await page.locator("#attach").innerHTML(); 8 | console.log('Inner HTML: ' + txt); 9 | }); 10 | await test.step("Inner Text", async () => { 11 | await page.click("#innerHTML"); 12 | let txt = await page.locator("#attach").innerText(); 13 | console.log('Inner Text: ' + txt); 14 | }); 15 | await test.step("Text Content", async () => { 16 | await page.click("#innerHTML"); 17 | let txt = await page.locator("#attach").textContent(); 18 | console.log('Text Content: ' + txt); 19 | }) 20 | }) -------------------------------------------------------------------------------- /android/mobile.test.ts: -------------------------------------------------------------------------------- 1 | import { _android as android, } from 'playwright'; 2 | import { test } from "@playwright/test"; 3 | 4 | test("Run in Android - Chrome", async () => { 5 | // Connect to the device. 6 | const [device] = await android.devices(); 7 | console.log(`Model: ${device.model()}`); 8 | console.log(`Serial: ${device.serial()}`); 9 | // Take screenshot of the device. 10 | await device.screenshot({ path: 'device.png' }); 11 | 12 | // Launch Chrome browser. 13 | await device.shell('am force-stop com.android.chrome'); 14 | const context = await device.launchBrowser(); 15 | 16 | // Use BrowserContext as usual. 17 | const page = await context.newPage(); 18 | await page.goto('https://letcode.in'); 19 | console.log(await page.evaluate(() => window.location.href)); 20 | await page.screenshot({ path: 'page.png' }); 21 | 22 | await page.click("a[role='button']"); 23 | await page.click("text=Log in") 24 | // Click input[name="email"] 25 | await page.click('input[name="email"]'); 26 | // Fill input[name="email"] 27 | await page.fill('input[name="email"]', 'koushik350@gmail.com'); 28 | // Press Tab 29 | await page.press('input[name="email"]', 'Tab'); 30 | // Fill input[name="password"] 31 | await page.fill('input[name="password"]', 'Pass123$'); 32 | // Click //button[normalize-space(.)='LOGIN'] 33 | await Promise.all([ 34 | page.waitForNavigation({ url: 'https://letcode.in/' }), 35 | page.click('//button[normalize-space(.)=\'LOGIN\']') 36 | ]); 37 | await page.click("a[role='button']"); 38 | await page.click("text=Sign out"); 39 | 40 | // close context and device 41 | await context.close(); 42 | await device.close(); 43 | }) 44 | -------------------------------------------------------------------------------- /annotation/annotation.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | // test.only("first test", async ({ page }) => { 4 | // // test.fail(); 5 | // test.slow() 6 | // // test.setTimeout(10000) 7 | // console.log("first test running"); 8 | // await page.goto("https://letcode.in") 9 | // // expect(await page.title()).toBe("LetCode with") 10 | // console.log("first test completed"); 11 | // }); 12 | test("second test", async ({ page }) => { 13 | console.log("second test running"); 14 | await page.goto("https://playwright.dev") 15 | console.log("second test completed"); 16 | }); 17 | test("third test", async ({ page, browserName }) => { 18 | console.log("Name: " + browserName); 19 | // test.fixme() 20 | // if (browserName === "firefox") { 21 | // test.skip() 22 | // } 23 | 24 | console.log("third test running"); 25 | await page.goto("https://letcode.in") 26 | console.log("third test completed"); 27 | // wrong 28 | }); -------------------------------------------------------------------------------- /apitest/service-now.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | let _number: number; 4 | let _sys_id: string; 5 | const short_description = "required ms office 365" 6 | // Create 7 | test("Create an Incident", async ({ request, baseURL }) => { 8 | const _response = await request.post(`${baseURL}`, { 9 | data: { 10 | "short_description": short_description, 11 | "category": "hardware" 12 | }, headers: { 13 | "Accept": "application/json" 14 | } 15 | }); 16 | expect(_response.status()).toBe(201); 17 | expect(_response.ok()).toBeTruthy(); 18 | console.log(await _response.json()); 19 | const res = await _response.json(); 20 | _number = res.result.task_effective_number; 21 | _sys_id = res.result.sys_id; 22 | 23 | // output as xml 24 | // console.log((await _response.body()).toString()); 25 | }) 26 | // test("", async ({ page }) => { 27 | // await page.request.get("") 28 | // }) 29 | // Get 30 | test("Get an Incident", async ({ request, baseURL }) => { 31 | const _response = await request.get(`${baseURL}`, { 32 | params: { 33 | task_effective_number: _number, 34 | sysparm_fields: "short_description, category" 35 | } 36 | }); 37 | console.log(await _response.json()); 38 | expect(_response.status()).toBe(200); 39 | expect(await _response.json()).toMatchObject({ 40 | result: [{ short_description: short_description, category: 'hardware' }] 41 | }) 42 | 43 | }) 44 | 45 | // Uppdate 46 | test("Put(Modify) an Incident", async ({ request, baseURL }) => { 47 | const _response = await request.put(`${baseURL}/${_sys_id}`, { 48 | data: { 49 | "short_description": "Very boring tutorial", 50 | "category": "Software" 51 | } 52 | }); 53 | console.log(await _response.json()); 54 | expect(_response.status()).toBe(200); 55 | expect(_response.ok()).toBeTruthy(); 56 | }) 57 | 58 | // Delete 59 | test("Delete an Incident", async ({ request, baseURL }) => { 60 | const _response = await request.delete(`${baseURL}/${_sys_id}`); 61 | // console.log(await _response.json()); 62 | expect(_response.status()).toBe(204); 63 | expect(_response.ok()).toBeTruthy(); 64 | }) 65 | -------------------------------------------------------------------------------- /azure-pipelines-1.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - main 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '10.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install 20 | npm run test 21 | displayName: 'npm install and test' 22 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - main 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '14.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install 20 | npm run test 21 | displayName: 'Executing tests' 22 | -------------------------------------------------------------------------------- /baseUrlDemo/baseUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | 4 | test("Goto Sign up page", async ({ page }) => { 5 | 6 | await page.goto("/signup"); 7 | expect(page.url()).toBe("https://letcode.in/signup") 8 | 9 | }); 10 | 11 | test("Goto Login up page", async ({ page }) => { 12 | await page.goto("/signin"); 13 | expect(page.url()).toBe("https://letcode.in/signin") 14 | }); 15 | 16 | test("Goto workspace up page", async ({ page }) => { 17 | await page.goto("/test"); 18 | expect(page.url()).toBe("https://letcode.in/test") 19 | }); 20 | -------------------------------------------------------------------------------- /baseUrlDemo/baseUrl.test2.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | // local config 4 | 5 | test.use({ 6 | baseURL: "https://play.letcode.in/" 7 | }) 8 | 9 | test("Goto Sign up page", async ({ page }) => { 10 | await page.goto("/edit"); 11 | expect(page.url()).toBe("https://play.letcode.in/edit") 12 | 13 | }); 14 | 15 | // test("Goto Login up page", async ({ page }) => { 16 | // await page.goto("/signin"); 17 | // expect(page.url()).toBe("https://letcode.in/signin") 18 | // }); 19 | 20 | // test("Goto workspace up page", async ({ page }) => { 21 | // await page.goto("/test"); 22 | // expect(page.url()).toBe("https://letcode.in/test") 23 | // }); 24 | -------------------------------------------------------------------------------- /booksAPI.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "bookId": 2, 4 | "title": "HP2", 5 | "author": "JKR", 6 | "category": "Mystery", 7 | "price": 234, 8 | "coverFileName": "9d8f4978-0ef8-42d0-873a-4eb583439237HP2.jpg" 9 | }, 10 | { 11 | "bookId": 3, 12 | "title": "HP3", 13 | "author": "JKR", 14 | "category": "Romance", 15 | "price": 213, 16 | "coverFileName": "c63ade52-3f90-41fa-980a-1136b6ad2128HP3.jpg" 17 | }, 18 | { 19 | "bookId": 4, 20 | "title": "HP4", 21 | "author": "JKR", 22 | "category": "Fiction", 23 | "price": 321, 24 | "coverFileName": "9d31690d-3b1d-4faa-a1d2-3a680a935008HP4.jpg" 25 | }, 26 | { 27 | "bookId": 5, 28 | "title": "HP5", 29 | "author": "JKR", 30 | "category": "Mystery", 31 | "price": 432, 32 | "coverFileName": "8f5f8b64-794c-4dbb-b369-6cc9cc762ce0HP5.jpg" 33 | }, 34 | { 35 | "bookId": 6, 36 | "title": "HP6", 37 | "author": "JKR", 38 | "category": "Fantasy", 39 | "price": 432, 40 | "coverFileName": "cff3d5ee-71f3-43d8-8625-33abcd48659eHP6.jpg" 41 | }, 42 | { 43 | "bookId": 7, 44 | "title": "HP7", 45 | "author": "JKR", 46 | "category": "Fiction", 47 | "price": 325, 48 | "coverFileName": "4ec2ffb6-b21a-43ce-bf90-04d56ec72644HP7.jpg" 49 | }, 50 | { 51 | "bookId": 15, 52 | "title": "HP1", 53 | "author": "JKR", 54 | "category": "Fantasy", 55 | "price": 342, 56 | "coverFileName": "f73b4e21-28fe-4d3f-895b-d8a553b7ba6fHP1.jpg" 57 | }, 58 | { 59 | "bookId": 18, 60 | "title": "Rot & Ruin", 61 | "author": "Jonathan Maberry ", 62 | "category": "Biography", 63 | "price": 123, 64 | "coverFileName": "eb592aa9-831e-47ed-aea8-fa79d7d316097157310.jpg" 65 | }, 66 | { 67 | "bookId": 21, 68 | "title": "Slayer", 69 | "author": " Kiersten White", 70 | "category": "Mystery", 71 | "price": 1234, 72 | "coverFileName": "6d91b7b0-b8d1-41ad-a0ef-65e2324fc1b3Slayer.jpg" 73 | }, 74 | { 75 | "bookId": 28, 76 | "title": "Çürük", 77 | "author": "Jonathan ", 78 | "category": "Fiction", 79 | "price": 1211, 80 | "coverFileName": "cbf832c1-cd01-47b3-93fa-62995a7dca1118799023.jpg" 81 | }, 82 | { 83 | "bookId": 29, 84 | "title": "Roomies", 85 | "author": "Christina Lauren ", 86 | "category": "Biography", 87 | "price": 334, 88 | "coverFileName": "267e7cea-d66e-4e00-a220-c0ee7e70fdaf33322.jpg" 89 | }, 90 | { 91 | "bookId": 30, 92 | "title": "A Princess in Theory", 93 | "author": "Alyssa Cole", 94 | "category": "Fiction", 95 | "price": 545, 96 | "coverFileName": "42314d38-bbc8-40ed-b890-612ea45791d735271238.jpg" 97 | }, 98 | { 99 | "bookId": 31, 100 | "title": "Robbie", 101 | "author": "Ella Frank", 102 | "category": "Fiction", 103 | "price": 345, 104 | "coverFileName": "2141f7a1-e6b8-4d88-91e3-0d3a141117be39086215.jpg" 105 | }, 106 | { 107 | "bookId": 32, 108 | "title": "Wicked and the Wallflower", 109 | "author": "Sarah MacLean", 110 | "category": "Fiction", 111 | "price": 512, 112 | "coverFileName": "75f8ca57-6b59-43aa-ae05-f51a7429dd5836301036.jpg" 113 | }, 114 | { 115 | "bookId": 33, 116 | "title": "Dr. Strange Beard", 117 | "author": "Penny Reid", 118 | "category": "Biography", 119 | "price": 312, 120 | "coverFileName": "9749b4d2-d8be-4e9b-b7ea-d437a3a9bf0e136395874.jpg" 121 | }, 122 | { 123 | "bookId": 34, 124 | "title": "The Simple Wild", 125 | "author": "K.A. Tucker", 126 | "category": "Biography", 127 | "price": 111, 128 | "coverFileName": "b868eb26-f12c-4dcf-ba19-03e0d6cafb8d36373564.jpg" 129 | }, 130 | { 131 | "bookId": 35, 132 | "title": "Marriage of Inconvenience", 133 | "author": "Penny Reid", 134 | "category": "Fantasy", 135 | "price": 345, 136 | "coverFileName": "8d3a0352-270b-405d-b8bc-ee0f59179cbf36395764.jpg" 137 | }, 138 | { 139 | "bookId": 36, 140 | "title": "The Hookup", 141 | "author": "Kristen Ashley ", 142 | "category": "Romance", 143 | "price": 434, 144 | "coverFileName": "dc785e9f-a753-42f7-a87e-fda5e97ce67bqq.jpg" 145 | }, 146 | { 147 | "bookId": 37, 148 | "title": "Birthday Girl", 149 | "author": "Penelope Douglas ", 150 | "category": "Romance", 151 | "price": 441, 152 | "coverFileName": "7616f5e9-a42a-4268-8ff0-4f77547dcc9crr.jpg" 153 | }, 154 | { 155 | "bookId": 38, 156 | "title": "Red Rising", 157 | "author": "Pierce Brown", 158 | "category": "Romance", 159 | "price": 512, 160 | "coverFileName": "24cd89a7-737f-490e-8c8e-cbeac7eac054tt.jpg" 161 | }, 162 | { 163 | "bookId": 39, 164 | "title": "Before We Were Yours", 165 | "author": "Lisa Wingate ", 166 | "category": "Romance", 167 | "price": 123, 168 | "coverFileName": "15396272-e3ec-4b1c-97d0-d2e841898f55aa.jpg" 169 | }, 170 | { 171 | "bookId": 40, 172 | "title": "A Dance with Dragons", 173 | "author": "George R.R. Martin", 174 | "category": "Fantasy", 175 | "price": 412, 176 | "coverFileName": "63df0288-6589-4e4c-8a68-def0bbf72714qa.jpg" 177 | }, 178 | { 179 | "bookId": 41, 180 | "title": "The Hate U Give", 181 | "author": "Angie Thomas", 182 | "category": "Biography", 183 | "price": 122, 184 | "coverFileName": "3d894fa1-8746-4443-b244-99624cc39f1fq1we.jpg" 185 | }, 186 | { 187 | "bookId": 42, 188 | "title": "All the Light We Cannot See", 189 | "author": "Anthony Doerr ", 190 | "category": "Romance", 191 | "price": 542, 192 | "coverFileName": "e49d1ff8-3bad-49fc-b1f1-496fc7fef07aq2.jpg" 193 | }, 194 | { 195 | "bookId": 43, 196 | "title": "The Help", 197 | "author": "Kathryn Stockett", 198 | "category": "Biography", 199 | "price": 124, 200 | "coverFileName": "921ff624-bcdb-4b48-8e5b-c3f34078bd11q3.jpg" 201 | }, 202 | { 203 | "bookId": 44, 204 | "title": "A Court of Mist and Fury", 205 | "author": "Sarah J. Maas ", 206 | "category": "Romance", 207 | "price": 645, 208 | "coverFileName": "7996d691-beb6-4c41-82f0-4a3f23641db626073150.jpg" 209 | }, 210 | { 211 | "bookId": 45, 212 | "title": "The Martian", 213 | "author": "Andy Weir", 214 | "category": "Fantasy", 215 | "price": 321, 216 | "coverFileName": "5b7162d6-2780-461b-be6f-e4debac083ad18007564.jpg" 217 | }, 218 | { 219 | "bookId": 46, 220 | "title": "Catching Fire", 221 | "author": "Suzanne Collins", 222 | "category": "Mystery", 223 | "price": 658, 224 | "coverFileName": "6df4a30a-e8a1-467d-9da6-d826989a9c606148028.jpg" 225 | }, 226 | { 227 | "bookId": 47, 228 | "title": "The Fault in Our Stars", 229 | "author": " John Green ", 230 | "category": "Mystery", 231 | "price": 346, 232 | "coverFileName": "a99a994c-b5d5-4f17-807f-33bcd1003505q3333.jpg" 233 | }, 234 | { 235 | "bookId": 48, 236 | "title": "11/22/63", 237 | "author": "Stephen King", 238 | "category": "Mystery", 239 | "price": 556, 240 | "coverFileName": "ab486162-5053-46ec-a89a-fdf2b686ff6310644930.jpg" 241 | }, 242 | { 243 | "bookId": 72, 244 | "title": "City of Girls", 245 | "author": "Elizabeth Gilbert ", 246 | "category": "Fiction", 247 | "price": 1222, 248 | "coverFileName": "e94cf1e2-a7bf-47e1-aed8-b8931aeee6b342135029.jpg" 249 | }, 250 | { 251 | "bookId": 73, 252 | "title": "Mrs. Everything", 253 | "author": "Jennifer Weiner ", 254 | "category": "Fiction", 255 | "price": 4444, 256 | "coverFileName": "07492ea6-09fb-46e9-8c9b-8a8f51afe989qq.jpg" 257 | }, 258 | { 259 | "bookId": 74, 260 | "title": "Magic for Liars", 261 | "author": "Sarah Gailey ", 262 | "category": "Fiction", 263 | "price": 777, 264 | "coverFileName": "7a904907-be5d-42c3-af15-6b770e5bd0d0qq.jpg" 265 | }, 266 | { 267 | "bookId": 75, 268 | "title": "Fix Her Up", 269 | "author": "Tessa Bailey ", 270 | "category": "Romance", 271 | "price": 3333, 272 | "coverFileName": "1561f3e0-c461-4273-864c-9e67c620a490qq.jpg" 273 | }, 274 | { 275 | "bookId": 76, 276 | "title": "Like a Love Story", 277 | "author": "Abdi Nazemian ", 278 | "category": "Romance", 279 | "price": 6666, 280 | "coverFileName": "bb4382d6-4c3f-462f-bbbe-a7f88afc1436qq.jpg" 281 | }, 282 | { 283 | "bookId": 77, 284 | "title": "The Beholder", 285 | "author": "Anna Bright ", 286 | "category": "Romance", 287 | "price": 3333, 288 | "coverFileName": "5ba1968d-a7f5-4a04-99c2-281088b8532fqq.jpg" 289 | }, 290 | { 291 | "bookId": 78, 292 | "title": "The Last Pirate of New York: A Ghost Ship, a Killer, and the Birth of a Gangster Nation", 293 | "author": "Rich Cohen", 294 | "category": "Biography", 295 | "price": 1212, 296 | "coverFileName": "716d7202-d166-452a-b998-a17fe2666438qq.jpg" 297 | }, 298 | { 299 | "bookId": 79, 300 | "title": "The Ministry of Truth: The Biography of George Orwell's \"1984\"", 301 | "author": "Dorian Lynskey", 302 | "category": "Biography", 303 | "price": 5874, 304 | "coverFileName": "0e6688cb-47e4-438c-9648-543f174878d5qq.jpg" 305 | }, 306 | { 307 | "bookId": 80, 308 | "title": "Rough Magic: Riding the World's Loneliest Horse Race", 309 | "author": "Lara Prior-Palmer", 310 | "category": "Biography", 311 | "price": 8264, 312 | "coverFileName": "577f11d3-df70-4671-a468-6318da8ec16cqq.jpg" 313 | }, 314 | { 315 | "bookId": 81, 316 | "title": "This Storm", 317 | "author": "James Ellroy", 318 | "category": "Mystery", 319 | "price": 2232, 320 | "coverFileName": "d810fa08-0ed8-4dcb-8158-6f1ab98a45d3qq.jpg" 321 | }, 322 | { 323 | "bookId": 83, 324 | "title": "One Night at the Lake", 325 | "author": " Bethany Chase", 326 | "category": "Mystery", 327 | "price": 555, 328 | "coverFileName": "fe476d1e-cdc3-404b-913e-a52889d5b9fcqq.jpg" 329 | }, 330 | { 331 | "bookId": 84, 332 | "title": "The Chosen", 333 | "author": "Taran Matharu ", 334 | "category": "Fantasy", 335 | "price": 5555, 336 | "coverFileName": "ffa6e013-86ac-4d00-8516-a8fd8bc8ddf5qq.jpg" 337 | }, 338 | { 339 | "bookId": 85, 340 | "title": "All of Us with Wings", 341 | "author": "Michelle Ruiz Keil ", 342 | "category": "Fantasy", 343 | "price": 555, 344 | "coverFileName": "3618116f-ba09-4172-bc6a-7aa2dbe7d7c4qq.jpg" 345 | }, 346 | { 347 | "bookId": 86, 348 | "title": "Soul of the Sword", 349 | "author": " Julie Kagawa", 350 | "category": "Fantasy", 351 | "price": 55555, 352 | "coverFileName": "58dbce6c-7c82-4491-be4f-c170867ba4b6qq.jpg" 353 | }, 354 | { 355 | "bookId": 90, 356 | "title": "7/1/1993", 357 | "author": "qwert", 358 | "category": "Fiction", 359 | "price": 123, 360 | "coverFileName": "Default_image.jpg" 361 | } 362 | ] -------------------------------------------------------------------------------- /chromeExtension/dictionary.test.ts: -------------------------------------------------------------------------------- 1 | import { chromium, test } from "@playwright/test"; 2 | 3 | test("Handle Chrome Extension", async () => { 4 | const pathToExtension = "Y://my-code-base//Playwright-Test-Runner//my-ext" 5 | console.log(pathToExtension); 6 | 7 | const userDataDir = pathToExtension; 8 | const browserContext = await chromium.launchPersistentContext(userDataDir, { 9 | headless: false, 10 | args: [ 11 | `--disable-extensions-except=${pathToExtension}`, 12 | `--load-extension=${pathToExtension}` 13 | ] 14 | }); 15 | const backgroundPage = browserContext.backgroundPages()[0]; 16 | await backgroundPage.waitForTimeout(10000) 17 | // Test the background page as you would any other page. 18 | // backgroundPage.$(""); 19 | // await browserContext.close(); 20 | }) -------------------------------------------------------------------------------- /clickAndHold/clickhold.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | test("Click and hold", async ({ page }) => { 3 | 4 | 5 | // Go to https://letcode.in/ 6 | await page.goto('https://letcode.in/'); 7 | 8 | // Click text=Explore Workspace 9 | await page.click('text=Explore Workspace'); 10 | await expect(page).toHaveURL('https://letcode.in/test'); 11 | 12 | // Click text=Click 13 | await page.click('text=Click'); 14 | await expect(page).toHaveURL('https://letcode.in/buttons'); 15 | 16 | // Click button:has-text("Button Hold!") 17 | await page.click('button:has-text("Button Hold!")', { 18 | delay: 3000 19 | }) 20 | const ele = await page.$("h2") 21 | expect(await ele?.textContent()).toContain('long pressed') 22 | }) 23 | -------------------------------------------------------------------------------- /clipboard/clip.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | const clipboard = require("clipboardy"); 3 | var url: any; 4 | test("Access clipboard", async ({ page }) => { 5 | await test.step("Goto the https://clipboardjs.com/", async () => { 6 | await page.goto("https://clipboardjs.com/"); 7 | }); 8 | await test.step("click to copy", async () => { 9 | await page.click("//button[@data-clipboard-action='copy']", { force: true }); 10 | }); 11 | await test.step("access the clipboard", async () => { 12 | let text = await clipboard.read(); 13 | console.log('From clipboard: ' + text); 14 | await page.click("button[data-clipboard-target='#foo']") 15 | url = await clipboard.read(); 16 | console.log('From clipboard URL: ' + url); 17 | const newPage = await page.context().newPage(); 18 | await newPage.goto(url); 19 | console.log("Title from new tab: " + await newPage.title()); 20 | 21 | }); 22 | }) 23 | test.skip("Open url from clipboard", async ({ page }) => { 24 | await page.goto(url); 25 | console.log(await page.title()); 26 | 27 | }) -------------------------------------------------------------------------------- /customReport/myReporter.ts: -------------------------------------------------------------------------------- 1 | import { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult, TestStep } from "@playwright/test/reporter" 2 | export default class MyReporter implements Reporter { 3 | 4 | onBegin(config: FullConfig, suite: Suite): void { 5 | console.log('Suite Title: ' + suite.suites[0].suites[0].suites[0].title); 6 | } 7 | 8 | onTestBegin(test: TestCase, result: TestResult): void { 9 | console.log('Test Started: ' + test.title); 10 | } 11 | onTestEnd(test: TestCase, result: TestResult): void { 12 | console.log('Test Ended: ' + test.title); 13 | console.log("Result: " + result.status); 14 | 15 | } 16 | onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void { 17 | console.log(chunk); 18 | 19 | } 20 | 21 | onStepBegin(test: TestCase, result: TestResult, step: TestStep): void { 22 | if (step.category === "test.step") { 23 | console.log("Test step started: " + step.title); 24 | } 25 | // console.log(step.category); 26 | 27 | } 28 | 29 | onStepEnd(test: TestCase, result: TestResult, step: TestStep): void { 30 | if (step.category === "test.step") { 31 | console.log("Test step started: " + step.title); 32 | } 33 | } 34 | 35 | onError(error: TestError): void { 36 | console.log(error.message); 37 | } 38 | 39 | onEnd(result: FullResult): void | Promise { 40 | console.log('On end: ' + result.status); 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /customReport/report.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | 4 | test.describe("Custom Reporter", () => { 5 | 6 | test("Navigation test", async ({ page }) => { 7 | await test.step("visit letcode", async () => { 8 | await page.goto("https://letcode.in") 9 | }) 10 | await test.step("visit playwright", async () => { 11 | await page.goto("https://playwright.dev"); 12 | console.log("step completed"); 13 | }) 14 | }) 15 | 16 | }) -------------------------------------------------------------------------------- /customWaits/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Text Change 9 | 10 | 15 | 16 | 17 | 18 |
19 |
20 | Loading.. 21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /customWaits/waitForAlert.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("Wait for an alert", async ({ page }) => { 4 | 5 | await test.step("Goto to page", async () => { 6 | await page.goto("https://letcode.in/waits") 7 | }) 8 | await test.step("Accept the alert", async () => { 9 | page.on("dialog", async (alert) => { 10 | console.log('Alert message: ' + alert.message()); 11 | await alert.accept(); 12 | }); 13 | // await page.click("#accept") 14 | await page.waitForEvent("dialog"); 15 | }) 16 | }) -------------------------------------------------------------------------------- /customWaits/waitForTextChange.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("Wait for text change", async ({ page }) => { 4 | 5 | await page.goto("http://127.0.0.1:5500/customWaits/index.html"); 6 | const text = await page.locator("#change").innerText(); 7 | await page.waitForFunction("document.querySelector('#change').innerText != 'Loading..'") 8 | expect(page.locator("#change")).toHaveText("Welcome") 9 | 10 | }) -------------------------------------------------------------------------------- /customWaits/waitForTitle.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("Wait for title change", async ({ page }) => { 4 | 5 | await test.step("Goto to Flipkart", async () => { 6 | await page.goto("https://www.flipkart.com/") 7 | }) 8 | await test.step("Close the login pop up", async () => { 9 | await page.click("button:has-text('✕')") 10 | }) 11 | await test.step("naviagate to gaming and verify the title", async () => { 12 | await page.hover("//div[text()='Electronics']"); 13 | await page.waitForSelector("'Gaming'"); 14 | await page.click("'Gaming'"); 15 | expect(page).toHaveTitle(/.*Gaming.*/) 16 | }) 17 | }) -------------------------------------------------------------------------------- /data/login.cred.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "koushik1@letcode.in", 3 | "pass": "Pass123$" 4 | } -------------------------------------------------------------------------------- /destructing/destructing.js: -------------------------------------------------------------------------------- 1 | 2 | // Destructuring assignment 3 | 4 | let fruits = ["apple", "mango", "grape", "banana"]; 5 | // let first = fruits[0] 6 | // console.log(first); 7 | let [first, sec, thr, last] = fruits; 8 | console.log(first, thr, last); 9 | // let [first, , thr] = fruits 10 | // console.log(thr); 11 | let [a, ...rest] = fruits 12 | console.log(a); 13 | console.log(rest); 14 | 15 | let nums = getRandomNum(); 16 | console.log(nums); 17 | 18 | const myBio = { 19 | fullName: { 20 | fname: "k", 21 | lname: "c" 22 | }, 23 | age: 27, 24 | gender: "m" 25 | } 26 | console.log(myBio); 27 | let { fullName: { lname, fname }, age, gender } = myBio; 28 | console.log(lname, fname); 29 | 30 | // console.log(fname, age, gender); 31 | // let myName = myBio.fname; 32 | // console.log(myName); 33 | 34 | 35 | function getRandomNum() { 36 | const num = Math.random() * 9999; 37 | return num.toString(); 38 | } 39 | -------------------------------------------------------------------------------- /device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ortoniKC/Playwright-Test-Runner/a895cab4f16d4dc2dd3716220e65df04db298626/device.png -------------------------------------------------------------------------------- /downloads/download.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("Download file", async ({ page }, testInfo) => { 4 | 5 | await test.step("Goto the application", async () => { 6 | await page.goto("https://www.lambdatest.com/selenium-playground/generate-file-to-download-demo"); 7 | expect(await page.title()).toContain("Selenium"); 8 | }) 9 | await test.step("Enter test data", async () => { 10 | await page.locator("#textbox").type("Lorem ipsum dolor sit amet consectetur adipisicing elit. Corporis earum itaque assumenda doloribus reprehenderit, laborum, cum eaque dolor ducimus quos ad, quibusdam blanditiis. Architecto animi eligendi vero necessitatibus quasi rem quis quod eaque eius iste officiis nostrum id quae, est dolores, saepe perferendis quo! Doloremque neque quos rerum harum esse."); 11 | await page.locator("#create").click(); 12 | }) 13 | await test.step("Download file", async () => { 14 | const [download] = await Promise.all([ 15 | page.waitForEvent('download'), 16 | page.click("text='Download'") 17 | ]); 18 | const path = download.suggestedFilename(); 19 | await download.saveAs(path); 20 | await testInfo.attach('downloaded', { path: path }); 21 | }) 22 | }) -------------------------------------------------------------------------------- /electron/Prices.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ortoniKC/Playwright-Test-Runner/a895cab4f16d4dc2dd3716220e65df04db298626/electron/Prices.exe -------------------------------------------------------------------------------- /electron/electron.test.ts: -------------------------------------------------------------------------------- 1 | import { test, _electron } from "@playwright/test"; 2 | 3 | test("Electron app automation demo", async ({ }) => { 4 | 5 | const electronApp = await _electron.launch({ 6 | executablePath: "./Prices.exe" 7 | }) 8 | const appPath = await electronApp.evaluate(async ({ app }) => { 9 | // This runs in the main Electron process, parameter here is always 10 | // the result of the require('electron') in the main app script. 11 | return app.getAppPath(); 12 | }); 13 | console.log(appPath); 14 | const window = await electronApp.firstWindow(); 15 | // Print the title. 16 | console.log(await window.title()); 17 | // Capture a screenshot. 18 | await window.screenshot({ path: 'intro.png' }); 19 | // Direct Electron console to Node terminal. 20 | window.on('console', console.log); 21 | await electronApp.close(); 22 | }) -------------------------------------------------------------------------------- /eng.traineddata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ortoniKC/Playwright-Test-Runner/a895cab4f16d4dc2dd3716220e65df04db298626/eng.traineddata -------------------------------------------------------------------------------- /fixtures/basePages.ts: -------------------------------------------------------------------------------- 1 | import LoginPage from "@pages/Login.page" 2 | import HeaderPage from "@pages/Header.page" 3 | import CommonFunctions from "@pages/common.page" 4 | 5 | import { test as baseTexst } from "@playwright/test"; 6 | // var a; 7 | // var b: string; 8 | 9 | // type pages= { 10 | // loginPage: LoginPage; 11 | // headerPage: HeaderPage; 12 | // commonPage: CommonFunctions; 13 | // } 14 | 15 | const test = baseTexst.extend<{ 16 | loginPage: LoginPage; 17 | headerPage: HeaderPage; 18 | commonPage: CommonFunctions; 19 | }>({ 20 | loginPage: async ({ page }, use) => { 21 | await use(new LoginPage(page)); 22 | }, 23 | headerPage: async ({ page, isMobile }, use) => { 24 | await use(new HeaderPage(page, isMobile)); 25 | }, 26 | commonPage: async ({ page }, use) => { 27 | await use(new CommonFunctions(page)); 28 | }, 29 | }) 30 | export default test; 31 | export const expect = test.expect; 32 | -------------------------------------------------------------------------------- /fixtures/myFixtures.ts: -------------------------------------------------------------------------------- 1 | import { test as baseTest } from "@playwright/test"; 2 | 3 | type koushik = { 4 | hey: string; 5 | } 6 | 7 | 8 | const fixture = baseTest.extend({ 9 | hey: "I am letcode", 10 | 11 | }) 12 | 13 | export const test = fixture; 14 | export const assert = fixture.expect 15 | export const describe = fixture.describe; -------------------------------------------------------------------------------- /fixtures/mytest.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from "./myFixtures" 2 | 3 | test("my tets 1", async ({ hey }, info) => { 4 | info.skip(); 5 | console.log(hey.toUpperCase()); 6 | let text = hey.toUpperCase() 7 | // assert(text).toBe("I AM letcode"); 8 | console.log("Is it passed? " + info.status); 9 | // info. 10 | 11 | 12 | }) -------------------------------------------------------------------------------- /fixtures/test.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "./basePages" 2 | import * as data from "../data/login.cred.json"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/") 6 | }) 7 | 8 | // test.describe.serial.only("", () => { }) 9 | test.describe("Skip on Failure", () => { 10 | 11 | test("E2E test", async ({ headerPage, loginPage, commonPage, page }) => { 12 | await test.step("Login", async () => { 13 | await headerPage.clickLoginLink(); 14 | expect(page.url()).toBe("https://letcode.in/signin") 15 | await loginPage.enterUserName(data.email); 16 | await loginPage.enterUserPassword(data.pass); 17 | await loginPage.clickLoginBtn(); 18 | const toaster = await commonPage.toaster(); 19 | expect(await toaster?.textContent()).toContain("Welcome"); 20 | await headerPage.clickSignOutLink(); 21 | }) 22 | await test.step("search product", async () => { 23 | // todo 24 | }) 25 | 26 | await test.step("Checkout", async () => { 27 | // await headerPage.clickLoginLink(); 28 | // expect(page.url()).toBe("https://letcode.in/signin") 29 | // await loginPage.enterUserName(data.email); 30 | // await loginPage.enterUserPassword(data.pass); 31 | // await loginPage.clickLoginBtn(); 32 | // const toaster = await commonPage.toaster(); 33 | // expect(await toaster?.textContent()).toContain("Welcome"); 34 | // await headerPage.clickSignOutLink(); 35 | }) 36 | }) 37 | // test("Login negative", async ({ headerPage, loginPage, commonPage, page }) => { 38 | // await headerPage.clickLoginLink(); 39 | // await headerPage.clickLoginLink(); 40 | // expect(page.url()).toBe("https://letcode.in/signin") 41 | // await loginPage.enterUserName(data.email); 42 | // await loginPage.enterUserPassword("wrong"); 43 | // await loginPage.clickLoginBtn(); 44 | // const toaster = await commonPage.toaster(); 45 | // expect(await toaster?.textContent()).toContain("Welcome"); 46 | // await headerPage.clickSignOutLink(); 47 | // }) 48 | }) -------------------------------------------------------------------------------- /frames/frame.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | test("New Frame API", async ({ page }) => { 3 | 4 | await page.goto("/frame"); 5 | const frame = page.frameLocator("#firstFr"); 6 | await frame.locator("input[name='fname']").type("koushik") 7 | const lname = frame.locator("input[name='lname']"); 8 | await lname.type("chatterjee") 9 | const text = await frame.locator("p.title.has-text-info").textContent(); 10 | console.log(text); 11 | }); 12 | test("Inner frame", async ({ page }) => { 13 | await page.goto("/frame"); 14 | const frame = page.frameLocator("#firstFr"); 15 | const innerFrame = frame.frameLocator("iframe.has-background-white"); 16 | await innerFrame.locator("input[name='email']").type("koushik@letcode.in") 17 | await frame.locator("input[name='fname']").type("koushik"); 18 | await page.click("'Log in'"); 19 | }) -------------------------------------------------------------------------------- /generateReport.bat: -------------------------------------------------------------------------------- 1 | allure serve -------------------------------------------------------------------------------- /github.config.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | 4 | const config: PlaywrightTestConfig = { 5 | use: { 6 | headless: true, 7 | browserName: "chromium" 8 | }, 9 | testMatch: ["amazonprice.test.ts"], 10 | retries: 1 11 | } 12 | export default config; -------------------------------------------------------------------------------- /harDemo/trackRequest.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("Track all Request", async ({ page }) => { 4 | 5 | 6 | await page.routeFromHAR("har/booksapi.har", { 7 | update: false 8 | }) 9 | 10 | await page.goto("https://bookcart.azurewebsites.net/", { 11 | waitUntil:"domcontentloaded" 12 | }) 13 | await page.fill("input[type='search']", "HP7") 14 | await page.click("span.mat-option-text") 15 | await page.waitForTimeout(5000) 16 | }) 17 | -------------------------------------------------------------------------------- /helper/globalsetup.ts: -------------------------------------------------------------------------------- 1 | import { FullConfig } from "@playwright/test"; 2 | const AdmZip = require("adm-zip"); 3 | 4 | async function globalSetup(config: FullConfig) { 5 | console.log("Report path " + config.rootDir); 6 | 7 | const reportPath = config.rootDir + "\\playwright-report"; 8 | console.log("Report path: " + reportPath); 9 | 10 | var zip = new AdmZip(); 11 | zip.addLocalFolder(reportPath, "./playwrightReport"); 12 | zip.writeZip("./report.zip") 13 | 14 | } 15 | export default globalSetup; -------------------------------------------------------------------------------- /highlight/highlight.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | 4 | test("Highlight elements", async ({ page }, testInfo) => { 5 | 6 | await page.goto("https://letcode.in/edit"); 7 | let signup = page.locator("text='Sign up'") 8 | let login = page.locator("text='Log in'") 9 | let sc = await page.screenshot( 10 | { 11 | path: "./img.png", 12 | mask: [signup, login] 13 | }); 14 | await testInfo.attach("highlighted screenshot", { 15 | body: sc, 16 | contentType: "image/png" 17 | }) 18 | let ele = page.locator("#fullName"); 19 | expect.soft(await ele.getAttribute("placeholder")).toBe("Enter first & last"); 20 | console.log(await page.title()); 21 | 22 | 23 | }) -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ortoniKC/Playwright-Test-Runner/a895cab4f16d4dc2dd3716220e65df04db298626/img.png -------------------------------------------------------------------------------- /locatorAPI/locatorIndetail.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test" 2 | test.describe("My awesome report suite", () => { 3 | 4 | test("Locator in detail", async ({ page }, testInfo) => { 5 | 6 | await test.step("Login", async () => { 7 | await page.goto("https://www.saucedemo.com/"); 8 | const user = page.locator("input[placeholder='Username']") 9 | await user.fill("standard_user"); 10 | const pass = page.locator("input[placeholder='Password']") 11 | await pass.fill("secret_sauce"); 12 | await page.click("#login-button"); 13 | const screenshot = await page.screenshot(); 14 | await testInfo.attach("login screenshot", { 15 | contentType: "image/png", 16 | body: screenshot 17 | }) 18 | }) 19 | await test.step("Change price", async () => { 20 | const priceSelect = page.locator("select.product_sort_container") 21 | await priceSelect.selectOption({ value: "lohi" }) 22 | await testInfo.attach("login screenshot", { 23 | contentType: "image/png", 24 | body: await page.screenshot() 25 | }) 26 | const firstProduct = page.locator("#inventory_container div.inventory_item_label a div") 27 | console.log(await firstProduct.first().textContent()); 28 | await priceSelect.selectOption({ value: "hilo" }) 29 | await testInfo.attach("login screenshot", { 30 | contentType: "image/png", 31 | body: await page.screenshot() 32 | }) 33 | console.log(await firstProduct.first().textContent()); 34 | }) 35 | }) 36 | }) -------------------------------------------------------------------------------- /locatorAPI/locatorsVsElementHandle.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("Strict - Element handle", async ({ page }) => { 4 | await page.goto("https://www.freelancer.in/login"); 5 | let btn = await page.$("button", { 6 | strict: true 7 | }); 8 | await btn?.hover() 9 | await btn?.click(); 10 | }) 11 | 12 | test("Locator API", async ({ page }) => { 13 | await page.goto("https://letcode.in/elements"); 14 | const ele = page.locator("input[name='username']") 15 | await ele?.fill("ortonikc"); 16 | await ele?.press("Enter"); 17 | await page.waitForSelector("app-gitrepos ol li", { timeout: 5000 }) 18 | const repos = page.locator("app-gitrepos ol li"); 19 | console.log(await repos.count()); 20 | // console.log(await repos.allInnerTexts()); 21 | let c = await repos.count() 22 | for (let i = 0; i < c; i++) { 23 | let text = await repos.nth(i).textContent(); 24 | console.log(text); 25 | } 26 | 27 | }) -------------------------------------------------------------------------------- /locators/github.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | 4 | test("Signin into Git", async ({ page }) => { 5 | await page.goto("https://github.com/login"); 6 | await page.fill("input:below(:text('Username or'))", "ortoniKC"); 7 | await page.fill("#password:above(:text('Sign in'))", "password"); 8 | await page.click("a:near(:text('Password'))"); 9 | expect(page.url()).toBe("https://github.com/password_reset") 10 | 11 | // thread.sleep(); 12 | // await page.waitForTimeout(3000); 13 | 14 | }) 15 | 16 | test("In depth", async ({ page }) => { 17 | await page.goto('https://opengiro.ing.de/pub/girokonto-einzelkonto'); 18 | await page.pause(); 19 | await page.fill("input:below(:text('Prof.')):right-of(:text('Vorname'))", 20 | "koushik"); 21 | }) -------------------------------------------------------------------------------- /logger/Logger.ts: -------------------------------------------------------------------------------- 1 | "using strict"; 2 | const winston = require('winston'); 3 | 4 | const options = { 5 | level: "info", 6 | transports: [ 7 | new winston.transports.Console({ 8 | level: "info", 9 | colorize: false 10 | }), 11 | new winston.transports.File({ 12 | filename: 'logs/error.log', 13 | level: 'info', 14 | maxsize: 5242880, // 5MB 15 | maxFiles: 5, 16 | colorize: false, 17 | }), 18 | ] 19 | } 20 | 21 | const logger = winston.createLogger(options); 22 | 23 | logger.info('Hello world'); 24 | logger.warn("warning"); 25 | 26 | 27 | -------------------------------------------------------------------------------- /logger/playLogger.test.ts: -------------------------------------------------------------------------------- 1 | import { chromium, test } from "@playwright/test"; 2 | 3 | 4 | test("Logger", async ({ page }) => { 5 | // const browser = await chromium.launch({ 6 | // logger: { 7 | // isEnabled: (name, severity) => true, 8 | // log: (name, severity, message, args) => console.log(`name = ${name} \n msg = ${message} \n severity = ${severity} \n args = ${args}`) 9 | // } 10 | // }); 11 | // const context = await browser.newContext(); 12 | // const page = await context.newPage(); 13 | const consoleLogs = []; 14 | page.on("console", msg => { 15 | if (msg.type() == "error") { 16 | console.log(msg.text()); 17 | consoleLogs.push(msg.text()); 18 | } 19 | }) 20 | await page.goto("https://letcode.in/elements"); 21 | const btn = page.locator("#search"); 22 | await btn.click(); 23 | await page.goto("https://www.amazon.in/asas") 24 | 25 | // write the logs to the winston logger 26 | }); -------------------------------------------------------------------------------- /network/apiresponse.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("Read API response", async ({ page }) => { 4 | await page.goto("https://letcode.in/elements"); 5 | // verify response status | URL | Body 6 | 7 | 8 | const [response] = await Promise.all([ 9 | page.waitForResponse(res => 10 | res.status() == 200 11 | && 12 | res.url() == "https://api.github.com/users/ortonikc" 13 | && 14 | res.body().then(b => { 15 | console.log(b); 16 | return b.includes("Koushik letcode") 17 | }) 18 | ), 19 | page.fill("input[name='username']", "ortonikc"), 20 | page.click("button") 21 | ]) 22 | console.log(await response.json()); 23 | 24 | }) -------------------------------------------------------------------------------- /network/auth.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | test 4 | } from "@playwright/test"; 5 | 6 | test.describe("Network - Playwright", () => { 7 | 8 | test("Http Authentication", async ({ browser }) => { 9 | const context = await browser.newContext({ 10 | // proxy: { 11 | // server: "", 12 | // username: "", 13 | // password: "" 14 | // }, 15 | httpCredentials: { 16 | username: "admin", 17 | password: "admin" 18 | } 19 | }) 20 | const page = await context.newPage(); 21 | await page.goto("https://the-internet.herokuapp.com/basic_auth"); 22 | const header = await page.$("h3"); 23 | if (header) { 24 | console.log(await header.textContent()); 25 | expect(await header.textContent()).toBe("Basic Auth"); 26 | } 27 | }) 28 | 29 | 30 | 31 | }) -------------------------------------------------------------------------------- /network/blocker.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | test("Block images - Network Intercept", async ({ page }) => { 3 | 4 | 5 | await page.route("**/*", request => { 6 | 7 | return request.request().url() 8 | .startsWith("https://googleads.g.doubleclick.net/pagead/ads") 9 | ? request.abort() : 10 | request.continue(); 11 | 12 | // return request.request().resourceType() === "image" 13 | // ? request.abort() : 14 | // request.continue(); 15 | }) 16 | await page.goto("https://letcode.in/test") 17 | await page.waitForTimeout(5000); 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /network/mock-api-response.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | test("Mock API Response", async ({ page }) => { 3 | 4 | await page.route("**/Book", (route) => { 5 | route.fulfill({ 6 | status: 200, 7 | contentType: "application/json", 8 | path: "booksAPI.json" 9 | }) 10 | }) 11 | await page.goto("http://localhost:4200/books/home"); 12 | const titles = page.locator(".title.is-4") 13 | const count = await titles.count(); 14 | expect(count).toBeGreaterThan(0); 15 | for (let index = 0; index < count; index++) { 16 | console.log(await titles.nth(index).textContent()); 17 | } 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-runner", 3 | "version": "1.0.0", 4 | "description": "Playwright Awesome Tutorial", 5 | "main": "index.js", 6 | "scripts": { 7 | "report": "npx playwright test --reporter=html && npx playwright show-report", 8 | "debug": "npx playwright test --debug", 9 | "test": "npx playwright test", 10 | "test-us": "npx playwright test --update-snapshots", 11 | "github-action": "npx playwright test --config=github.config.ts" 12 | }, 13 | "devDependencies": { 14 | "@playwright/test": "1.23.2", 15 | "@types/nodemailer": "^6.4.4", 16 | "adm-zip": "^0.5.9", 17 | "clipboardy": "^2.3.0", 18 | "experimental-allure-playwright": "0.0.3", 19 | "nodemailer": "^6.6.3", 20 | "tesseract.js": "^2.1.5", 21 | "ts-node": "10.0.0", 22 | "typescript": "4.3.2", 23 | "winston": "^3.3.3" 24 | }, 25 | "keywords": [ 26 | "playwright", 27 | "playwright tutorial", 28 | "playwright test runner", 29 | "letcode koushik" 30 | ], 31 | "author": "Koushik (LetCode)", 32 | "license": "ISC" 33 | } -------------------------------------------------------------------------------- /parameterized-test/computerDB.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@fixtures/myFixtures"; 2 | import { expect } from "@playwright/test"; 3 | const computerData = [{ 4 | name: "Comp A", 5 | manufacture: "Tandy Corporation" 6 | }, 7 | { 8 | name: "Comp B", 9 | manufacture: "Commodore International" 10 | }, 11 | { 12 | name: "Comp C", 13 | manufacture: "Thinking Machines" 14 | }, 15 | { 16 | name: "Comp D", 17 | manufacture: "Nokia" 18 | } 19 | ] 20 | computerData.forEach(data => { 21 | 22 | test(`Parameterized test ${data.name}`, async ({ page }) => { 23 | await page.goto("https://computer-database.gatling.io/computers"); 24 | await page.click("#add"); 25 | await page.fill("#name", data.name); 26 | await page.selectOption("#company", { 27 | label: data.manufacture 28 | }); 29 | await page.click("input[type='submit']"); 30 | expect(page.locator("div.alert-message.warning")).toContainText("Done") 31 | }) 32 | }) -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { devices, PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | 4 | const config: PlaywrightTestConfig = { 5 | 6 | 7 | // workers: 4, 8 | // fullyParallel: true, 9 | // projects: [ 10 | // { 11 | // name: 'chromium', 12 | // use: { ...devices['Desktop Chrome'] }, 13 | // }, 14 | // { 15 | // name: 'Pixel', 16 | // use: { ...devices['Pixel 5'] }, 17 | // }, 18 | // // { 19 | // // name: 'firefox', 20 | // // use: { ...devices['Desktop Firefox'] }, 21 | // // }, 22 | // // { 23 | // // name: 'webkit', 24 | // // use: { ...devices['Desktop Safari'] }, 25 | // // }, 26 | // ], 27 | 28 | use: { 29 | viewport: null, 30 | headless: !true, 31 | // browserName: "chromium", 32 | screenshot: "on", 33 | video: "on", 34 | // trace: "on", 35 | baseURL: "https://www.letcode.in", 36 | // baseURL: "https://dev107189.service-now.com/api/now/table/incident", 37 | extraHTTPHeaders: { 38 | "Authorization": "Basic YWRtaW46U0NxN2pDb2tDbFI4" 39 | } 40 | // baseURL: "https://letcode.in", 41 | // contextOptions: { 42 | // permissions: ["clipboard-read"] 43 | // } 44 | , 45 | launchOptions: { 46 | args: ["--start-maximized"], 47 | 48 | // logger: { 49 | // // isEnabled: (name, severity) => true, 50 | // // log: (name, severity, message, args) => console.log(name, severity) 51 | // } 52 | } 53 | }, 54 | // timeout: 60000, 55 | // grep: [new RegExp("@smoke"), new RegExp("@reg")], 56 | // testMatch: ["harDemo/trackRequest.test.ts"], 57 | retries: 0, 58 | // reporter: "./customReport/myReporter.ts" 59 | reporter: [ 60 | ["dot"], // -> console 61 | ["json", { outputFile: "test-result.json" }], // -> JSON 62 | ['html', { 63 | open: "always" 64 | }] // -> HTML 65 | ], 66 | // globalTeardown: './helper/globalsetup.ts' 67 | } 68 | export default config; -------------------------------------------------------------------------------- /pom/letcodemodules/credentials/Header.page.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | export default class HeaderPage { 4 | 5 | private page: Page; 6 | private isMob: boolean | undefined; 7 | 8 | constructor(page: Page, isMob: boolean | undefined) { 9 | this.page = page; 10 | this.isMob = isMob 11 | } 12 | 13 | 14 | // locators 15 | 16 | public get eleLoginBtn() { 17 | const loginBtn = this.page.$("text=Log in"); 18 | if (loginBtn != null) { 19 | return loginBtn; 20 | } else throw new Error("No Element") 21 | } 22 | 23 | public get eleSignOutBtn() { 24 | const signoutEle = this.page.$("text=Sign out"); 25 | if (signoutEle != null) { 26 | return signoutEle; 27 | } else throw new Error("No Element") 28 | } 29 | 30 | public async clickLoginLink() { 31 | console.log("Is mobile view? " + this.isMob); 32 | 33 | if (this.isMob) { 34 | await this.page.click("//a[@aria-label='menu']") 35 | } 36 | await Promise.all([ 37 | this.page.waitForNavigation({ 38 | waitUntil: "domcontentloaded" 39 | }), 40 | this.page.click("text=Log in") 41 | ]) 42 | // const ele = await this.eleLoginBtn; 43 | // await ele?.click(); 44 | } 45 | public async clickSignOutLink() { 46 | const ele = await this.eleSignOutBtn; 47 | await ele?.click(); 48 | } 49 | } -------------------------------------------------------------------------------- /pom/letcodemodules/credentials/Login.page.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | export default class LoginPage { 4 | 5 | private page: Page; 6 | constructor(page: Page) { 7 | this.page = page; 8 | } 9 | 10 | eleEmailTextField = async () => await this.page.$("input[name='email']"); 11 | 12 | // public get eleEmailTextField() { 13 | // return this.page.$("input[name='email']") 14 | // // return elename; 15 | // } 16 | 17 | elePassTextField = async () => await this.page.$("input[name='password']"); 18 | 19 | public get eleLoginBtn() { 20 | return this.page.$("//button[text()='LOGIN']") 21 | // return elename; 22 | } 23 | 24 | public async enterUserName(name: string) { 25 | const ele = await this.eleEmailTextField(); 26 | if (ele != null) 27 | await ele.fill(name); 28 | else throw new Error("No element, hence failed") 29 | } 30 | public async enterUserPassword(pass: string) { 31 | const ele = await this.elePassTextField(); 32 | await ele?.fill(pass); 33 | } 34 | public async clickLoginBtn() { 35 | await this.page.click("//button[text()='LOGIN']") 36 | // const ele = await this.eleLoginBtn; 37 | // await ele?.click(); 38 | } 39 | 40 | public async login(username: string, pass: string) { 41 | await this.enterUserName(username); 42 | await this.enterUserPassword(pass); 43 | await this.clickLoginBtn(); 44 | } 45 | } -------------------------------------------------------------------------------- /pom/letcodemodules/credentials/common.page.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | export default class CommonFunctions { 4 | 5 | private page: Page; 6 | constructor(page: Page) { 7 | this.page = page; 8 | } 9 | goto = async () => { 10 | await this.page.goto("https://letcode.in") 11 | } 12 | toaster = async () => await this.page.waitForSelector("div[role='alertdialog']"); 13 | 14 | // public async verifToastMessage() { 15 | 16 | // } 17 | } -------------------------------------------------------------------------------- /reportDemo/clip.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | const clipboard = require("clipboardy"); 3 | var url: any; 4 | test("Access clipboard", async ({ page }) => { 5 | await test.step("Goto the https://clipboardjs.com/", async () => { 6 | await page.goto("https://clipboardjs.com/"); 7 | }); 8 | await test.step("click to copy", async () => { 9 | await page.click("//button[@data-clipboard-action='copy']", { force: true }); 10 | }); 11 | await test.step("access the clipboard", async () => { 12 | let text = await clipboard.read(); 13 | console.log('From clipboard: ' + text); 14 | await page.click("button[data-clipboard-target='#foo']") 15 | url = await clipboard.read(); 16 | console.log('From clipboard URL: ' + url); 17 | const newPage = await page.context().newPage(); 18 | await newPage.goto(url); 19 | console.log("Title from new tab: " + await newPage.title()); 20 | 21 | }); 22 | }) 23 | test("Open url from clipboard", async ({ page }) => { 24 | await page.goto(url); 25 | console.log(await page.title()); 26 | }) -------------------------------------------------------------------------------- /reportDemo/github.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | 4 | test("Signin into Git", async ({ page }) => { 5 | await page.goto("https://github.com/login"); 6 | await page.fill("input:below(:text('Username or'))", "ortoniKC"); 7 | await page.fill("#password:above(:text('Sign in'))", "password"); 8 | await page.click("a:near(:text('Password'))"); 9 | expect(page.url()).toBe("https://github.com/password_reset") 10 | 11 | // thread.sleep(); 12 | // await page.waitForTimeout(3000); 13 | 14 | }) 15 | 16 | test("In depth", async ({ page }) => { 17 | await page.goto('https://opengiro.ing.de/pub/girokonto-einzelkonto'); 18 | // await page.pause(); 19 | await page.fill("input:below(:text('Prof.')):right-of(:text('Vorname'))", 20 | "koushik"); 21 | }) -------------------------------------------------------------------------------- /reportDemo/service-now.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | let _number: number; 4 | let _sys_id: string; 5 | const short_description = "required ms office 365" 6 | // Create 7 | test("Create an Incident", async ({ request, baseURL }) => { 8 | 9 | const _response = await request.post(`${baseURL}`, { 10 | data: { 11 | "short_description": short_description, 12 | "category": "hardware" 13 | }, headers: { 14 | "Accept": "application/json" 15 | } 16 | }); 17 | expect(_response.status()).toBe(201); 18 | expect(_response.ok()).toBeTruthy(); 19 | console.log(await _response.json()); 20 | const res = await _response.json(); 21 | _number = res.result.task_effective_number; 22 | _sys_id = res.result.sys_id; 23 | 24 | // output as xml 25 | // console.log((await _response.body()).toString()); 26 | }) 27 | // test("", async ({ page }) => { 28 | // await page.request.get("") 29 | // }) 30 | // Get 31 | test("Get an Incident", async ({ request, baseURL }) => { 32 | const _response = await request.get(`${baseURL}`, { 33 | params: { 34 | task_effective_number: _number, 35 | sysparm_fields: "short_description, category" 36 | } 37 | }); 38 | console.log(await _response.json()); 39 | expect(_response.status()).toBe(200); 40 | expect(await _response.json()).toMatchObject({ 41 | result: [{ short_description: short_description, category: 'hardware' }] 42 | }) 43 | 44 | }) 45 | 46 | // Uppdate 47 | test("Put(Modify) an Incident", async ({ request, baseURL }) => { 48 | const _response = await request.put(`${baseURL}/${_sys_id}`, { 49 | data: { 50 | "short_description": "Very boring tutorial", 51 | "category": "Software" 52 | } 53 | }); 54 | console.log(await _response.json()); 55 | expect(_response.status()).toBe(200); 56 | expect(_response.ok()).toBeTruthy(); 57 | }) 58 | 59 | // Delete 60 | test("Delete an Incident", async ({ request, baseURL }) => { 61 | const _response = await request.delete(`${baseURL}/${_sys_id}`); 62 | // console.log(await _response.json()); 63 | expect(_response.status()).toBe(204); 64 | expect(_response.ok()).toBeTruthy(); 65 | }) 66 | -------------------------------------------------------------------------------- /shadow-dom/shadow.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | // test("Interact with shadow DOM", async ({ page }) => { 4 | // await page.goto("https://letcode.in/shadow"); 5 | // await page.fill("#fname", "koushik"); 6 | // await page.waitForTimeout(3000); 7 | // }); 8 | 9 | 10 | 11 | test("chromium bug App", async ({ page }) => { 12 | await page.goto("https://bugs.chromium.org/p/chromium/issues/list"); 13 | // select dropdown 14 | const ele = await page.$("#can") 15 | if (ele) { 16 | await ele.selectOption({ 17 | label: "Issues to verify" 18 | }) 19 | } else throw new Error("Eleemnt not found") 20 | 21 | // input data 22 | 23 | await page.fill("#searchq", "some bug"); 24 | await page.waitForTimeout(3000); 25 | }) -------------------------------------------------------------------------------- /slider/slider.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("Slider Demo", async ({ page }) => { 4 | await test.step("Goto demo site", async () => { 5 | await page.goto("https://groww.in/calculators/sip-calculator"); 6 | 7 | }); 8 | await test.step("handle slider", async () => { 9 | await page.waitForSelector("(//div[@role='slider'])[1]"); 10 | const s = await page.$("(//div[@role='slider'])[1]") 11 | let ele = page.locator("#MONTHLY_INVESTMENT") 12 | let text = await ele.inputValue(); 13 | console.log('Initial text: ' + text); 14 | let targetAmount = "82500"; 15 | let isCompleted = false; 16 | if (s) { 17 | while (!isCompleted) { 18 | let srcBound = await s.boundingBox(); 19 | if (srcBound) { 20 | await page.mouse.move(srcBound.x + srcBound.width / 2, 21 | srcBound.y + srcBound.height / 2) 22 | await page.mouse.down(); 23 | await page.mouse.move(srcBound.x + 15, srcBound.y + srcBound.height / 2); 24 | await page.mouse.up(); 25 | let text = await ele.inputValue(); 26 | if (text == targetAmount) { 27 | isCompleted = true; 28 | } 29 | } 30 | } 31 | 32 | } 33 | 34 | 35 | 36 | await page.waitForTimeout(5000) 37 | }); 38 | }) 39 | -------------------------------------------------------------------------------- /tags/tags.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | test('Test signup page @smoke', async () => { 4 | console.log("some signup test @smoke"); 5 | }); 6 | 7 | test('Test login page @sanity', async () => { 8 | console.log("some login test @sanity"); 9 | }); 10 | 11 | test('Test login page @reg', async () => { 12 | console.log("some login test @reg"); 13 | }); 14 | 15 | 16 | test('Test add to cart page @smoke', async () => { 17 | console.log("some add to cart test @smoke"); 18 | }); -------------------------------------------------------------------------------- /tesseractdemo/justdial.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | const Tesseract = require('tesseract.js'); 3 | 4 | test("Tesseract - Image to text", async ({ page }) => { 5 | 6 | await page.goto("https://www.justdial.com/Chennai/Car-Washing-Services-in-Adyar/nct-10079017"); 7 | await page.waitForSelector('#best_deal_div >> text=X'); 8 | await page.click('#best_deal_div >> text=X', { force: true, delay: 2000 }); 9 | const images = await page.$$("section p.contact-info span a"); 10 | for await (let img of images) { 11 | const name = Date.now(); 12 | await img.screenshot({ path: `${name}.png` }); 13 | await convertToText(`${name}.png`) 14 | } 15 | }); 16 | 17 | async function convertToText(name: string) { 18 | let imgText = await Tesseract.recognize( 19 | `./${name}`); 20 | console.log(imgText.data.text); 21 | } 22 | 23 | // Selenium code to handle pseudo element (not working for this scenario) 24 | /** 25 | public static void main(String[] args) { 26 | driver.get("https://www.justdial.com/Chennai/Car-Washing-Services-in-Ashok-Nagar/nct-10079017"); 27 | List phones = driver.findElementsByCssSelector("section#bcard0 section.jcar p.contact-info a b span"); 28 | for (WebElement ph : phones) { 29 | String script = "return window.getComputedStyle(arguments[0], ':before').getPropertyValue('content')"; 30 | String phone = driver.executeScript(script, ph).toString(); 31 | System.out.println(phone); 32 | } 33 | driver.quit(); 34 | } 35 | */ -------------------------------------------------------------------------------- /test-1.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('test', async ({ page }) => { 4 | // Recording... 5 | }); -------------------------------------------------------------------------------- /test-runner.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /test/first.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page, test } from "@playwright/test"; 2 | 3 | test.describe("Suite demo", () => { 4 | let page: Page; 5 | test.beforeAll(async ({ browser }) => { 6 | page = await browser.newPage(); 7 | await page.goto("https://letcode.in"); 8 | }) 9 | // test.beforeEach(() => { }) 10 | test("open letcode and verify title", async () => { 11 | const title = await page.title(); 12 | expect(title).toBe("LetCode with Koushik"); 13 | }); 14 | 15 | test("open letcode and login", async () => { 16 | // const title = await page.title(); 17 | // expect(title).toBe("LetCode with Koushik"); 18 | await Promise.all([ 19 | page.waitForNavigation(/*{ url: 'https://letcode.in/signin' }*/), 20 | page.click('text=/.*Log in.*/') 21 | ]); 22 | await page.click('input[name="email"]'); 23 | await page.fill('input[name="email"]', 'koushik350@gmail.com'); 24 | await page.fill('input[name="password"]', 'Pass123$'); 25 | await Promise.all([ 26 | page.waitForNavigation(/*{ url: 'https://letcode.in/' }*/), 27 | page.click('//button[normalize-space(.)=\'LOGIN\']') 28 | ]); 29 | expect(page.url()).toContain("https://letcode.in/"); 30 | }); 31 | }) 32 | 33 | -------------------------------------------------------------------------------- /test/smoke/cms/pom.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@fixtures/basePages"; 2 | import test from "@fixtures/basePages"; 3 | import * as data from "../../../data/login.cred.json"; 4 | 5 | 6 | test.describe("POM - TC001", () => { 7 | 8 | test("Login positive", async ({ headerPage, loginPage, commonPage, page }) => { 9 | await page.goto("https://letcode.in/") 10 | expect(page.url()).toBe("https://letcode.in/") 11 | await headerPage.clickLoginLink(); 12 | expect(page.url()).toBe("https://letcode.in/signin") 13 | await loginPage.enterUserName(data.email); 14 | await loginPage.enterUserPassword(data.pass); 15 | await loginPage.clickLoginBtn(); 16 | const toaster = await commonPage.toaster(); 17 | expect(await toaster?.textContent()).toContain("Welcome"); 18 | await page.reload(); 19 | await headerPage.clickSignOutLink(); 20 | }); 21 | test.only("Login again", async ({ headerPage, page, loginPage }) => { 22 | await page.goto("https://letcode.in/") 23 | await headerPage.clickLoginLink(); 24 | await loginPage.login("koushik350@gmail.com", data.pass); 25 | await page.waitForNavigation(); 26 | expect(page.url()).toBe("https://letcode.in/") 27 | }) 28 | }) -------------------------------------------------------------------------------- /test/tc001.test.ts.bak: -------------------------------------------------------------------------------- 1 | 2 | import * as data from "../../data/login.cred.json"; 3 | import { BrowserContext, expect, Page, test } from "@playwright/test"; 4 | import CommonFunctions from "../letcodemodules/credentials/common.page"; 5 | import HeaderPage from "../letcodemodules/credentials/Header.page"; 6 | import LoginPage from "../letcodemodules/credentials/Login.page"; 7 | 8 | test.describe("TC002", () => { 9 | let header: HeaderPage; 10 | let login: LoginPage; 11 | let common: CommonFunctions; 12 | let page: Page; 13 | let context: BrowserContext; 14 | test.beforeAll(async ({ browser }) => { 15 | context = await browser.newContext(); 16 | await context.tracing.start({ 17 | screenshots: true, 18 | snapshots: true 19 | }) 20 | page = await context.newPage(); 21 | header = new HeaderPage(page); 22 | login = new LoginPage(page); 23 | common = new CommonFunctions(page); 24 | }) 25 | test.afterAll(async () => { 26 | await context.tracing.stop({ 27 | path: "trace.zip" 28 | }); 29 | }) 30 | test.beforeEach(async () => { 31 | await page.goto("https://letcode.in") 32 | }) 33 | 34 | test("Login positive", async () => { 35 | expect(page.url()).toBe("https://letcode.in/") 36 | await header.clickLoginLink(); 37 | expect(page.url()).toBe("https://letcode.in/signin") 38 | await login.enterUserName(data.email); 39 | await login.enterUserPassword(data.pass); 40 | await login.clickLoginBtn(); 41 | const toaster = await common.toaster(); 42 | expect(await toaster?.textContent()).toContain("Welcome"); 43 | await header.clickSignOutLink(); 44 | }); 45 | test("Login again", async () => { 46 | await header.clickLoginLink(); 47 | await login.login("koushik350@gmail.com", data.pass); 48 | await page.waitForNavigation(); 49 | expect(page.url()).toBe("https://letcode.in/") 50 | }) 51 | }) -------------------------------------------------------------------------------- /timeout_demo/waitfor.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | 4 | test("Playwright waitfor demo", async ({ page }) => { 5 | 6 | await page.goto("https://letcode.in/"); 7 | await page.click("'Log in'"); 8 | await page.fill("input[name='email']", "koushik1@letcode.in"); 9 | await page.fill("input[name='password']", "Pass123$"); 10 | await page.click("button:has-text('LOGIN')"); 11 | const toast = page.locator("#toast-container div.toast-info") 12 | await toast.waitFor({ state: "visible" }); 13 | await toast.waitFor({ 14 | state: "hidden" 15 | }) 16 | await page.click("'Sign out'"); 17 | }) -------------------------------------------------------------------------------- /tracing/tracing.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | // Update: 3 | // Instead of this, we can directly use it in the playwright.config.ts 4 | test("Conetext tracing", async ({ browser }) => { 5 | const context = await browser.newContext(); 6 | // start tracing 7 | await context.tracing.start( 8 | { 9 | screenshots: true, snapshots: true 10 | } 11 | ); 12 | const page = await context.newPage(); 13 | await page.goto('https://letcode.in'); 14 | // Stop tracing and export it into a zip archive. 15 | await context.tracing.stop({ path: 'trace0.zip' }); 16 | }) 17 | // for other browser 18 | // test("Browser tracing", async ({ page, browser }) => { 19 | // await browser.startTracing(page, { path: "./trace1.zip" }); 20 | // await page.goto('https://letcode.in'); 21 | // await browser.stopTracing(); 22 | // }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "strict": true, 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "resolveJsonModule": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@pages/*": [ 11 | "pom/letcodemodules/credentials/*" 12 | ], 13 | "@fixtures/*": [ 14 | "fixtures/*" 15 | ], 16 | "@files/*": [ 17 | "uploadFolder/*" 18 | ] 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /v1.5-releases/drag.tets.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("my test", async ({ page }) => { 4 | await page.goto("https://letcode.in/dropable") 5 | const src = await page.$("#draggable") 6 | const dst = await page.$("#droppable"); 7 | if (src && dst) { 8 | const srcBound = await src.boundingBox() 9 | const dstBound = await dst.boundingBox() 10 | if (srcBound && dstBound) { 11 | await page.mouse.move(srcBound.x + srcBound.width / 2, srcBound.y + srcBound.height / 2) 12 | await page.mouse.down(); 13 | await page.mouse.move(dstBound.x + dstBound.width / 2, dstBound.y + dstBound.height / 2) 14 | await page.mouse.down(); 15 | } else { 16 | throw new Error("No Element") 17 | } 18 | } 19 | 20 | }) 21 | -------------------------------------------------------------------------------- /v1.5-releases/mousewheel.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | test("Color Scheme", async ({ page }) => { 4 | await page.goto("https://playwright.dev") 5 | console.log(await page.title()); 6 | let git = page.locator("text=GitHub"); 7 | const box = await git.boundingBox(); 8 | if (box) { 9 | const y = box.y; 10 | await page.mouse.wheel(0, y); 11 | } 12 | // await git.scrollIntoViewIfNeeded(); 13 | // await page.waitForTimeout(3000); 14 | }) 15 | -------------------------------------------------------------------------------- /v1.5-releases/myAwesome.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test.describe.parallel("Parallel", () => { 4 | test("goto letcode", async ({ page }) => { 5 | await page.goto("https://letcode.in") 6 | console.log(await page.title()); 7 | }); 8 | test("goto letcode", async ({ page }) => { 9 | await page.goto("https://playwright.dev") 10 | console.log(await page.title()); 11 | }); 12 | test("goto google.com", async ({ page }) => { 13 | await page.goto("https://google.com") 14 | console.log(await page.title()); 15 | }); 16 | }) -------------------------------------------------------------------------------- /visual-comparsion/visual.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("Visual comparsion", async ({ page }) => { 4 | await page.goto("https://letcode.in"); 5 | 6 | expect(await page.screenshot({ 7 | fullPage: true 8 | })).toMatchSnapshot("letcode.png") 9 | 10 | }) 11 | 12 | test('example test', async ({ page }) => { 13 | await page.goto('https://playwright.dev'); 14 | expect(await page.screenshot()).toMatchSnapshot('snapshot-name.png'); 15 | }); -------------------------------------------------------------------------------- /visual-comparsion/visual.test.ts-snapshots/letcode-chromium-win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ortoniKC/Playwright-Test-Runner/a895cab4f16d4dc2dd3716220e65df04db298626/visual-comparsion/visual.test.ts-snapshots/letcode-chromium-win32.png -------------------------------------------------------------------------------- /visual-comparsion/visual.test.ts-snapshots/snapshot-name-chromium-win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ortoniKC/Playwright-Test-Runner/a895cab4f16d4dc2dd3716220e65df04db298626/visual-comparsion/visual.test.ts-snapshots/snapshot-name-chromium-win32.png -------------------------------------------------------------------------------- /webscraping/amazonprice.test.ts: -------------------------------------------------------------------------------- 1 | const url = "https://www.amazon.in/Maono-AU-400-Lavalier-Microphone-Black/dp/B07JF9B592/ref=sr_1_4?dchild=1&keywords=moana+mic&qid=1630822326&sr=8-4"; 2 | 3 | import { test } from "@playwright/test"; 4 | 5 | import * as auth from "./auth.json" 6 | 7 | const nodemailer = require("nodemailer"); 8 | test("Amazon Price Drop Notification", async ({ page }) => { 9 | 10 | // Navigate to the Amazon product page 11 | await page.goto(url); 12 | // get the price of the product 13 | const price = await page.$eval("#priceblock_ourprice", el => el.textContent); 14 | // remove the currency symbol and the comma from the price 15 | const currentPrice = price?.replace("₹", '').split(".")[0]; 16 | console.log(currentPrice); 17 | // send the email using nodemailer 18 | sendEmailNotification(currentPrice); 19 | if (Number(currentPrice) < 400) { 20 | } 21 | 22 | }) 23 | 24 | function sendEmailNotification(currentPrice: string | undefined) { 25 | // github environment variables 26 | const { MY_EMAIL, MY_PASS } = process.env; 27 | console.log(MY_EMAIL, MY_PASS); 28 | // create a transporter object 29 | const transporter = nodemailer.createTransport({ 30 | service: 'gmail', 31 | auth: { 32 | user: MY_EMAIL, 33 | pass: MY_PASS 34 | } 35 | }); 36 | transporter.sendMail({ 37 | from: MY_EMAIL, 38 | to: 'koushik350@gmail.com', 39 | subject: 'Amazon Price Drop Notification', 40 | html: `

The price of the product has dropped to ${currentPrice}

click to open` 41 | }, (err: any, info: any) => { 42 | if (err) { 43 | console.error(err); 44 | } else { 45 | console.log(info); 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /webscraping/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "", 3 | "password": "" 4 | } -------------------------------------------------------------------------------- /webscraping/youtube.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | const list = ["https://www.youtube.com/playlist?list=PL699Xf-_ilW6vI9FHmePi1TvKyzYATgXi", 4 | "https://www.youtube.com/playlist?list=PL699Xf-_ilW7EyC6lMuU4jelKemmS6KgD"]; 5 | list.forEach(url => { 6 | test("Calculate youtube playlist duration" + Date.now(), async ({ page }) => { 7 | await page.goto(url); 8 | const videos = await page.$$("ytd-thumbnail-overlay-time-status-renderer span"); 9 | console.log(videos.length); 10 | let totalMinutes = 0; 11 | await Promise.all( 12 | videos.map(async ele => { 13 | const duration = await ele.innerText(); 14 | // 15:45:15 15 | const timeSlices = duration.trim().split(":") 16 | let minutes = 0; 17 | let seconds = 0; 18 | if (timeSlices.length == 2) { 19 | minutes = Number(timeSlices[0]); 20 | seconds = Number(timeSlices[1]); 21 | minutes += seconds / 60; 22 | } else if (timeSlices.length == 3) { 23 | let hours = Number(timeSlices[0]); 24 | minutes = Number(timeSlices[1]); 25 | seconds = Number(timeSlices[2]); 26 | minutes += ((hours * 60) + (seconds / 60)) 27 | } 28 | totalMinutes += minutes; 29 | }) 30 | ) 31 | console.log(totalMinutes); 32 | const hours = Math.floor(totalMinutes / 60); 33 | const minutes = Math.trunc(totalMinutes % 60); 34 | const seconds = Math.trunc((totalMinutes - Math.trunc(totalMinutes)) * 60); 35 | const title = await page.title(); 36 | console.log( 37 | `${title} -->${hours}h ${minutes}m ${seconds}s`); 38 | }) 39 | }) 40 | --------------------------------------------------------------------------------