fix(expect): continue polling in array-based matchers, fix edge cases (#9430)

This commit is contained in:
Pavel Feldman 2021-10-11 13:01:09 -08:00 committed by GitHub
parent b1160ec239
commit 876e08315b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 15 deletions

View File

@ -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<T, R, Element | undefined>;
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);

View File

@ -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 };
}
}

View File

@ -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('<div id=div></div>');
const locator = page.locator('p');
setTimeout(() => {
page.evaluate(() => {
div.innerHTML = "<p>Text 1</p><p>Text 2</p>";
}).catch(() => {});
}, 500);
await expect(locator).toHaveText(['Text 1', 'Text 2']);
});
test('pass empty', async ({ page }) => {
await page.setContent('<div></div>');
const locator = page.locator('p');
await expect(locator).toHaveText([]);
});
test('pass not empty', async ({ page }) => {
await page.setContent('<div><p>Test</p></div>');
const locator = page.locator('p');
await expect(locator).not.toHaveText([]);
});
test('pass on empty', async ({ page }) => {
await page.setContent('<div></div>');
const locator = page.locator('p');
await expect(locator).not.toHaveText(['Test']);
});
test('pass eventually empty', async ({ page }) => {
await page.setContent('<div id=div><p>Text</p></div>');
const locator = page.locator('p');
setTimeout(() => {
page.evaluate(() => div.innerHTML = "").catch(() => {});
}, 500);
await expect(locator).not.toHaveText([]);
});
test('fail', async ({ page }) => {
await page.setContent('<div>Text 1</div><div>Text 3</div>');
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);
});