├── .github └── workflows │ ├── publish.yaml │ └── test.yaml ├── .gitignore ├── .nvmrc ├── README.md ├── changelog.md ├── contributing.md ├── environment.d.ts ├── features └── connect-without-extension.feature ├── package.json ├── playwright-test.config.ts ├── playwright.config.ts ├── pnpm-lock.yaml ├── qa ├── TonConnectWidget.ts ├── WalletExtension.ts ├── index.ts ├── test.ts ├── tonkeeper │ ├── Tonkeeper.ts │ ├── tonkeeperExtension.ts │ └── tonkeeperFixture.ts └── util.ts ├── steps ├── fixtures.ts └── index.ts ├── test └── lab.spec.ts ├── tsconfig-release.json └── tsconfig.json /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_run: 5 | workflows: [test] 6 | types: 7 | - completed 8 | branches: 9 | - main 10 | 11 | jobs: 12 | publish: 13 | permissions: 14 | contents: read 15 | id-token: write 16 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: pnpm/action-setup@v4 21 | with: 22 | version: 9 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 22 26 | cache: 'pnpm' 27 | cache-dependency-path: '**/pnpm-lock.yaml' 28 | - run: pnpm install --frozen-lockfile 29 | - run: pnpm build 30 | - uses: JS-DevTools/npm-publish@v3 31 | with: 32 | token: ${{ secrets.NPM_TOKEN }} 33 | tag: latest 34 | access: public 35 | provenance: true 36 | dry-run: false 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: 'pnpm' 23 | cache-dependency-path: '**/pnpm-lock.yaml' 24 | - run: pnpm install 25 | - run: pnpm lint 26 | - run: npx playwright install --with-deps 27 | - run: pnpm test 28 | - uses: actions/upload-artifact@v4 29 | if: always() 30 | with: 31 | name: playwright-artifacts 32 | path: | 33 | playwright-report/ 34 | test-results/ 35 | retention-days: 10 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .idea 4 | .cache* 5 | 6 | # npm 7 | node_modules 8 | npm-debug.log 9 | types 10 | 11 | # env 12 | .env 13 | 14 | # tests 15 | extension 16 | test-results 17 | playwright-report 18 | qa-report 19 | **/.features-gen/**/*.spec.js 20 | 21 | .nx 22 | /update.sh 23 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TON-Connect QA 2 | 3 | This guide provides a quick setup process for Playwright to automate tests for TON Dapps, note that this is a basic configuration 4 | 5 | ## Prerequisites 6 | 7 | - Node.js v18+ 8 | - Playwright and TypeScript knowledge 9 | 10 | ## Installation 11 | 12 | 1. Install Playwright and its dependencies: 13 | 14 | ```bash 15 | npm init playwright@latest 16 | ``` 17 | 18 | Follow the prompts to complete the installation 19 | 20 | 21 | 2. Install dev dependency: 22 | 23 | ```bash 24 | npm install --save-dev @tonconnect/qa 25 | ``` 26 | 27 | ## Setup 28 | 29 | 1. Create or update your Playwright configuration file (e.g., `playwright.config.ts`): 30 | 31 | ```typescript 32 | import 'dotenv/config' 33 | import { defineConfig, devices } from '@playwright/test' 34 | 35 | // Define Playwright configuration 36 | export default defineConfig({ 37 | testDir: './test', 38 | fullyParallel: true, 39 | forbidOnly: !!process.env.CI, 40 | retries: process.env.CI ? 2 : 0, 41 | workers: process.env.CI ? 1 : undefined, 42 | reporter: 'html', 43 | use: { 44 | // Set base URL for tests 45 | baseURL: 'http://localhost:3000', 46 | trace: 'on-first-retry', 47 | }, 48 | projects: [ 49 | { 50 | name: 'chromium', 51 | use: { ...devices['Desktop Chrome'] }, 52 | }, 53 | ], 54 | }) 55 | ``` 56 | 57 | 2. Create a test file (e.g., `tests/example.spec.ts`): 58 | 59 | ```typescript 60 | // Import necessary modules and setup 61 | import { TonConnectWidget, testWith, tonkeeperFixture } from '@tonconnect/qa' 62 | 63 | // Create a test instance Tonkeeper fixtures 64 | const test = testWith(tonkeeperFixture(process.env.WALLET_MNEMONIC!)) 65 | 66 | // Extract expect function from test 67 | const { expect } = test 68 | 69 | // Define a basic test case 70 | test('lab', async ({ context, wallet }) => { 71 | // Navigate to the homepage 72 | const app = await context.newPage() 73 | await app.goto('https://ton-connect.github.io/demo-dapp-with-react-ui/') 74 | 75 | // Click the connect button 76 | const connectButton = app.getByRole('button', { name: 'Connect wallet to send the transaction' }) 77 | 78 | // Connect Tonkeeper to the dapp 79 | const tonConnect = new TonConnectWidget(app, connectButton) 80 | await tonConnect.connectWallet('Tonkeeper') 81 | await wallet.connect() 82 | 83 | // Verify the connected account address 84 | const accountSelector = app.locator('div[data-tc-text]') 85 | await expect(accountSelector).toHaveText('0QAy…WfyR') 86 | 87 | // Sending transactions 88 | await app.getByRole('button', { name: 'Send transaction' }).click() 89 | await wallet.accept() 90 | }) 91 | ``` 92 | 93 | ## Running Tests 94 | 95 | To run your Playwright tests with Tonkeeper: 96 | 97 | 1. Start your local development server (if testing against a local app). 98 | 99 | 2. Run the tests: 100 | 101 | ```bash 102 | npx playwright test --config playwright-test.config.ts 103 | ``` 104 | 105 | This will execute your tests using Playwright with Tonkeeper integration 106 | 107 | 108 | ## BDD 109 | ### QA 110 | 111 | Write scenarios in folder [features](features) see [Gherkin Reference](https://cucumber.io/docs/gherkin/reference/) and [Cucumber Anti-Patterns](https://www.thinkcode.se/blog/2016/06/22/cucumber-antipatterns) 112 | 113 | ### SE 114 | 115 | Describe step in folder [steps](steps) 116 | 117 | ## Develop 118 | 119 | ```shell 120 | pnpm install 121 | pnpm lint 122 | pnpm format 123 | pnpm playwright install 124 | pnpm test # simple bdd test from features/*.feature 125 | # for test with tonkeeper setup WALLET_MNEMONIC=".." in file .env 126 | pnpm tonkeeper 127 | ``` 128 | 129 | ## Techstack 130 | 131 | - [Gherkin](https://cucumber.io/docs/gherkin/) — language used for describe acceptance scenarios 132 | - [Cucumber Anti-Patterns](https://www.thinkcode.se/blog/2016/06/22/cucumber-antipatterns) 133 | - [Node.js](https://nodejs.org/) — main platform for automation implementation 134 | - [Playwright](https://playwright.dev/) + [BDD](https://vitalets.github.io/playwright-bdd/) — framework browser automation used for implementation steps uses in scenarios 135 | 136 | 137 | ## Release 138 | 139 | ```bash 140 | pnpm release [--dry-run] 141 | ``` 142 | 143 | ## TODO research 144 | 145 | - https://www.browserstack.com/ 146 | - https://vitalets.github.io/playwright-bdd/#/guides/usage-with-browserstack 147 | - https://saucelabs.com/ 148 | - https://vitalets.github.io/playwright-bdd/#/guides/usage-with-saucelabs 149 | - https://nx.dev/ 150 | - https://vitalets.github.io/playwright-bdd/#/guides/usage-with-nx 151 | - IDE integration 152 | - Intellij IDE / Aqua https://vitalets.github.io/playwright-bdd/#/guides/ide-integration?id=intellij-ide-aqua 153 | - VS Code https://vitalets.github.io/playwright-bdd/#/guides/ide-integration?id=vs-code 154 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ### Added 6 | 7 | - Provided methods for Ton Connect Widget and Tonkeeper for writing acceptance tests using Playwright 8 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This agreement is for collaboration, it may not be detailed enough, if it is not clear how to do what you want, this is a normal situation, just ask your colleagues 4 | 5 | ## Main flow 6 | 7 | ### Step 1 — get code 8 | 9 | ```shell 10 | git clone git@github.com:ton-connect/ton-connect-qa.git 11 | cd ton-connect-qa 12 | git checkout -b name-of-feature origin/main 13 | ``` 14 | 15 | ### Step 2 — write code 16 | 17 | Coding and testing local see [README.md Development](https://github.com/elemgame/elemgame.app#development) 18 | 19 | > Git history: work log vs recipe https://www.bitsnbites.eu/git-history-work-log-vs-recipe/ 20 | 21 | Use [Conventional Commits](https://www.conventionalcommits.org/) 22 | 23 | ```shell 24 | git commit --message "feat: paypal payment for different users" 25 | ``` 26 | 27 | or 28 | 29 | ```shell 30 | git commit --message "fix: hide password display when searching for a user" 31 | ``` 32 | 33 | ### Step 3 — make fork 34 | 35 | Follow by link for make fork: 36 | https://github.com/ton-connect/ton-connect-qa/fork 37 | 38 | Setup your remote 39 | 40 | ```bash 41 | git remote add self url_your_fork 42 | ``` 43 | 44 | ### Step 4 — make pull requests 45 | 46 | Push and create pull requests 47 | 48 | ```shell 49 | git push --set-upstream self name-of-feature 50 | ``` 51 | 52 | Follow by link: 53 | 54 | ```shell 55 | https://github.com/ton-connect/ton-connect-qa/pull/new/name-of-feature 56 | ``` 57 | 58 | ### Step 5 — update branch from main 59 | 60 | This step may be necessary in case your colleagues suggest additional changes after reviewing the code. 61 | 62 | > [!NOTE] 63 | > A tidy, linear Git history https://www.bitsnbites.eu/a-tidy-linear-git-history/ 64 | 65 | Get the latest upstream changes and update the working branch: 66 | 67 | ```shell 68 | git fetch --prune origin 69 | git rebase --autostash --ignore-date origin/main 70 | ``` 71 | > [!WARNING] 72 | > Please note that you get the current state of the main branch from the **origin** remote for doing push to **self** 73 | 74 | During the rebase, there may be conflicts, they need to be resolved and after the decision to continue the rebase: 75 | 76 | ```shell 77 | git rebase --continue 78 | ``` 79 | 80 | Upload the updated working branch to the repository, given that we changed the history, this should be done with the force option: 81 | 82 | ```shell 83 | git push --force --set-upstream self name-of-feature 84 | ``` 85 | 86 | More details can be found in the tutorial: [git rebase](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase) 87 | -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | WALLET_MNEMONIC: string 5 | } 6 | } 7 | } 8 | 9 | export {} 10 | -------------------------------------------------------------------------------- /features/connect-without-extension.feature: -------------------------------------------------------------------------------- 1 | Feature: Connect without extension 2 | 3 | Scenario: Try connect to wallet Tonkeeper without extension in demo dapp vue react 4 | Given I am open app "https://townsquarexyz.github.io/demo-dapp-with-vue-ui/" 5 | Then I see in title "Demo Dapp Vue UI" 6 | When I click on connect button 7 | Then I see widget with title "Connect your wallet" 8 | When I select wallet "Tonkeeper" 9 | Then I see widget with title "Tonkeeper" 10 | Then I see widget with second title "Scan the QR code below with your phone’s or Tonkeeper’s camera" 11 | When I select option "Browser Extension" 12 | Then I see widget with second title "Seems you don't have installed Tonkeeper browser extension" 13 | 14 | Scenario: Try connect to wallet Tonkeeper without extension in demo dapp with react 15 | Given I am open app "https://ton-connect.github.io/demo-dapp-with-react-ui/" 16 | Then I see in title "Demo Dapp with @tonconnect/ui-react" 17 | When I click on connect button 18 | Then I see widget with title "Connect your TON wallet" 19 | When I select wallet "Tonkeeper" 20 | Then I see widget with title "Tonkeeper" 21 | Then I see widget with second title "Scan the QR code below with your phone’s or Tonkeeper’s camera" 22 | When I select option "Browser Extension" 23 | Then I see widget with second title "Seems you don't have installed Tonkeeper browser extension" 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tonconnect/qa", 3 | "version": "1.0.0-alpha.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "exports": { 7 | "types": "./types/index.d.ts", 8 | "default": "./dist/index.js" 9 | }, 10 | "main": "./dist/index.js", 11 | "types": "./types/index.d.ts", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ton-connect/ton-connect-qa" 15 | }, 16 | "keywords": ["ton", "connect", "qa"], 17 | "author": "tonconnect", 18 | "license": "MIT", 19 | "files": [ 20 | "README.md", 21 | "changelog.md", 22 | "contributing.md", 23 | "qa", 24 | "types", 25 | "dist" 26 | ], 27 | "scripts": { 28 | "build": "rm -fr dist && tsc --declaration --project tsconfig-release.json --outDir dist", 29 | "release": "pnpm build && pnpm lint && pnpm test && pnpm release-it", 30 | "test": "npx bddgen && npx playwright test", 31 | "tonkeeper": "npx playwright test --config playwright-test.config.ts", 32 | "watch:bdd": "nodemon -w ./features -w ./steps -e feature,js,ts --exec \"npx bddgen\"", 33 | "watch:pw": "playwright test --ui", 34 | "watch": "run-p watch:*", 35 | "lint": "prettier --check test qa steps features *.ts", 36 | "format": "prettier --write test qa steps features *.ts", 37 | "report": "npx http-server ./qa-report -c-1 -o index.html" 38 | }, 39 | "publishConfig": { 40 | "access": "public", 41 | "registry": "https://registry.npmjs.org/" 42 | }, 43 | "release-it": { 44 | "github": { 45 | "release": true 46 | }, 47 | "plugins": { 48 | "@release-it/keep-a-changelog": { 49 | "filename": "changelog.md" 50 | } 51 | } 52 | }, 53 | "prettier": { 54 | "tabWidth": 2, 55 | "useTabs": false, 56 | "singleQuote": true, 57 | "printWidth": 100, 58 | "semi": false, 59 | "trailingComma": "all", 60 | "bracketSpacing": true, 61 | "plugins": [ 62 | "prettier-plugin-gherkin" 63 | ] 64 | }, 65 | "devDependencies": { 66 | "@playwright/test": "^1.50.0", 67 | "@release-it/keep-a-changelog": "^7.0.0", 68 | "@synthetixio/synpress": "^4.0.7", 69 | "@synthetixio/synpress-cache": "^0.0.7", 70 | "@synthetixio/synpress-tsconfig": "^0.0.8", 71 | "@types/fs-extra": "^11.0.4", 72 | "@types/node": "^20.9.4", 73 | "cypress": "^14.0.3", 74 | "dotenv": "^16.4.7", 75 | "fs-extra": "^11.3.0", 76 | "http-server": "14.1.1", 77 | "node": "^20.19.0", 78 | "nodemon": "^3.1.9", 79 | "npm-run-all": "^4.1.5", 80 | "playwright-bdd": "^8.1.0", 81 | "prettier": "3.4.2", 82 | "prettier-plugin-gherkin": "^3.1.1", 83 | "release-it": "^19.0.1", 84 | "typescript": "^5.7.2" 85 | }, 86 | "dependencies": { 87 | "zod": "^3.24.2" 88 | }, 89 | "pnpm": { 90 | "overrides": { 91 | "ws@>=8.0.0 <8.17.1": ">=8.17.1", 92 | "axios@>=1.3.2 <=1.7.3": ">=1.7.4", 93 | "esbuild@<=0.24.2": ">=0.25.0", 94 | "axios@>=1.0.0 <1.8.2": ">=1.8.2" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /playwright-test.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | /* eslint-disable notice/notice */ 3 | import { defineConfig, devices } from '@playwright/test' 4 | 5 | /** 6 | * See https://playwright.dev/docs/test-configuration 7 | */ 8 | export default defineConfig({ 9 | testDir: './test', 10 | 11 | /* Maximum time one test can run for */ 12 | // timeout: 30_000, 13 | 14 | expect: { 15 | /** 16 | * Maximum time expect() should wait for the condition to be met 17 | * For example in `await expect(locator).toHaveText()` 18 | */ 19 | timeout: 5_000, 20 | }, 21 | 22 | /* Fail the build on CI if you accidentally left test.only in the source code */ 23 | forbidOnly: !!process.env.CI, 24 | 25 | /* Retry on CI only */ 26 | retries: process.env.CI ? 2 : 0, 27 | 28 | /* Opt out of parallel tests on CI */ 29 | workers: process.env.CI ? 1 : undefined, 30 | 31 | /* Reporter to use, see https://playwright.dev/docs/test-reporters */ 32 | reporter: [['html'], ['list']], 33 | 34 | /* Shared settings for all the projects below, see https://playwright.dev/docs/api/class-testoptions */ 35 | use: { 36 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit) */ 37 | actionTimeout: 0, 38 | 39 | /* Base URL to use in actions like `await page.goto('/')` */ 40 | // baseURL: 'http://localhost:3000', 41 | 42 | /* Collect trace when retrying the failed test, see https://playwright.dev/docs/trace-viewer */ 43 | trace: 'on-first-retry', 44 | }, 45 | 46 | /* Configure projects for major browsers */ 47 | projects: [ 48 | { 49 | name: 'chromium', 50 | 51 | /* Project-specific settings */ 52 | use: { 53 | ...devices['Desktop Chrome'], 54 | }, 55 | }, 56 | 57 | // { 58 | // name: 'firefox', 59 | // use: { 60 | // ...devices['Desktop Firefox'], 61 | // }, 62 | // }, 63 | // 64 | // { 65 | // name: 'webkit', 66 | // use: { 67 | // ...devices['Desktop Safari'], 68 | // }, 69 | // }, 70 | 71 | /* Test against mobile viewports */ 72 | // { 73 | // name: 'Mobile Chrome', 74 | // use: { 75 | // ...devices['Pixel 5'], 76 | // }, 77 | // }, 78 | // { 79 | // name: 'Mobile Safari', 80 | // use: { 81 | // ...devices['iPhone 12'], 82 | // }, 83 | // }, 84 | 85 | /* Test against branded browsers */ 86 | // { 87 | // name: 'Microsoft Edge', 88 | // use: { 89 | // channel: 'msedge', 90 | // }, 91 | // }, 92 | // { 93 | // name: 'Google Chrome', 94 | // use: { 95 | // channel: 'chrome', 96 | // }, 97 | // }, 98 | ], 99 | 100 | /* Folder for test artifacts such as screenshots, videos, traces, etc */ 101 | // outputDir: 'test-results/', 102 | 103 | /* Run your local dev server before starting the tests */ 104 | // webServer: { 105 | // command: 'npm run start', 106 | // port: 3000, 107 | // }, 108 | }) 109 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig, devices } from '@playwright/test' 3 | import { defineBddConfig, cucumberReporter } from 'playwright-bdd' 4 | 5 | const testDir = defineBddConfig({ 6 | features: 'features/*.feature', 7 | steps: 'steps/*.ts', 8 | }) 9 | 10 | export default defineConfig({ 11 | testDir, 12 | reporter: [ 13 | cucumberReporter('html', { 14 | outputFile: 'qa-report/index.html', 15 | externalAttachments: true, 16 | attachmentsBaseURL: 'http://127.0.0.1:8080/data', 17 | }), 18 | ['html', { open: 'never' }], 19 | ], 20 | use: { 21 | screenshot: 'on', 22 | trace: 'on', 23 | }, 24 | projects: [ 25 | { 26 | name: 'chromium', 27 | use: { ...devices['Desktop Chrome'] }, 28 | }, 29 | ], 30 | }) 31 | -------------------------------------------------------------------------------- /qa/TonConnectWidget.ts: -------------------------------------------------------------------------------- 1 | import { type Locator, type Page } from '@playwright/test' 2 | 3 | export class TonConnectWidget { 4 | readonly page: Page 5 | readonly title: Locator 6 | readonly titleSecond: Locator 7 | readonly connectButton: Locator 8 | 9 | constructor(page: Page, connectButton: Locator) { 10 | this.page = page 11 | this.title = page.locator('#tc-widget-root h1') 12 | this.titleSecond = page.locator('#tc-widget-root h2') 13 | this.connectButton = connectButton 14 | } 15 | 16 | clickButton(name: string) { 17 | return this.page.getByRole('button', { name }).click() 18 | } 19 | 20 | async connectWallet(name: string) { 21 | await this.connect() 22 | await this.clickButton(name) 23 | await this.clickButton('Browser Extension') 24 | } 25 | 26 | connect() { 27 | return this.connectButton.click() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /qa/WalletExtension.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserContext } from '@playwright/test' 2 | 3 | export abstract class WalletExtension { 4 | /** 5 | * Creates an instance of Wallet 6 | * 7 | * @param context - The Playwright BrowserContext in which the extension is running 8 | * @param extensionId - The ID of the extension 9 | * @param password - The password for the Wallet 10 | */ 11 | constructor( 12 | readonly context: BrowserContext, 13 | readonly extensionId: string, 14 | readonly password: string = 'tester@1234', 15 | ) { 16 | this.context = context 17 | this.extensionId = extensionId 18 | this.password = password 19 | } 20 | 21 | /** 22 | * Imports a wallet using the given seed phrase 23 | * 24 | * @param seedPhrase - The seed phrase to import 25 | */ 26 | abstract importWallet(seedPhrase: string): Promise 27 | 28 | abstract connect(confirm?: boolean): Promise 29 | 30 | abstract accept(confirm?: boolean): Promise 31 | } 32 | -------------------------------------------------------------------------------- /qa/index.ts: -------------------------------------------------------------------------------- 1 | export { Tonkeeper } from './tonkeeper/Tonkeeper' 2 | export { TonConnectWidget } from './TonConnectWidget' 3 | export { testWith, launchPersistentContext, type WalletFixture } from './test' 4 | export { tonkeeperFixture } from './tonkeeper/tonkeeperFixture' 5 | export { tonkeeperExtension } from './tonkeeper/tonkeeperExtension' 6 | export { getExtensionId } from './util' 7 | -------------------------------------------------------------------------------- /qa/test.ts: -------------------------------------------------------------------------------- 1 | import { type BrowserContext, chromium, type Fixtures, type TestType } from '@playwright/test' 2 | import { mergeTests, test as base } from '@playwright/test' 3 | import { WalletExtension } from './WalletExtension' 4 | 5 | export function launchPersistentContext(extensionPath: string, slowMo = 0) { 6 | const browserArgs = [ 7 | `--disable-extensions-except=${extensionPath}`, 8 | `--load-extension=${extensionPath}`, 9 | ] 10 | if (process.env.HEADLESS) { 11 | browserArgs.push('--headless=new') 12 | 13 | if (slowMo > 0) { 14 | console.warn('[WARNING] Slow motion makes no sense in headless mode. It will be ignored!') 15 | } 16 | } 17 | return chromium.launchPersistentContext('', { 18 | headless: extensionPath === '', 19 | args: browserArgs, 20 | slowMo: process.env.HEADLESS ? 0 : slowMo, 21 | }) 22 | } 23 | 24 | export function testWith( 25 | customFixtures: TestType, 26 | ): TestType { 27 | return mergeTests(base, customFixtures) 28 | } 29 | 30 | export type WalletFixture = { 31 | context: BrowserContext 32 | wallet: WalletExtension 33 | } 34 | -------------------------------------------------------------------------------- /qa/tonkeeper/Tonkeeper.ts: -------------------------------------------------------------------------------- 1 | import { WalletExtension } from '../WalletExtension' 2 | 3 | /** 4 | * Tonkeeper class for interacting with the Tonkeeper extension in Playwright tests 5 | * 6 | * This class provides methods to perform various operations on the Tonkeeper extension, 7 | * such as importing wallets, switching networks, confirming transactions, and more. 8 | * 9 | * @class 10 | * @extends WalletExtension 11 | */ 12 | export class Tonkeeper extends WalletExtension { 13 | get onboardingPage() { 14 | return 'chrome-extension://' + this.extensionId + '/index.html' 15 | } 16 | 17 | async importWallet(seedPhrase: string): Promise { 18 | const pages = this.context.pages() 19 | let extension = pages[pages.length - 1] // return last tab 20 | if (!extension) { 21 | extension = await this.context.newPage() 22 | } 23 | await extension.goto(this.onboardingPage) 24 | const getStartedButton = extension.locator('#root button') 25 | await getStartedButton.click() 26 | const modalDialogSelector = '#react-portal-modal-container .dialog-content' 27 | const modalTitleSelector = `${modalDialogSelector} h2` 28 | const modalButtonSelector = `${modalDialogSelector} button` 29 | const modalFormInputSelector = (n: number) => `${modalDialogSelector} input[tabindex="${n}"]` 30 | await extension.waitForSelector(modalTitleSelector, { state: 'visible' }) 31 | const testnetAccountButton = extension.locator( 32 | '#react-portal-modal-container button:nth-child(4)', 33 | ) 34 | const [recoveryPhrasePage] = await Promise.all([ 35 | this.context.waitForEvent('page'), 36 | testnetAccountButton.click(), 37 | ]) 38 | // TODO maybe can use open app `chrome-extension://${extensionId}/index.html?add_wallet=testnet` 39 | await recoveryPhrasePage.waitForSelector(modalTitleSelector, { state: 'visible' }) 40 | const seedPhraseWords = seedPhrase.split(' ') 41 | for (const [n, word] of seedPhraseWords.entries()) { 42 | const input = recoveryPhrasePage.locator(modalFormInputSelector(n + 1)) 43 | await input.fill(word) 44 | } 45 | await recoveryPhrasePage.locator(modalButtonSelector).click() 46 | await recoveryPhrasePage.waitForSelector(modalTitleSelector, { state: 'visible' }) 47 | // TODO add wallet select it need if wallet not init 48 | await recoveryPhrasePage.locator(modalButtonSelector).click() 49 | await recoveryPhrasePage.locator('#create-password').fill(this.password) 50 | await recoveryPhrasePage.locator('#create-password-confirm').fill(this.password) 51 | await recoveryPhrasePage.getByRole('button', { name: 'Continue' }).click() 52 | await recoveryPhrasePage.getByRole('button', { name: 'Save' }).click() 53 | } 54 | 55 | async connect(confirm?: boolean): Promise { 56 | const acceptPage = await this.context.waitForEvent('page') 57 | await acceptPage.getByRole('button', { name: 'Connect wallet' }).click() 58 | await acceptPage.locator('#unlock-password').fill(this.password) 59 | if (confirm !== false) { 60 | await acceptPage.getByRole('button', { name: 'Confirm' }).click() 61 | } else { 62 | await acceptPage.getByRole('button', { name: 'Cancel' }).click() 63 | } 64 | } 65 | 66 | async accept(confirm?: boolean): Promise { 67 | const acceptPage = await this.context.waitForEvent('page') 68 | await acceptPage.getByRole('button', { name: 'Confirm' }).click() 69 | await acceptPage.locator('#unlock-password').fill(this.password) 70 | if (confirm !== false) { 71 | await acceptPage.getByRole('button', { name: 'Confirm' }).click() 72 | } else { 73 | await acceptPage.getByRole('button', { name: 'Cancel' }).click() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /qa/tonkeeper/tonkeeperExtension.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'node:path' 3 | import { downloadFile, ensureCacheDirExists, unzipArchive } from '@synthetixio/synpress-cache' 4 | 5 | export const VERSION = '3.27.4' 6 | export const PLATFORM = 'chrome' 7 | export const REPO = 'tonkeeper/tonkeeper-web' 8 | export const DOWNLOAD_URL = `https://github.com/${REPO}/releases/download/v${VERSION}/tonkeeper_${PLATFORM}_v${VERSION}.zip` 9 | 10 | export async function tonkeeperExtension(forceCache = true) { 11 | let outputDir = '' 12 | if (forceCache) { 13 | outputDir = ensureCacheDirExists() 14 | } else { 15 | outputDir = path.resolve('./', 'downloads') 16 | if (!fs.existsSync(outputDir)) { 17 | fs.mkdirSync(outputDir) 18 | } 19 | } 20 | 21 | const downloadResult = await downloadFile({ 22 | url: DOWNLOAD_URL, 23 | outputDir, 24 | fileName: `tonkeeper-${PLATFORM}-${VERSION}.zip`, 25 | }) 26 | 27 | const unzipResult = await unzipArchive({ 28 | archivePath: downloadResult.filePath, 29 | }) 30 | 31 | return unzipResult.outputPath 32 | } 33 | -------------------------------------------------------------------------------- /qa/tonkeeper/tonkeeperFixture.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test' 2 | import { type WalletFixture, launchPersistentContext } from '../test' 3 | import { getExtensionId } from '../util' 4 | import { tonkeeperExtension } from './tonkeeperExtension' 5 | import { Tonkeeper } from './Tonkeeper' 6 | 7 | export const tonkeeperFixture = (mnemonic?: string, slowMo = 0) => { 8 | return test.extend({ 9 | context: async ({ context: _ }, use) => { 10 | const tonkeeperPath = await tonkeeperExtension() 11 | const context = await launchPersistentContext(tonkeeperPath, slowMo) 12 | await use(context) 13 | await context.close() 14 | }, 15 | wallet: async ({ context }, use) => { 16 | const tonkeeper = new Tonkeeper(context, await getExtensionId(context)) 17 | if (mnemonic) { 18 | await tonkeeper.importWallet(mnemonic) 19 | } 20 | await use(tonkeeper) 21 | await context.close() 22 | }, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /qa/util.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserContext } from '@playwright/test' 2 | 3 | export async function getExtensionId(context: BrowserContext) { 4 | let [background] = context.serviceWorkers() 5 | if (!background) { 6 | background = await context.waitForEvent('serviceworker') 7 | } 8 | const extensionId = background.url().split('/')[2] 9 | if (!extensionId) { 10 | throw new Error('Can not getting extensionId') 11 | } 12 | return extensionId 13 | } 14 | -------------------------------------------------------------------------------- /steps/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { test as base, createBdd } from 'playwright-bdd' 2 | import { 3 | type WalletFixture, 4 | launchPersistentContext, 5 | tonkeeperExtension, 6 | Tonkeeper, 7 | getExtensionId, 8 | } from '../qa' 9 | 10 | export const test = base.extend({ 11 | context: async ({ $tags, context: _ }, use) => { 12 | let tonkeeperPath = '' 13 | if ($tags.includes('@tonkeeper')) { 14 | tonkeeperPath = await tonkeeperExtension() 15 | } 16 | const context = await launchPersistentContext(tonkeeperPath) 17 | await use(context) 18 | await context.close() 19 | }, 20 | wallet: async ({ $tags, context }, use) => { 21 | let tonkeeper = new Tonkeeper(context, '') 22 | if ($tags.includes('@tonkeeper')) { 23 | tonkeeper = new Tonkeeper(context, await getExtensionId(context)) 24 | const mnemonic = process.env.WALLET_MNEMONIC! 25 | await tonkeeper.importWallet(mnemonic) 26 | } 27 | await use(tonkeeper) 28 | await context.close() 29 | }, 30 | }) 31 | 32 | export const { Given, When, Then } = createBdd(test) 33 | -------------------------------------------------------------------------------- /steps/index.ts: -------------------------------------------------------------------------------- 1 | import { expect } from '@playwright/test' 2 | import { Given, When, Then } from './fixtures' 3 | import { TonConnectWidget } from '../qa/TonConnectWidget' 4 | 5 | Given('I am open app {string}', async ({ page }, appUrl: string) => { 6 | await page.goto(appUrl) 7 | }) 8 | 9 | When('I click on connect wallet {string}', async ({ page, wallet }, name: string) => { 10 | const connectButton = page.getByRole('button', { name: 'Connect wallet to send the transaction' }) 11 | await new TonConnectWidget(page, connectButton).connectWallet(name) 12 | await wallet.connect() 13 | }) 14 | 15 | When('I click on connect button', async ({ page }) => { 16 | const connectButton = page.getByRole('button', { name: 'Connect wallet to send the transaction' }) 17 | await new TonConnectWidget(page, connectButton).connect() 18 | }) 19 | 20 | When('I select wallet {string}', async ({ page }, name: string) => { 21 | const connectButton = page.getByRole('button', { name: 'Connect wallet to send the transaction' }) 22 | await new TonConnectWidget(page, connectButton).clickButton(name) 23 | }) 24 | 25 | When('I select option {string}', async ({ page }, name: string) => { 26 | const connectButton = page.getByRole('button', { name: 'Connect wallet to send the transaction' }) 27 | await new TonConnectWidget(page, connectButton).clickButton(name) 28 | }) 29 | 30 | Then('I see widget with title {string}', async ({ page }, text: string) => { 31 | const connectButton = page.getByRole('button', { name: 'Connect wallet to send the transaction' }) 32 | const widget = new TonConnectWidget(page, connectButton) 33 | await expect(widget.title).toContainText([text]) 34 | }) 35 | 36 | Then('I see account {string}', async ({ page }, text: string) => { 37 | const accountSelector = page.locator('div[data-tc-text]') 38 | await expect(accountSelector).toHaveText(text) 39 | }) 40 | 41 | Then('I see widget with second title {string}', async ({ page }, text: string) => { 42 | const connectButton = page.getByRole('button', { name: 'Connect wallet to send the transaction' }) 43 | const widget = new TonConnectWidget(page, connectButton) 44 | await expect(widget.titleSecond).toContainText([text]) 45 | }) 46 | 47 | Then('I see in title {string}', async ({ page }, text: string) => { 48 | await expect(page).toHaveTitle(new RegExp(text)) 49 | }) 50 | -------------------------------------------------------------------------------- /test/lab.spec.ts: -------------------------------------------------------------------------------- 1 | // Import necessary modules and setup 2 | import { TonConnectWidget, testWith, tonkeeperFixture } from '../qa' 3 | 4 | // Create a test instance Tonkeeper fixtures 5 | const test = testWith(tonkeeperFixture(process.env.WALLET_MNEMONIC!)) 6 | 7 | // Extract expect function from test 8 | const { expect } = test 9 | 10 | // Define a basic test case 11 | test('lab', async ({ context, wallet }) => { 12 | // Navigate to the homepage 13 | const app = await context.newPage() 14 | await app.goto('https://ton-connect.github.io/demo-dapp-with-react-ui/') 15 | 16 | // Click the connect button 17 | const connectButton = app.getByRole('button', { name: 'Connect wallet to send the transaction' }) 18 | 19 | // Connect Tonkeeper to the dapp 20 | const tonConnect = new TonConnectWidget(app, connectButton) 21 | await tonConnect.connectWallet('Tonkeeper') 22 | await wallet.connect() 23 | 24 | // Verify the connected account address 25 | const accountSelector = app.locator('div[data-tc-text]') 26 | await expect(accountSelector).toHaveText('0QAy…WfyR') 27 | 28 | // Sending transactions 29 | await app.getByRole('button', { name: 'Send transaction' }).click() 30 | await wallet.accept() 31 | }) 32 | -------------------------------------------------------------------------------- /tsconfig-release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@synthetixio/synpress-tsconfig/base.json", 3 | "compilerOptions": { 4 | "rootDir": "qa", 5 | "outDir": "types", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "declarationMap": true 9 | }, 10 | "include": ["qa"], 11 | "files": ["environment.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@synthetixio/synpress-tsconfig/base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "esModuleInterop": true, 6 | "exactOptionalPropertyTypes": false, // Allows for `undefined` in `playwright.config.ts` 7 | "types": ["cypress"], 8 | "sourceMap": false 9 | }, 10 | "include": ["playwright.config.ts", "steps", "qa"], 11 | "files": ["environment.d.ts"] 12 | } 13 | --------------------------------------------------------------------------------