diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 0a9f38aa79..581fee02dc 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1163,10 +1163,11 @@ export class Frame extends SdkObject { async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ pass: boolean, received?: any, log?: string[] }> { const controller = new ProgressController(metadata, this); - const querySelectorAll = options.expression === 'to.have.count' || options.expression.endsWith('.array'); + const isListMatcher = options.expression.endsWith('.array'); + const querySelectorAll = options.expression === 'to.have.count' || isListMatcher; const mainWorld = options.expression === 'to.have.property'; - const omitAttached = (!options.isNot && options.expression === 'to.be.hidden') || (options.isNot && options.expression === 'to.be.visible'); - + const expectsEmptyList = options.expectedText?.length === 0; + const omitAttached = (isListMatcher && options.isNot !== expectsEmptyList) || (!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) => { // We don't have an element and we don't need an element => pass. if (!element && options.omitAttached) @@ -1263,18 +1264,22 @@ export class Frame extends SdkObject { const callback = injected.eval(callbackText) as DomTaskBody; const poller = logScale ? injected.pollLogScale.bind(injected) : injected.pollRaf.bind(injected); return poller((progress, continuePolling) => { + let element: Element | undefined; + let elements: Element[] = []; if (querySelectorAll) { - const elements = injected.querySelectorAll(info.parsed, document); + elements = injected.querySelectorAll(info.parsed, document); + element = elements[0]; progress.logRepeating(` selector resolved to ${elements.length} element${elements.length === 1 ? '' : 's'}`); - return callback(progress, elements[0], taskData as T, elements, continuePolling); + } else { + element = injected.querySelector(info.parsed, document, info.strict); + elements = []; + if (element) + progress.logRepeating(` selector resolved to ${injected.previewNode(element)}`); } - const element = injected.querySelector(info.parsed, document, info.strict); if (!element && !omitAttached) return continuePolling; - if (element) - progress.logRepeating(` selector resolved to ${injected.previewNode(element)}`); - return callback(progress, element, taskData as T, [], continuePolling); + return callback(progress, element, taskData as T, elements, continuePolling); }); }, { info, taskData, callbackText, querySelectorAll: options.querySelectorAll, logScale: options.logScale, omitAttached: options.omitAttached }); }, true); diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 762b9aa6fa..74c24ffdc8 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -890,22 +890,30 @@ export class InjectedScript { if (received && options.expectedText) { // "To match an array" is "to contain an array" + "equal length" const lengthShouldMatch = expression !== 'to.contain.text.array'; - if (received.length !== options.expectedText.length && lengthShouldMatch) { + const matchesLength = received.length === options.expectedText.length || !lengthShouldMatch; + if (matchesLength === options.isNot) { progress.setIntermediateResult(received); return continuePolling; } + if (!matchesLength) + return { received, pass: !options.isNot }; // Each matcher should get a "received" that matches it, in order. let i = 0; const matchers = options.expectedText.map(e => new ExpectedTextMatcher(e)); + let allMatchesFound = true; for (const matcher of matchers) { - while (i < received.length && matcher.matches(received[i]) === options.isNot) + while (i < received.length && !matcher.matches(received[i])) i++; - if (i === received.length) { - progress.setIntermediateResult(received); - return continuePolling; + if (i >= received.length) { + allMatchesFound = false; + break; } } + if (allMatchesFound === options.isNot) { + progress.setIntermediateResult(received); + return continuePolling; + } return { received, pass: !options.isNot }; } } diff --git a/tests/playwright-test/playwright.expect.text.spec.ts b/tests/playwright-test/playwright.expect.text.spec.ts index 53f7447a0b..ab1ad11c76 100644 --- a/tests/playwright-test/playwright.expect.text.spec.ts +++ b/tests/playwright-test/playwright.expect.text.spec.ts @@ -155,6 +155,44 @@ test('should support toHaveText w/ array', async ({ runInlineTest }) => { await expect(locator).toHaveText(['Text 1', /Text \\d+a/]); }); + test('pass lazy', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('p'); + setTimeout(() => { + page.evaluate(() => { + div.innerHTML = "

Text 1

Text 2

"; + }).catch(() => {}); + }, 500); + await expect(locator).toHaveText(['Text 1', 'Text 2']); + }); + + test('pass empty', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('p'); + await expect(locator).toHaveText([]); + }); + + test('pass not empty', async ({ page }) => { + await page.setContent('

Test

'); + const locator = page.locator('p'); + await expect(locator).not.toHaveText([]); + }); + + test('pass on empty', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('p'); + await expect(locator).not.toHaveText(['Test']); + }); + + test('pass eventually empty', async ({ page }) => { + await page.setContent('

Text

'); + const locator = page.locator('p'); + setTimeout(() => { + page.evaluate(() => div.innerHTML = "").catch(() => {}); + }, 500); + await expect(locator).not.toHaveText([]); + }); + test('fail', async ({ page }) => { await page.setContent('
Text 1
Text 3
'); const locator = page.locator('div'); @@ -168,7 +206,7 @@ test('should support toHaveText w/ array', async ({ runInlineTest }) => { expect(output).toContain('- "Extra"'); expect(output).toContain('waiting for selector "div"'); expect(output).toContain('selector resolved to 2 elements'); - expect(result.passed).toBe(1); + expect(result.passed).toBe(6); expect(result.failed).toBe(1); expect(result.exitCode).toBe(1); });