feat(expect): narrow down available assertions for Page/Locator/APIResponse (#26658)
Fixes #26381.
This commit is contained in:
parent
197f79c933
commit
81cc39ea6e
65
packages/playwright-test/types/test.d.ts
vendored
65
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -4610,9 +4610,6 @@ interface AsymmetricMatchers {
|
||||||
stringMatching(sample: string | RegExp): AsymmetricMatcher;
|
stringMatching(sample: string | RegExp): AsymmetricMatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
|
||||||
type ExtraMatchers<T, Type, Matchers> = T extends Type ? Matchers : IfAny<T, Matchers, {}>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link GenericAssertions} class provides assertion methods that can be used to make assertions about any values
|
* The {@link GenericAssertions} class provides assertion methods that can be used to make assertions about any values
|
||||||
* in the tests. A new instance of {@link GenericAssertions} is created by calling
|
* in the tests. A new instance of {@link GenericAssertions} is created by calling
|
||||||
|
|
@ -5067,33 +5064,41 @@ interface GenericAssertions<R> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseMatchers<R, T> = GenericAssertions<R> & PlaywrightTest.Matchers<R, T>;
|
type FunctionAssertions = {
|
||||||
|
/**
|
||||||
|
* Retries the callback until it passes.
|
||||||
|
*/
|
||||||
|
toPass(options?: { timeout?: number, intervals?: number[] }): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
type MakeMatchers<R, T> = BaseMatchers<R, T> & {
|
type BaseMatchers<R, T> = GenericAssertions<R> & PlaywrightTest.Matchers<R, T> & SnapshotAssertions;
|
||||||
/**
|
type AllowedGenericMatchers<R> = Pick<GenericAssertions<R>, 'toBe' | 'toBeDefined' | 'toBeFalsy' | 'toBeNull' | 'toBeTruthy' | 'toBeUndefined'>;
|
||||||
* If you know how to test something, `.not` lets you test its opposite.
|
|
||||||
*/
|
type SpecificMatchers<R, T> =
|
||||||
not: MakeMatchers<R, T>;
|
T extends Page ? PageAssertions & AllowedGenericMatchers<R> :
|
||||||
/**
|
T extends Locator ? LocatorAssertions & AllowedGenericMatchers<R> :
|
||||||
* Use resolves to unwrap the value of a fulfilled promise so any other
|
T extends APIResponse ? APIResponseAssertions & AllowedGenericMatchers<R> :
|
||||||
* matcher can be chained. If the promise is rejected the assertion fails.
|
BaseMatchers<R, T> & (T extends Function ? FunctionAssertions : {});
|
||||||
*/
|
type AllMatchers<R, T> = PageAssertions & LocatorAssertions & APIResponseAssertions & FunctionAssertions & BaseMatchers<R, T>;
|
||||||
resolves: MakeMatchers<Promise<R>, Awaited<T>>;
|
|
||||||
/**
|
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
||||||
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
||||||
* If the promise is fulfilled the assertion fails.
|
type MakeMatchers<R, T> = {
|
||||||
*/
|
/**
|
||||||
rejects: MakeMatchers<Promise<R>, Awaited<T>>;
|
* If you know how to test something, `.not` lets you test its opposite.
|
||||||
} & SnapshotAssertions &
|
*/
|
||||||
ExtraMatchers<T, Page, PageAssertions> &
|
not: MakeMatchers<R, T>;
|
||||||
ExtraMatchers<T, Locator, LocatorAssertions> &
|
/**
|
||||||
ExtraMatchers<T, APIResponse, APIResponseAssertions> &
|
* Use resolves to unwrap the value of a fulfilled promise so any other
|
||||||
ExtraMatchers<T, Function, {
|
* matcher can be chained. If the promise is rejected the assertion fails.
|
||||||
/**
|
*/
|
||||||
* Retries the callback until it passes.
|
resolves: MakeMatchers<Promise<R>, Awaited<T>>;
|
||||||
*/
|
/**
|
||||||
toPass(options?: { timeout?: number, intervals?: number[] }): Promise<void>;
|
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
||||||
}>;
|
* If the promise is fulfilled the assertion fails.
|
||||||
|
*/
|
||||||
|
rejects: MakeMatchers<Promise<R>, any>;
|
||||||
|
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T>>;
|
||||||
|
|
||||||
export type Expect = {
|
export type Expect = {
|
||||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
||||||
|
|
@ -5119,8 +5124,6 @@ export type Expect = {
|
||||||
not: Omit<AsymmetricMatchers, 'any' | 'anything'>;
|
not: Omit<AsymmetricMatchers, 'any' | 'anything'>;
|
||||||
} & AsymmetricMatchers;
|
} & AsymmetricMatchers;
|
||||||
|
|
||||||
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
|
||||||
|
|
||||||
// --- BEGINGLOBAL ---
|
// --- BEGINGLOBAL ---
|
||||||
declare global {
|
declare global {
|
||||||
export namespace PlaywrightTest {
|
export namespace PlaywrightTest {
|
||||||
|
|
|
||||||
|
|
@ -294,44 +294,79 @@ test('should propose only the relevant matchers when custom expect matcher class
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('custom matchers', async ({ page }) => {
|
test('custom matchers', async ({ page }) => {
|
||||||
|
// Page-specific assertions apply to Page.
|
||||||
await test.expect(page).toHaveURL('https://example.com');
|
await test.expect(page).toHaveURL('https://example.com');
|
||||||
await test.expect(page).not.toHaveURL('https://example.com');
|
await test.expect(page).not.toHaveURL('https://example.com');
|
||||||
await test.expect(page).toBe(true);
|
// Some generic assertions also apply to Page.
|
||||||
|
test.expect(page).toBe(true);
|
||||||
|
test.expect(page).toBeDefined();
|
||||||
|
test.expect(page).toBeFalsy();
|
||||||
|
test.expect(page).toBeNull();
|
||||||
|
test.expect(page).toBeTruthy();
|
||||||
|
test.expect(page).toBeUndefined();
|
||||||
|
|
||||||
|
// Locator-specific and most generic assertions do not apply to Page.
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
await test.expect(page).toBeEnabled();
|
await test.expect(page).toBeEnabled();
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
await test.expect(page).not.toBeEnabled();
|
await test.expect(page).not.toBeEnabled();
|
||||||
|
// @ts-expect-error
|
||||||
|
test.expect(page).toEqual();
|
||||||
|
|
||||||
|
// Locator-specific assertions apply to Locator.
|
||||||
await test.expect(page.locator('foo')).toBeEnabled();
|
await test.expect(page.locator('foo')).toBeEnabled();
|
||||||
await test.expect(page.locator('foo')).toBeEnabled({ enabled: false });
|
await test.expect(page.locator('foo')).toBeEnabled({ enabled: false });
|
||||||
await test.expect(page.locator('foo')).not.toBeEnabled({ enabled: true });
|
await test.expect(page.locator('foo')).not.toBeEnabled({ enabled: true });
|
||||||
|
await test.expect(page.locator('foo')).toBeChecked();
|
||||||
|
await test.expect(page.locator('foo')).not.toBeChecked({ checked: true });
|
||||||
|
await test.expect(page.locator('foo')).not.toBeEditable();
|
||||||
|
await test.expect(page.locator('foo')).toBeEditable({ editable: false });
|
||||||
|
await test.expect(page.locator('foo')).toBeVisible();
|
||||||
|
await test.expect(page.locator('foo')).not.toBeVisible({ visible: false });
|
||||||
|
// Some generic assertions also apply to Locator.
|
||||||
|
test.expect(page.locator('foo')).toBe(true);
|
||||||
|
|
||||||
|
// Page-specific and most generic assertions do not apply to Locator.
|
||||||
|
// @ts-expect-error
|
||||||
|
await test.expect(page.locator('foo')).toHaveURL('https://example.com');
|
||||||
|
// @ts-expect-error
|
||||||
|
await test.expect(page.locator('foo')).toHaveLength(1);
|
||||||
|
|
||||||
|
// Wrong arguments for assertions do not compile.
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
await test.expect(page.locator('foo')).toBeEnabled({ unknown: false });
|
await test.expect(page.locator('foo')).toBeEnabled({ unknown: false });
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
await test.expect(page.locator('foo')).toBeEnabled({ enabled: 'foo' });
|
await test.expect(page.locator('foo')).toBeEnabled({ enabled: 'foo' });
|
||||||
|
|
||||||
await test.expect(page.locator('foo')).toBe(true);
|
// Generic assertions work.
|
||||||
// @ts-expect-error
|
test.expect([123]).toHaveLength(1);
|
||||||
await test.expect(page.locator('foo')).toHaveURL('https://example.com');
|
test.expect('123').toMatchSnapshot('name');
|
||||||
|
test.expect(await page.screenshot()).toMatchSnapshot('screenshot.png');
|
||||||
|
|
||||||
|
// All possible assertions apply to "any" type.
|
||||||
|
const x: any = 123;
|
||||||
|
test.expect(x).toHaveLength(1);
|
||||||
|
await test.expect(x).toHaveURL('url');
|
||||||
|
await test.expect(x).toBeEnabled();
|
||||||
|
test.expect(x).toMatchSnapshot('snapshot name');
|
||||||
|
|
||||||
|
// APIResponse-specific assertions apply to APIResponse.
|
||||||
const res = await page.request.get('http://i-do-definitely-not-exist.com');
|
const res = await page.request.get('http://i-do-definitely-not-exist.com');
|
||||||
await test.expect(res).toBeOK();
|
await test.expect(res).toBeOK();
|
||||||
await test.expect(res).toBe(true);
|
// Some generic assertions also apply to APIResponse.
|
||||||
|
test.expect(res).toBe(true);
|
||||||
|
// Page-specific and most generic assertions do not apply to APIResponse.
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
await test.expect(res).toHaveURL('https://example.com');
|
await test.expect(res).toHaveURL('https://example.com');
|
||||||
|
// @ts-expect-error
|
||||||
|
test.expect(res).toEqual(123);
|
||||||
|
|
||||||
|
// Explicitly casting to "any" supports all assertions.
|
||||||
await test.expect(res as any).toHaveURL('https://example.com');
|
await test.expect(res as any).toHaveURL('https://example.com');
|
||||||
|
|
||||||
|
// Playwright-specific assertions do not apply to generic values.
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
await test.expect(123).toHaveURL('https://example.com');
|
await test.expect(123).toHaveURL('https://example.com');
|
||||||
|
|
||||||
await test.expect(page.locator('foo')).toBeChecked();
|
|
||||||
await test.expect(page.locator('foo')).not.toBeChecked({ checked: true });
|
|
||||||
|
|
||||||
await test.expect(page.locator('foo')).not.toBeEditable();
|
|
||||||
await test.expect(page.locator('foo')).toBeEditable({ editable: false });
|
|
||||||
|
|
||||||
await test.expect(page.locator('foo')).toBeVisible();
|
|
||||||
await test.expect(page.locator('foo')).not.toBeVisible({ visible: false });
|
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
|
||||||
65
utils/generate_types/overrides-test.d.ts
vendored
65
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -294,9 +294,6 @@ interface AsymmetricMatchers {
|
||||||
stringMatching(sample: string | RegExp): AsymmetricMatcher;
|
stringMatching(sample: string | RegExp): AsymmetricMatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
|
||||||
type ExtraMatchers<T, Type, Matchers> = T extends Type ? Matchers : IfAny<T, Matchers, {}>;
|
|
||||||
|
|
||||||
interface GenericAssertions<R> {
|
interface GenericAssertions<R> {
|
||||||
not: GenericAssertions<R>;
|
not: GenericAssertions<R>;
|
||||||
toBe(expected: unknown): R;
|
toBe(expected: unknown): R;
|
||||||
|
|
@ -325,33 +322,41 @@ interface GenericAssertions<R> {
|
||||||
toThrowError(error?: unknown): R;
|
toThrowError(error?: unknown): R;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseMatchers<R, T> = GenericAssertions<R> & PlaywrightTest.Matchers<R, T>;
|
type FunctionAssertions = {
|
||||||
|
/**
|
||||||
|
* Retries the callback until it passes.
|
||||||
|
*/
|
||||||
|
toPass(options?: { timeout?: number, intervals?: number[] }): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
type MakeMatchers<R, T> = BaseMatchers<R, T> & {
|
type BaseMatchers<R, T> = GenericAssertions<R> & PlaywrightTest.Matchers<R, T> & SnapshotAssertions;
|
||||||
/**
|
type AllowedGenericMatchers<R> = Pick<GenericAssertions<R>, 'toBe' | 'toBeDefined' | 'toBeFalsy' | 'toBeNull' | 'toBeTruthy' | 'toBeUndefined'>;
|
||||||
* If you know how to test something, `.not` lets you test its opposite.
|
|
||||||
*/
|
type SpecificMatchers<R, T> =
|
||||||
not: MakeMatchers<R, T>;
|
T extends Page ? PageAssertions & AllowedGenericMatchers<R> :
|
||||||
/**
|
T extends Locator ? LocatorAssertions & AllowedGenericMatchers<R> :
|
||||||
* Use resolves to unwrap the value of a fulfilled promise so any other
|
T extends APIResponse ? APIResponseAssertions & AllowedGenericMatchers<R> :
|
||||||
* matcher can be chained. If the promise is rejected the assertion fails.
|
BaseMatchers<R, T> & (T extends Function ? FunctionAssertions : {});
|
||||||
*/
|
type AllMatchers<R, T> = PageAssertions & LocatorAssertions & APIResponseAssertions & FunctionAssertions & BaseMatchers<R, T>;
|
||||||
resolves: MakeMatchers<Promise<R>, Awaited<T>>;
|
|
||||||
/**
|
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
||||||
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
||||||
* If the promise is fulfilled the assertion fails.
|
type MakeMatchers<R, T> = {
|
||||||
*/
|
/**
|
||||||
rejects: MakeMatchers<Promise<R>, Awaited<T>>;
|
* If you know how to test something, `.not` lets you test its opposite.
|
||||||
} & SnapshotAssertions &
|
*/
|
||||||
ExtraMatchers<T, Page, PageAssertions> &
|
not: MakeMatchers<R, T>;
|
||||||
ExtraMatchers<T, Locator, LocatorAssertions> &
|
/**
|
||||||
ExtraMatchers<T, APIResponse, APIResponseAssertions> &
|
* Use resolves to unwrap the value of a fulfilled promise so any other
|
||||||
ExtraMatchers<T, Function, {
|
* matcher can be chained. If the promise is rejected the assertion fails.
|
||||||
/**
|
*/
|
||||||
* Retries the callback until it passes.
|
resolves: MakeMatchers<Promise<R>, Awaited<T>>;
|
||||||
*/
|
/**
|
||||||
toPass(options?: { timeout?: number, intervals?: number[] }): Promise<void>;
|
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
||||||
}>;
|
* If the promise is fulfilled the assertion fails.
|
||||||
|
*/
|
||||||
|
rejects: MakeMatchers<Promise<R>, any>;
|
||||||
|
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T>>;
|
||||||
|
|
||||||
export type Expect = {
|
export type Expect = {
|
||||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
||||||
|
|
@ -377,8 +382,6 @@ export type Expect = {
|
||||||
not: Omit<AsymmetricMatchers, 'any' | 'anything'>;
|
not: Omit<AsymmetricMatchers, 'any' | 'anything'>;
|
||||||
} & AsymmetricMatchers;
|
} & AsymmetricMatchers;
|
||||||
|
|
||||||
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
|
||||||
|
|
||||||
// --- BEGINGLOBAL ---
|
// --- BEGINGLOBAL ---
|
||||||
declare global {
|
declare global {
|
||||||
export namespace PlaywrightTest {
|
export namespace PlaywrightTest {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue