-
Notifications
You must be signed in to change notification settings - Fork 0
add testing packages #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,5 +23,7 @@ node_modules | |
| # outputs | ||
| dist/ | ||
| dist-ssr/ | ||
| dist-testing/ | ||
| .tanstack/ | ||
| .wvb/ | ||
| .wrangler/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,3 +14,5 @@ supportedArchitectures: | |
| - darwin | ||
| - linux | ||
| - win32 | ||
|
|
||
| npmMinimalAgeGate: 0 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # `@wvb-playground/testing` | ||
|
|
||
| A tiny, platform-agnostic **`WebviewDriver`** abstraction for end-to-end testing | ||
| webviews, plus ready-made implementations for each platform's automation tool. | ||
|
|
||
| Test suites are written once against `WebviewDriver`; each platform supplies a | ||
| concrete driver. App-specific suites (e.g. | ||
| [`@wvb-playground/webview-hacker-news/testing`](../../webviews/hacker-news/testing)) | ||
| provide the *cases*; this package provides the *driver*. | ||
|
|
||
| ## Exports | ||
|
|
||
| | Import | What | | ||
| | --- | --- | | ||
| | `@wvb-playground/testing` | The `WebviewDriver` interface + `DriverOptions`/`WaitOptions` types | | ||
| | `@wvb-playground/testing/playwright` | `createPlaywrightDriver(page, options)` — Electron | | ||
| | `@wvb-playground/testing/selenium` | `createSeleniumDriver(wd, options)` — Tauri (`tauri-driver`) | | ||
| | `@wvb-playground/testing/appium` | `createAppiumDriver(browser, options)` — Android / iOS | | ||
|
|
||
| Each automation library (`playwright-core`, `selenium-webdriver`, `webdriverio`) | ||
| is an **optional peer dependency** — install only the one your platform needs. | ||
|
|
||
| ## The interface | ||
|
|
||
| ```ts | ||
| interface WebviewDriver { | ||
| goto(path: string): Promise<void>; // full load of an in-app path | ||
| location(): Promise<string>; // pathname + search | ||
| click(selector: string): Promise<void>; | ||
| fill(selector: string, value: string): Promise<void>; | ||
| text(selector: string): Promise<string>; | ||
| getAttribute(selector: string, name: string): Promise<string | null>; | ||
| count(selector: string): Promise<number>; | ||
| isVisible(selector: string): Promise<boolean>; | ||
| waitForVisible(selector: string, options?: { timeoutMs?: number }): Promise<void>; | ||
| waitForHidden(selector: string, options?: { timeoutMs?: number }): Promise<void>; | ||
| } | ||
| ``` | ||
|
|
||
| `selector` is a CSS selector against the live webview DOM. Single-element | ||
| operations act on the **first visible match**, so a test id present in both the | ||
| desktop and mobile chrome resolves to whichever layout is on screen — suites stay | ||
| viewport-agnostic. `DriverOptions` carries `{ baseURL, defaultTimeoutMs? }`. | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Electron — Playwright | ||
|
|
||
| ```ts | ||
| import { _electron as electron } from '@playwright/test'; | ||
| import { createPlaywrightDriver } from '@wvb-playground/testing/playwright'; | ||
|
|
||
| const app = await electron.launch({ args: [mainJs] }); | ||
| const page = await app.firstWindow(); | ||
| const driver = createPlaywrightDriver(page, { baseURL: 'app://news.wvb.dev' }); | ||
| ``` | ||
|
|
||
| ### Tauri — Selenium (`tauri-driver`) | ||
|
|
||
| ```ts | ||
| import { Builder } from 'selenium-webdriver'; | ||
| import { createSeleniumDriver } from '@wvb-playground/testing/selenium'; | ||
|
|
||
| const wd = await new Builder().usingServer('http://127.0.0.1:4444').build(); | ||
| const driver = createSeleniumDriver(wd, { baseURL: 'tauri://localhost' }); | ||
| ``` | ||
|
|
||
| ### Android / iOS — Appium (WebdriverIO) | ||
|
|
||
| ```ts | ||
| import { remote } from 'webdriverio'; | ||
| import { createAppiumDriver } from '@wvb-playground/testing/appium'; | ||
|
|
||
| const browser = await remote({ capabilities: { /* … */ } }); | ||
| await browser.switchContext('WEBVIEW_…'); // CSS selectors need the webview context | ||
| const driver = createAppiumDriver(browser, { baseURL: 'app://news.wvb.dev' }); | ||
| ``` | ||
|
|
||
| ### Running an app-specific suite | ||
|
|
||
| ```ts | ||
| import { test } from 'vitest'; | ||
| import { cases } from '@wvb-playground/webview-hacker-news/testing'; | ||
|
|
||
| const driver = createPlaywrightDriver(page, { baseURL }); | ||
| for (const c of cases) test(c.name, () => c.run(driver)); | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| { | ||
| "name": "@wvb-playground/testing", | ||
| "version": "0.0.0", | ||
| "description": "Platform-agnostic WebviewDriver abstraction plus Playwright/Selenium/Appium implementations for webview E2E testing.", | ||
| "type": "module", | ||
| "license": "MIT", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.mts", | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
| "./playwright": { | ||
| "types": "./dist/playwright.d.mts", | ||
| "default": "./dist/playwright.mjs" | ||
| }, | ||
| "./selenium": { | ||
| "types": "./dist/selenium.d.mts", | ||
| "default": "./dist/selenium.mjs" | ||
| }, | ||
| "./appium": { | ||
| "types": "./dist/appium.d.mts", | ||
| "default": "./dist/appium.mjs" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "files": [ | ||
| "README.md", | ||
| "dist" | ||
| ], | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsdown", | ||
| "prepack": "tsdown", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "peerDependencies": { | ||
| "playwright-core": "^1.40.0", | ||
| "selenium-webdriver": "^4.0.0", | ||
| "webdriverio": "^8.0.0 || ^9.0.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "playwright-core": { | ||
| "optional": true | ||
| }, | ||
| "selenium-webdriver": { | ||
| "optional": true | ||
| }, | ||
| "webdriverio": { | ||
| "optional": true | ||
| } | ||
| }, | ||
| "devDependencies": { | ||
| "@types/selenium-webdriver": "^4.1.28", | ||
| "playwright-core": "^1.49.0", | ||
| "selenium-webdriver": "^4.27.0", | ||
| "tsdown": "0.22.2", | ||
| "typescript": "6.0.3", | ||
| "webdriverio": "^9.0.0" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import type { Browser } from 'webdriverio'; | ||
| import type { DriverOptions, WaitOptions, WebviewDriver } from './driver'; | ||
| import { joinUrl } from './internal'; | ||
|
|
||
| /** | ||
| * A {@link WebviewDriver} backed by an Appium session driven through WebdriverIO, | ||
| * for Android and iOS. The `browser` must already be switched into the WEBVIEW | ||
| * context (`browser.switchContext('WEBVIEW_…')`) so CSS selectors hit the DOM. | ||
| * `webdriverio` is an optional peer dependency. | ||
| * | ||
| * ```ts | ||
| * import { remote } from 'webdriverio'; | ||
| * import { createAppiumDriver } from '@wvb-playground/testing/appium'; | ||
| * | ||
| * const browser = await remote({ capabilities: { ... } }); | ||
| * // … switch into the webview context … | ||
| * const driver = createAppiumDriver(browser, { baseURL: 'app://news.wvb.dev' }); | ||
| * ``` | ||
| */ | ||
| export function createAppiumDriver(browser: Browser, options: DriverOptions): WebviewDriver { | ||
| const { baseURL, defaultTimeoutMs = 10_000 } = options; | ||
| const timeout = (opts?: WaitOptions) => opts?.timeoutMs ?? defaultTimeoutMs; | ||
|
|
||
| async function firstVisible(selector: string): Promise<WebdriverIO.Element | null> { | ||
| for (const el of await browser.$$(selector)) { | ||
| if (await el.isDisplayed()) return el; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| async function requireVisible(selector: string, timeoutMs: number): Promise<WebdriverIO.Element> { | ||
| let found: WebdriverIO.Element | null = null; | ||
| await browser.waitUntil( | ||
| async () => { | ||
| found = await firstVisible(selector); | ||
| return found !== null; | ||
| }, | ||
| { timeout: timeoutMs, timeoutMsg: `element never became visible: ${selector}` } | ||
| ); | ||
| if (!found) throw new Error(`element never became visible: ${selector}`); | ||
| return found; | ||
| } | ||
|
|
||
| return { | ||
| async goto(path) { | ||
| await browser.url(joinUrl(baseURL, path)); | ||
| }, | ||
| location: () => browser.execute(() => location.pathname + location.search), | ||
| async click(selector) { | ||
| await (await requireVisible(selector, defaultTimeoutMs)).click(); | ||
| }, | ||
| async fill(selector, value) { | ||
| const el = await requireVisible(selector, defaultTimeoutMs); | ||
| await el.clearValue(); | ||
| await el.setValue(value); | ||
| }, | ||
| async text(selector) { | ||
| return (await (await requireVisible(selector, defaultTimeoutMs)).getText()).trim(); | ||
| }, | ||
| async getAttribute(selector, name) { | ||
| const el = await firstVisible(selector); | ||
| return el ? el.getAttribute(name) : null; | ||
| }, | ||
| async count(selector) { | ||
| return (await browser.$$(selector)).length; | ||
| }, | ||
| async isVisible(selector) { | ||
| return (await firstVisible(selector)) !== null; | ||
| }, | ||
| async waitForVisible(selector, opts) { | ||
| await requireVisible(selector, timeout(opts)); | ||
| }, | ||
| async waitForHidden(selector, opts) { | ||
| await browser.waitUntil(async () => (await firstVisible(selector)) === null, { | ||
| timeout: timeout(opts), | ||
| timeoutMsg: `element never hid: ${selector}`, | ||
| }); | ||
| }, | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| export interface WaitOptions { | ||
| /** Maximum time to wait, in milliseconds. */ | ||
| timeoutMs?: number; | ||
| } | ||
|
|
||
| /** | ||
| * The minimal, platform-agnostic surface that drives a webview during E2E tests. | ||
| * Test suites are written against this interface; a per-platform implementation | ||
| * (see `@wvb-playground/testing/{playwright,selenium,appium}`) maps it onto a | ||
| * concrete automation tool: | ||
| * | ||
| * - Electron → Playwright (`page.locator(...)`) | ||
| * - Tauri → Selenium WebDriver (`driver.findElement(By.css(...))`) | ||
| * - Android → Appium / WebdriverIO in the WEBVIEW context (`$(...)`) | ||
| * - iOS → Appium / WebdriverIO in the WEBVIEW context (`$(...)`) | ||
| * | ||
| * Selector semantics | ||
| * ------------------ | ||
| * `selector` is a CSS selector evaluated against the live webview DOM. All | ||
| * single-element operations (`click`, `fill`, `text`, `getAttribute`, | ||
| * `waitForVisible`) act on the **first visible match**. This lets a test id that | ||
| * appears in both the desktop and the mobile chrome (e.g. a theme toggle or a | ||
| * navigation link) resolve to whichever layout is currently on screen, so suites | ||
| * stay viewport-agnostic. | ||
| * | ||
| * Failure semantics | ||
| * ----------------- | ||
| * Operations should reject if the target cannot be found/actioned within the | ||
| * host's timeout. The wait helpers reject on timeout. Rejected promises fail the | ||
| * surrounding test. | ||
| */ | ||
| export interface WebviewDriver { | ||
| /** | ||
| * Load the app at an in-app path with a full document load (resetting client | ||
| * state), e.g. `"/"`, `"/post/3"`, `"/u/byte_poet"`. The implementation resolves | ||
| * the path against a configured base URL (an `app://`/`tauri://` scheme, a dev | ||
| * server, a `file://` bundle, …). | ||
| */ | ||
| goto(path: string): Promise<void>; | ||
|
|
||
| /** The current in-app location as `pathname + search`, e.g. `"/post/3?tag=core"`. */ | ||
| location(): Promise<string>; | ||
|
|
||
| /** Click the first visible element matching `selector`. */ | ||
| click(selector: string): Promise<void>; | ||
|
|
||
| /** Replace the value of the first visible `<input>`/`<textarea>` matching `selector`. */ | ||
| fill(selector: string, value: string): Promise<void>; | ||
|
|
||
| /** Trimmed text content of the first visible element matching `selector`. */ | ||
| text(selector: string): Promise<string>; | ||
|
|
||
| /** Value of `name` on the first visible element matching `selector`, or `null`. */ | ||
| getAttribute(selector: string, name: string): Promise<string | null>; | ||
|
|
||
| /** Number of elements in the DOM matching `selector`. */ | ||
| count(selector: string): Promise<number>; | ||
|
|
||
| /** Whether at least one element matching `selector` is visible. */ | ||
| isVisible(selector: string): Promise<boolean>; | ||
|
|
||
| /** Resolve once an element matching `selector` becomes visible; reject on timeout. */ | ||
| waitForVisible(selector: string, options?: WaitOptions): Promise<void>; | ||
|
|
||
| /** Resolve once no element matching `selector` is visible; reject on timeout. */ | ||
| waitForHidden(selector: string, options?: WaitOptions): Promise<void>; | ||
| } | ||
|
|
||
| /** Common options shared by the per-platform driver factories. */ | ||
| export interface DriverOptions { | ||
| /** | ||
| * Base URL that in-app paths passed to {@link WebviewDriver.goto} are resolved | ||
| * against, e.g. `"http://localhost:4173"` or `"app://news.wvb.dev"`. | ||
| */ | ||
| baseURL: string; | ||
| /** Default timeout (ms) for waits when a call doesn't pass one. Defaults to 10000. */ | ||
| defaultTimeoutMs?: number; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export type { DriverOptions, WaitOptions, WebviewDriver } from './driver'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| /** Join a base URL with an in-app path, tolerant of custom schemes (`app://…`). */ | ||
| export function joinUrl(baseURL: string, path: string): string { | ||
| return baseURL.replace(/\/+$/, '') + (path.startsWith('/') ? path : `/${path}`); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||
| import type { Page } from 'playwright-core'; | ||||||
| import type { DriverOptions, WaitOptions, WebviewDriver } from './driver'; | ||||||
| import { joinUrl } from './internal'; | ||||||
|
|
||||||
| /** | ||||||
| * A {@link WebviewDriver} backed by a Playwright `Page` — for Electron (via the | ||||||
| * `_electron` API, passing `app.firstWindow()`) or any Chromium/WebKit/Firefox | ||||||
| * page. `playwright-core` is an optional peer dependency. | ||||||
| * | ||||||
| * ```ts | ||||||
| * import { _electron as electron } from '@playwright/test'; | ||||||
| * import { createPlaywrightDriver } from '@wvb-playground/testing/playwright'; | ||||||
| * | ||||||
| * const app = await electron.launch({ args: [main] }); | ||||||
| * const driver = createPlaywrightDriver(await app.firstWindow(), { baseURL: 'app://news.wvb.dev' }); | ||||||
| * ``` | ||||||
| */ | ||||||
| export function createPlaywrightDriver(page: Page, options: DriverOptions): WebviewDriver { | ||||||
| const { baseURL, defaultTimeoutMs } = options; | ||||||
| // `:visible` resolves the first *visible* match — important when a test id | ||||||
| // exists in both the desktop and mobile chrome. | ||||||
| const visible = (selector: string) => page.locator(`${selector}:visible`).first(); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Compound CSS selectors (comma-separated) break under Prompt for AI agents
Suggested change
|
||||||
| const timeout = (opts?: WaitOptions) => opts?.timeoutMs ?? defaultTimeoutMs; | ||||||
|
|
||||||
| return { | ||||||
| async goto(path) { | ||||||
| await page.goto(joinUrl(baseURL, path)); | ||||||
| }, | ||||||
| location: () => page.evaluate(() => location.pathname + location.search), | ||||||
| async click(selector) { | ||||||
| await visible(selector).click({ timeout: defaultTimeoutMs }); | ||||||
| }, | ||||||
| async fill(selector, value) { | ||||||
| await visible(selector).fill(value, { timeout: defaultTimeoutMs }); | ||||||
| }, | ||||||
| async text(selector) { | ||||||
| const value = await visible(selector).innerText({ timeout: defaultTimeoutMs }); | ||||||
| return value.trim(); | ||||||
| }, | ||||||
| getAttribute: (selector, name) => | ||||||
| visible(selector).getAttribute(name, { timeout: defaultTimeoutMs }), | ||||||
| count: selector => page.locator(selector).count(), | ||||||
| async isVisible(selector) { | ||||||
| return (await page.locator(`${selector}:visible`).count()) > 0; | ||||||
| }, | ||||||
| async waitForVisible(selector, opts) { | ||||||
| await visible(selector).waitFor({ state: 'visible', timeout: timeout(opts) }); | ||||||
| }, | ||||||
| async waitForHidden(selector, opts) { | ||||||
| await page | ||||||
| .locator(selector) | ||||||
| .first() | ||||||
| .waitFor({ state: 'hidden', timeout: timeout(opts) }); | ||||||
| }, | ||||||
| }; | ||||||
| } | ||||||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
@types/selenium-webdriveris redundant —selenium-webdriverv4+ bundles its own TypeScript types. This can cause type conflicts if the@typespackage version drifts from the actualselenium-webdriverversion.Prompt for AI agents