-
Notifications
You must be signed in to change notification settings - Fork 41
feat: adding playwright webview support with getByWebView() locator #172
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
Merged
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
de0f07c
feat(protocol): add WebViewInfo, WebViewBridge, and WebViewSession types
gmegidish a2c68f3
feat(query-engine): add webview strategy kind
gmegidish 4ba901a
feat(core): add WebViewLocator, Page stub, and screen.getByWebView()
gmegidish 8ed5a83
feat(core): implement Page class and WebLocator stub
gmegidish afe7eaa
feat(core): Playwright-compatible DOM selector engine and WebLocator …
gmegidish 6029b75
feat(core): add PageAssertions and WebLocatorAssertions to expect()
gmegidish 09684e4
refactor(expect): WebLocatorAssertions extends LocatorAssertions
gmegidish 6ef547d
style(expect): restore multi-line inline functions for readability
gmegidish 1928a59
feat(tests): add unit tests for Page and WebLocator (step 7)
gmegidish 571a2e5
Merge origin/main into feat-adding-webview; thread stepFn through web…
gmegidish 35ebda1
feat(webview): mobilecli webView bridge, page goBack/goForward, iOS p…
gmegidish 71b4f4c
refactor(core): share step/eval helpers and drop dead WebViewSession …
gmegidish 0e08b8b
refactor(core): dedupe assertion helpers, share webview test fakes, t…
gmegidish a3983e3
fix test so it uses local mobilewright without npx
gmegidish 02bde8b
Merge remote-tracking branch 'origin/main' into feat-adding-webview
gmegidish 71c1c14
fix(core): resolve webview locators uniquely, reset regex state, hono…
gmegidish f3b1882
fix(core): scope chained web locators and reset regex state in waitFo…
gmegidish 7798f42
fix(core): assert element exists before web locator actions instead o…
gmegidish 19b32fb
test(core): add regression tests for webview resolution, regex state,…
gmegidish d876764
upgrade mobilecli
gmegidish 3b522c1
test(core): cover webview locator nth/first, chaining, and page() err…
gmegidish c95aa9f
fixed tests
gmegidish bfbf455
chore(deps): pin playwright, playwright-core and @playwright/test to …
gmegidish 182d5ec
feat(core): route webview locators and web-first assertions through p…
gmegidish becc706
docs: add webview playwright parity design specs and plans
gmegidish 7b29e69
feat(webview): run the injected engine on real devices (engine detect…
gmegidish 40fcf40
test(e2e): add on-device webview Playwright-parity conformance suite
gmegidish 5eaf547
docs: add webview conformance suite spec and plan
gmegidish 9bed6d4
feat(core): export Page and WebLocator from the umbrella package
gmegidish 0b416cd
chore: drop superpowers design docs from the branch
gmegidish 49f84ea
refactor(core): tidy webview locator and query-engine per clean-code …
gmegidish 79966c7
refactor(core): drop unsupported Page.screenshot() stub until a captu…
gmegidish 9ff58d1
fix(core): inject the webview engine after navigation settles so it s…
gmegidish 65ecb44
test(e2e): add a live-navigation webview conformance test against a r…
gmegidish 93a6dc6
chore: deleted old test
gmegidish 0eb57f6
test(e2e): run shared conformance specs under both mobilewright and p…
gmegidish cb0bfc3
fix(core): re-inject the webview engine when a page navigation drops it
gmegidish 029ef5d
test(e2e): split platform-specific tests into per-platform projects
gmegidish 1d57ef3
feat(core): make webview Page and Locator drop-in Playwright Page/Loc…
gmegidish bcb4a6d
test(e2e): drive conformance specs with Playwright's expect; skip foc…
gmegidish aa74609
feat(core): select webviews by testId and harden webview resolution
gmegidish bb2ce67
docs: add Web Views guide
gmegidish a695974
fix(core): recognize WebKit's engine-missing error so the webview eng…
gmegidish c831050
test(e2e): run e2e tests serially by dropping fullyParallel
gmegidish 685e33b
Merge remote-tracking branch 'origin/main' into fix-improving-webview
gmegidish 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 |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| --- | ||
| sidebar_position: 7 | ||
| title: Web Views | ||
| --- | ||
|
|
||
| # Web Views | ||
|
|
||
| Many mobile apps embed web content in a native **web view** (`WKWebView` on iOS, Android System WebView on Android, or a React Native web view). Mobilewright lets you drive that web content with the **same web API as [Playwright](https://playwright.dev)** — the same locators, the same actions, and the same web‑first assertions. | ||
|
|
||
| This is not a look‑alike API. Under the hood Mobilewright runs Playwright's own injected engine inside the web view, and the objects you get back (`Page`, `Locator`) **implement Playwright's interfaces**. That means a test written against `@playwright/test` can run, unchanged, against a web view on a real device. | ||
|
|
||
| ## Requirements | ||
|
|
||
| This guide is about web views **embedded inside a native app** — it is *not* a way to automate a standalone browser over the Chrome DevTools Protocol (CDP). Mobilewright attaches to the web view through the app process, so: | ||
|
|
||
| - **The app must be debuggable.** Mobilewright can only inspect and inject into a web view that the OS allows it to attach to: | ||
| - **Android** — the app must be built with `android:debuggable="true"` (a debug build). Release builds disable web view debugging. | ||
| - **iOS** — the app must carry the `get-task-allow` entitlement (a development/debug build, including Simulator builds). App Store / distribution builds do not. | ||
| - It must be a real, in-app web view (`WKWebView`, Android System WebView, or a React Native web view) — not native UI that merely looks web-like. | ||
|
|
||
| If the app isn't debuggable, `getByWebView()` won't find a web view to attach to. | ||
|
|
||
| ## Getting a page | ||
|
|
||
| From a `screen`, locate the web view and call `.page()` to attach to it: | ||
|
|
||
| ```typescript | ||
| import { test, expect } from '@mobilewright/test'; | ||
|
|
||
| test('open the in-app browser', async ({ device, screen }) => { | ||
| await device.launchApp('com.example.app'); | ||
|
|
||
| // Navigate to the screen that hosts the web view (app-specific): | ||
| await screen.getByText('Web View').tap(); | ||
|
|
||
| // Attach to the web view and get a Playwright-style Page: | ||
| const page = await screen.getByWebView().page(); | ||
|
|
||
| await page.goto('https://example.com'); | ||
| await expect(page.getByRole('heading')).toHaveText('Example Domain'); | ||
| }); | ||
| ``` | ||
|
|
||
| `screen.getByWebView()` resolves the web view in the current screen. If an app shows **more than one** web view, pick one by position with `.first()`, `.last()`, or `.nth(i)`: | ||
|
|
||
| ```typescript | ||
| const page = await screen.getByWebView().nth(1).page(); | ||
| ``` | ||
|
|
||
| Or select a specific web view by its **native testId** — the accessibility identifier on the web view element (`resource-id` on Android, e.g. a React Native `<WebView testID="checkout">`; `accessibilityIdentifier` on iOS): | ||
|
|
||
| ```typescript | ||
| const page = await screen.getByWebView({ testId: 'checkout' }).page(); | ||
| ``` | ||
|
|
||
| ## Driving the page | ||
|
|
||
| A web `Page` exposes the Playwright locator factories and navigation methods you already know: | ||
|
|
||
| ```typescript | ||
| // Locators — same builders as Playwright | ||
| page.locator('#submit'); | ||
| page.getByRole('button', { name: 'Sign in' }); | ||
| page.getByText('Welcome back'); | ||
| page.getByLabel('Email'); | ||
| page.getByPlaceholder('you@example.com'); | ||
| page.getByTestId('cart'); | ||
| page.getByAltText('Company logo'); | ||
| page.getByTitle('Close'); | ||
|
|
||
| // Navigation | ||
| await page.goto('https://example.com/login'); | ||
| await page.reload(); | ||
| await page.goBack(); | ||
| await page.goForward(); | ||
| await page.waitForLoadState('domcontentloaded'); | ||
| await page.waitForURL(/\/dashboard/); | ||
|
|
||
| const title = await page.title(); | ||
| const html = await page.content(); | ||
| const ua = await page.evaluate(() => navigator.userAgent); | ||
| ``` | ||
|
|
||
| Locators support the usual actions and queries, and they **auto-wait** just like native locators (see [Auto-waiting](./auto-waiting)): | ||
|
|
||
| ```typescript | ||
| await page.getByPlaceholder('Email').fill('user@example.com'); | ||
| await page.getByRole('button', { name: 'Sign in' }).click(); | ||
| await page.locator('#search').press('Enter'); | ||
| await page.getByText('Terms').hover(); | ||
| await page.locator('#footer-link').scrollIntoViewIfNeeded(); | ||
|
|
||
| const count = await page.getByRole('listitem').count(); | ||
| const value = await page.locator('#email').inputValue(); | ||
| ``` | ||
|
|
||
| ## Assertions | ||
|
|
||
| Web views use Playwright's **web-first assertions**, which retry until the condition holds or the timeout elapses: | ||
|
|
||
| ```typescript | ||
| await expect(page.locator('#status')).toBeVisible(); | ||
| await expect(page.getByRole('heading')).toHaveText('Dashboard'); | ||
| await expect(page.locator('input[name="email"]')).toHaveValue(/@example\.com$/); | ||
| await expect(page.getByRole('listitem')).toHaveCount(3); | ||
| await expect(page.locator('#btn')).toHaveClass(/primary/); | ||
| await expect(page).toHaveURL(/\/dashboard/); | ||
| await expect(page).toHaveTitle(/Dashboard/); | ||
| ``` | ||
|
|
||
| Both `expect` from `@mobilewright/test` and `expect` from `@playwright/test` work on web pages and locators — they route through the same injected matcher, so the results are identical. | ||
|
|
||
| ## Sharing code with Playwright | ||
|
|
||
| Because Mobilewright's web `Page` and `Locator` **implement Playwright's `Page` and `Locator`**, you can write a test body **once** and run it both on a device (Mobilewright) and in a desktop browser (Playwright) — no copy‑paste, no adapter layer. | ||
|
|
||
| The pattern is: extract the test body into a function that receives `page` and `expect` as parameters (typed against `@playwright/test`), then call it from a thin wrapper in each runner. | ||
|
|
||
| **1. The shared spec** — `specs/login.spec.ts`: | ||
|
|
||
| ```typescript | ||
| import { type Page, type Expect } from '@playwright/test'; | ||
|
|
||
| // Pure test logic. Imports nothing from a specific runner — it receives the | ||
| // page and expect, so the exact same code runs under either runtime. | ||
| export async function loginSpec(page: Page, expect: Expect): Promise<void> { | ||
| await page.getByPlaceholder('Email').fill('user@example.com'); | ||
| await page.getByPlaceholder('Password').fill('correct horse'); | ||
| await page.getByRole('button', { name: 'Sign in' }).click(); | ||
|
|
||
| await expect(page).toHaveURL(/\/dashboard/); | ||
| await expect(page.getByRole('heading')).toHaveText('Welcome'); | ||
| } | ||
| ``` | ||
|
|
||
| **2. The Mobilewright runner** — `login.test.ts` (runs on a real device's web view): | ||
|
|
||
| ```typescript | ||
| import { test } from '@mobilewright/test'; | ||
| import { expect } from '@playwright/test'; | ||
| import { loginSpec } from './specs/login.spec'; | ||
|
|
||
| test('login works in the app web view', async ({ device, screen }) => { | ||
| await device.launchApp('com.example.app'); | ||
| await screen.getByText('Web View').tap(); | ||
|
|
||
| const page = await screen.getByWebView().page(); | ||
| await page.goto('https://example.com/login'); | ||
|
|
||
| await loginSpec(page, expect); | ||
| }); | ||
| ``` | ||
|
|
||
| **3. The Playwright runner** — `login.pw.ts` (runs in a desktop browser): | ||
|
|
||
| ```typescript | ||
| import { test, expect } from '@playwright/test'; | ||
| import { loginSpec } from './specs/login.spec'; | ||
|
|
||
| test('login works in the browser', async ({ page }) => { | ||
| await page.goto('https://example.com/login'); | ||
| await loginSpec(page, expect); | ||
| }); | ||
| ``` | ||
|
|
||
| The body in `loginSpec` is identical for both. The only per-runtime code is *how the `page` is obtained* — from a launched app's web view on device, or from Playwright's `page` fixture in the browser. This makes Playwright a useful **parity oracle**: if a spec passes in the browser but fails on device, your app behaves differently there. | ||
|
|
||
| :::tip | ||
| Keep the runners apart with file naming. Point Mobilewright at `*.test.ts` and Playwright at `*.pw.ts` (via `testMatch` in each config) so neither runner picks up the other's wrapper, and the shared `*.spec.ts` files are imported by both but run by neither. | ||
| ::: | ||
|
|
||
| ## How it works | ||
|
|
||
| When you attach to a web view, Mobilewright injects Playwright's own selector-and-assertion engine into the page. Locators are resolved in-page by that engine, and every web-first assertion runs Playwright's matcher inside the web view — so selector semantics, whitespace normalization, and matcher behavior match Playwright exactly. | ||
|
|
||
| The engine is re-injected automatically after navigations (a fresh document drops it), including page-initiated redirects, so your locators keep working across `goto`, `reload`, and in-page navigation. | ||
|
|
||
| ## Supported API and limitations | ||
|
|
||
| The web-first surface for driving content is supported: navigation, the `getBy*` locators, actions (`click`, `fill`, `type`, `press`, `hover`, `focus`, `scrollIntoViewIfNeeded`), value/state queries, and the web-first `expect` matchers (`toBeVisible`, `toHaveText`, `toHaveValue`, `toHaveCount`, `toHaveAttribute`, `toHaveClass`, `toHaveCSS`, `toHaveId`, `toHaveJSProperty`, `toBeChecked`, `toBeEnabled`, `toBeEditable`, `toHaveURL`, `toHaveTitle`, …). | ||
|
|
||
| Some Playwright capabilities have no equivalent inside an embedded web view and will throw if called: | ||
|
|
||
| - Network interception (`page.route`, `routeFromHAR`) | ||
| - Screenshots / visual snapshots (`page.screenshot`, `toHaveScreenshot`) | ||
| - Dialogs, downloads, file choosers, and multiple tabs / popups | ||
| - `page.pdf`, `page.addInitScript`, browser contexts | ||
|
|
||
| ### Platform notes | ||
|
|
||
| - **`page.url()` is synchronous** (matching Playwright) and returns the last URL from a navigation Mobilewright drove. After a link click or in-app redirect it can lag — use `expect(page).toHaveURL(...)` or `page.waitForURL(...)` for live, auto-waiting checks. | ||
| - **`toBeFocused()` is not reliable on Android.** The embedded Android System WebView ignores programmatic focus without renderer focus emulation (a capability that isn't available there), so `document.activeElement` doesn't update. Focus assertions work on iOS and in desktop Chromium. |
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
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,17 @@ | ||
| import { defineConfig, devices } from '@playwright/test'; | ||
|
|
||
| // Runs the shared conformance specs (src/conformance/specs) against a real | ||
| // browser, as the parity oracle for the mobilewright on-device runner. Only | ||
| // picks up *.pw.ts so it never collides with the mobilewright *.test.ts files. | ||
| export default defineConfig({ | ||
| testDir: './src', | ||
| testMatch: '**/*.pw.ts', | ||
| timeout: 60_000, | ||
| fullyParallel: true, | ||
| projects: [ | ||
| { | ||
| name: 'chromium', | ||
| use: { ...devices['Desktop Chrome'] }, | ||
| }, | ||
| ], | ||
| }); |
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,11 @@ | ||
| import { test, expect } from '@playwright/test'; | ||
| import { conformanceSpecs } from './specs/index.js'; | ||
|
|
||
| // Playwright runner: drive the exact same conformance specs against a real | ||
| // browser. This is the parity oracle — if a spec passes here but fails under | ||
| // mobilewright (conformance.test.ts), mobilewright diverges from Playwright. | ||
| for (const spec of conformanceSpecs) { | ||
| test(spec.name, async ({ page }) => { | ||
| await spec.run(page, expect); | ||
| }); | ||
| } |
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,15 @@ | ||
| import { test } from '@mobilewright/test'; | ||
| import { expect } from '@playwright/test'; | ||
| import { openWebviewPage } from './harness.js'; | ||
| import { conformanceSpecs } from './specs/index.js'; | ||
|
|
||
| // mobilewright runner: drive the shared conformance specs against a real | ||
| // on-device webview, using Playwright's own expect (which the MobileWebViewPage / | ||
| // MobileWebViewLocator satisfy). Each spec body lives in ./specs and also runs | ||
| // under Playwright via conformance.pw.ts — same files, two runtimes. | ||
| for (const spec of conformanceSpecs) { | ||
| test(spec.name, async ({ device, screen }) => { | ||
| const page = await openWebviewPage({ device, screen }); | ||
| await spec.run(page, expect); | ||
| }); | ||
| } | ||
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,21 @@ | ||
| import type { Device, Screen, Page } from 'mobilewright'; | ||
|
|
||
| const PLAYGROUND_APP = 'com.mobilenext.playground'; | ||
|
|
||
| function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
|
|
||
| // Launch the Playground app, open its WebView screen, and return the web Page. | ||
| // All conformance tests start from the Page this returns. | ||
| export async function openWebviewPage(ctx: { device: Device; screen: Screen }): Promise<Page> { | ||
| await ctx.device.terminateApp(PLAYGROUND_APP).catch(() => {}); | ||
| await ctx.device.launchApp(PLAYGROUND_APP); | ||
| // Android's foreground detection races right after launch (a known mobilecli | ||
| // flake), which makes the subsequent webview list fail; let it settle. | ||
| await sleep(2000); | ||
| const webviewButton = ctx.screen.getByText('Web View'); | ||
| await webviewButton.tap(); | ||
| const page = await ctx.screen.getByWebView().page(); | ||
| return page; | ||
| } |
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,49 @@ | ||
| import type { Page, Expect } from '@playwright/test'; | ||
| import { pageWithBody, isAndroidWebView } from './fixtures.js'; | ||
|
|
||
| export const actionsSpec = async (page: Page, expect: Expect): Promise<void> => { | ||
| await page.goto(pageWithBody(` | ||
| <button id="b" onclick="this.textContent='clicked'">press me</button> | ||
| <input id="fill" type="text"> | ||
| <input id="type" type="text"> | ||
| <input id="key" type="text" onkeydown="this.value='key:'+event.key"> | ||
| <input id="focusable" type="text"> | ||
| <div id="hovered">idle</div> | ||
| <button id="hover" onmouseover="document.getElementById('hovered').textContent='hovered'">hover me</button> | ||
| <div style="height:2000px"></div> | ||
| <button id="bottom">bottom</button> | ||
| `)); | ||
|
|
||
| // click | ||
| await page.locator('#b').click(); | ||
| await expect(page.locator('#b')).toHaveText('clicked'); | ||
|
|
||
| // fill | ||
| await page.locator('#fill').fill('hello@example.com'); | ||
| await expect(page.locator('#fill')).toHaveValue('hello@example.com'); | ||
|
|
||
| // type (appends) | ||
| await page.locator('#type').type('abc'); | ||
| await expect(page.locator('#type')).toHaveValue('abc'); | ||
|
|
||
| // press | ||
| await page.locator('#key').press('Enter'); | ||
| await expect(page.locator('#key')).toHaveValue('key:Enter'); | ||
|
|
||
| // focus — Android System WebView ignores programmatic el.focus() without | ||
| // renderer focus emulation (a CDP-only capability we don't have on Android), | ||
| // so activeElement never updates. Known gap: skip the focus assertion there. | ||
| const onAndroid = await isAndroidWebView(page); | ||
| if (!onAndroid) { | ||
| await page.locator('#focusable').focus(); | ||
| await expect(page.locator('#focusable')).toBeFocused(); | ||
| } | ||
|
|
||
| // hover | ||
| await page.locator('#hover').hover(); | ||
| await expect(page.locator('#hovered')).toHaveText('hovered'); | ||
|
|
||
| // scrollIntoViewIfNeeded — no throw, element becomes in viewport | ||
| await page.locator('#bottom').scrollIntoViewIfNeeded(); | ||
| await expect(page.locator('#bottom')).toBeInViewport(); | ||
| }; |
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,37 @@ | ||
| import type { Page, Expect } from '@playwright/test'; | ||
| import { pageWithBody } from './fixtures.js'; | ||
|
|
||
| export const stateAssertionsSpec = async (page: Page, expect: Expect): Promise<void> => { | ||
| await page.goto(pageWithBody(` | ||
| <div id="visible">shown</div> | ||
| <div id="hidden" style="display:none">gone</div> | ||
| <button id="enabled">ok</button> | ||
| <button id="disabled" disabled>no</button> | ||
| <input id="editable" type="text"> | ||
| <input id="readonly" type="text" readonly> | ||
| <input id="checkbox" type="checkbox" checked> | ||
| <input id="empty" type="text" value=""> | ||
| `)); | ||
|
|
||
| await expect(page.locator('#visible')).toBeVisible(); | ||
| await expect(page.locator('#hidden')).not.toBeVisible(); | ||
| await expect(page.locator('#hidden')).toBeHidden(); | ||
| await expect(page.locator('#visible')).not.toBeHidden(); | ||
|
|
||
| await expect(page.locator('#enabled')).toBeEnabled(); | ||
| await expect(page.locator('#disabled')).toBeDisabled(); | ||
| await expect(page.locator('#disabled')).not.toBeEnabled(); | ||
|
|
||
| await expect(page.locator('#editable')).toBeEditable(); | ||
| await expect(page.locator('#readonly')).not.toBeEditable(); | ||
|
|
||
| await expect(page.locator('#checkbox')).toBeChecked(); | ||
|
|
||
| await expect(page.locator('#visible')).toBeAttached(); | ||
| await expect(page.locator('#missing')).not.toBeAttached(); | ||
|
|
||
| await expect(page.locator('#empty')).toBeEmpty(); | ||
| await expect(page.locator('#visible')).not.toBeEmpty(); | ||
|
|
||
| await expect(page.locator('#visible')).toBeInViewport(); | ||
| }; |
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.
Uh oh!
There was an error while loading. Please reload this page.