From f78302e8dd62749328ba39fcdac6a53c50ba6ed5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 28 Sep 2021 17:11:04 -0700 Subject: [PATCH] fix(toBeHidden): return true to missing elements (#9205) --- src/server/frames.ts | 34 ++++--- src/test/matchers/toMatchText.ts | 3 +- .../playwright.expect.true.spec.ts | 92 +++++++++++++++++++ 3 files changed, 116 insertions(+), 13 deletions(-) diff --git a/src/server/frames.ts b/src/server/frames.ts index dd23064898..0a9f38aa79 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -70,7 +70,7 @@ export type NavigationEvent = { }; export type SchedulableTask = (injectedScript: js.JSHandle) => Promise>>; -export type DomTaskBody = (progress: InjectedScriptProgress, element: Element, data: T, elements: Element[], continuePolling: any) => R; +export type DomTaskBody = (progress: InjectedScriptProgress, element: E, data: T, elements: Element[], continuePolling: any) => R; export class FrameManager { private _page: Page; @@ -1165,9 +1165,18 @@ export class Frame extends SdkObject { const controller = new ProgressController(metadata, this); const querySelectorAll = options.expression === 'to.have.count' || options.expression.endsWith('.array'); const mainWorld = options.expression === 'to.have.property'; + const omitAttached = (!options.isNot && options.expression === 'to.be.hidden') || (options.isNot && options.expression === 'to.be.visible'); + return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, options, elements, continuePolling) => { - return progress.injectedScript.expect(progress, element, options, elements, continuePolling); - }, options, { strict: true, querySelectorAll, mainWorld, logScale: true, ...options }).catch(e => { + // We don't have an element and we don't need an element => pass. + if (!element && options.omitAttached) + return { pass: !options.isNot }; + // We don't have an element and we DO need an element => fail. + if (!element) + return { pass: !!options.isNot }; + // We have an element. + return progress.injectedScript.expect(progress, element!, options, elements, continuePolling); + }, { omitAttached, ...options }, { strict: true, querySelectorAll, mainWorld, omitAttached, logScale: true, ...options }).catch(e => { if (js.isJavaScriptErrorInEvaluate(e)) throw e; return { received: controller.lastIntermediateResult(), pass: !!options.isNot, log: metadata.log }; @@ -1231,17 +1240,17 @@ export class Frame extends SdkObject { this._parentFrame = null; } - private async _scheduleRerunnableTask(metadata: CallMetadata, selector: string, body: DomTaskBody, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise { + private async _scheduleRerunnableTask(metadata: CallMetadata, selector: string, body: DomTaskBody, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise { const controller = new ProgressController(metadata, this); - return this._scheduleRerunnableTaskWithController(controller, selector, body, taskData, options); + return this._scheduleRerunnableTaskWithController(controller, selector, body as DomTaskBody, taskData, options); } private async _scheduleRerunnableTaskWithController( controller: ProgressController, selector: string, - body: DomTaskBody, + body: DomTaskBody, taskData: T, - options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean, querySelectorAll?: boolean, logScale?: boolean } = {}): Promise { + options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean, querySelectorAll?: boolean, logScale?: boolean, omitAttached?: boolean } = {}): Promise { const info = this._page.parseSelector(selector, options); const callbackText = body.toString(); @@ -1250,8 +1259,8 @@ export class Frame extends SdkObject { return controller.run(async progress => { progress.log(`waiting for selector "${selector}"`); const rerunnableTask = new RerunnableTask(data, progress, injectedScript => { - return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale }) => { - const callback = injected.eval(callbackText) as DomTaskBody; + return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached }) => { + const callback = injected.eval(callbackText) as DomTaskBody; const poller = logScale ? injected.pollLogScale.bind(injected) : injected.pollRaf.bind(injected); return poller((progress, continuePolling) => { if (querySelectorAll) { @@ -1261,12 +1270,13 @@ export class Frame extends SdkObject { } const element = injected.querySelector(info.parsed, document, info.strict); - if (!element) + if (!element && !omitAttached) return continuePolling; - progress.logRepeating(` selector resolved to ${injected.previewNode(element)}`); + if (element) + progress.logRepeating(` selector resolved to ${injected.previewNode(element)}`); return callback(progress, element, taskData as T, [], continuePolling); }); - }, { info, taskData, callbackText, querySelectorAll: options.querySelectorAll, logScale: options.logScale }); + }, { info, taskData, callbackText, querySelectorAll: options.querySelectorAll, logScale: options.logScale, omitAttached: options.omitAttached }); }, true); if (this._detached) diff --git a/src/test/matchers/toMatchText.ts b/src/test/matchers/toMatchText.ts index a7280c5f2b..5106b3ace1 100644 --- a/src/test/matchers/toMatchText.ts +++ b/src/test/matchers/toMatchText.ts @@ -18,6 +18,7 @@ import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from 'expect/build/print'; +import colors from 'colors/safe'; import { ExpectedTextValue } from '../../protocol/channels'; import { isRegExp, isString } from '../../utils/utils'; import { currentTestInfo } from '../globals'; @@ -122,6 +123,6 @@ export function callLogText(log: string[] | undefined): string { return ` Call log: -${(log || []).join('\n')} + - ${colors.dim((log || []).join('\n - '))} `; } diff --git a/tests/playwright-test/playwright.expect.true.spec.ts b/tests/playwright-test/playwright.expect.true.spec.ts index fc37aef836..2539776148 100644 --- a/tests/playwright-test/playwright.expect.true.spec.ts +++ b/tests/playwright-test/playwright.expect.true.spec.ts @@ -137,6 +137,12 @@ test('should support toBeVisible, toBeHidden', async ({ runInlineTest }) => { await expect(locator).toBeHidden(); }); + test('was hidden', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('button'); + await expect(locator).toBeHidden(); + }); + test('not hidden', async ({ page }) => { await page.setContent(''); const locator = page.locator('input'); @@ -144,10 +150,96 @@ test('should support toBeVisible, toBeHidden', async ({ runInlineTest }) => { }); `, }, { workers: 1 }); + expect(result.passed).toBe(5); + expect(result.exitCode).toBe(0); +}); + +test('should support toBeVisible, toBeHidden wait', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + + test('visible', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('span'); + setTimeout(() => { + page.$eval('div', div => div.innerHTML = 'Hello').catch(() => {}); + }, 0); + await expect(locator).toBeVisible(); + }); + + test('not hidden', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('span'); + setTimeout(() => { + page.$eval('div', div => div.innerHTML = 'Hello').catch(() => {}); + }, 0); + await expect(locator).not.toBeHidden(); + }); + + test('not visible', async ({ page }) => { + await page.setContent('
Hello
'); + const locator = page.locator('span'); + setTimeout(() => { + page.$eval('span', span => span.textContent = '').catch(() => {}); + }, 0); + await expect(locator).not.toBeVisible(); + }); + + test('hidden', async ({ page }) => { + await page.setContent('
Hello
'); + const locator = page.locator('span'); + setTimeout(() => { + page.$eval('span', span => span.textContent = '').catch(() => {}); + }, 0); + await expect(locator).toBeHidden(); + }); + `, + }, { workers: 1 }); expect(result.passed).toBe(4); expect(result.exitCode).toBe(0); }); +test('should support toBeVisible, toBeHidden fail', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + + test('visible', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('button'); + await expect(locator).toBeVisible({ timeout: 500 }); + }); + + test('not visible', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).not.toBeVisible({ timeout: 500 }); + }); + + test('hidden', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).toBeHidden({ timeout: 500 }); + }); + + test('not hidden', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('button'); + await expect(locator).not.toBeHidden({ timeout: 500 }); + }); + + test('not hidden 2', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('button'); + await expect(locator).not.toBeHidden({ timeout: 500 }); + }); + `, + }, { workers: 1 }); + expect(result.failed).toBe(5); + expect(result.exitCode).toBe(1); +}); + test('should support toBeFocused', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': `