model.setSorting && toggleSorting(column)}
>
@@ -84,7 +99,9 @@ export function GridView
(model: GridViewProps) {
return
+ style={{
+ width: i < model.columns.length - 1 ? model.columnWidths.get(column) : undefined,
+ }}>
{body}
;
})}
diff --git a/packages/web/src/shared/resizeView.tsx b/packages/web/src/shared/resizeView.tsx
index e575866de0..401867f223 100644
--- a/packages/web/src/shared/resizeView.tsx
+++ b/packages/web/src/shared/resizeView.tsx
@@ -58,7 +58,7 @@ export const ResizeView: React.FC<{
right: 0,
bottom: 0,
left: -(7 - resizerWidth) / 2,
- zIndex: 1000,
+ zIndex: 100, // Above the content, but below the film strip hover.
pointerEvents: 'none',
}}
ref={ref}>
diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts
index 02555d42eb..8b4fdc27b9 100644
--- a/tests/config/browserTest.ts
+++ b/tests/config/browserTest.ts
@@ -67,10 +67,12 @@ const test = baseTest.extend
await run(false);
}, { scope: 'worker' }],
- defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel }, run) => {
+ defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel, isLinux }, run) => {
if (browserName === 'chromium')
await run('Lax');
- else if (browserName === 'webkit')
+ else if (browserName === 'webkit' && isLinux)
+ await run('Lax');
+ else if (browserName === 'webkit' && !isLinux)
await run('None');
else if (browserName === 'firefox' && channel === 'firefox-beta')
await run(browserMajorVersion >= 103 && browserMajorVersion < 110 ? 'Lax' : 'None');
diff --git a/tests/config/testModeFixtures.ts b/tests/config/testModeFixtures.ts
index 6b6feff7c2..40b1996719 100644
--- a/tests/config/testModeFixtures.ts
+++ b/tests/config/testModeFixtures.ts
@@ -30,11 +30,12 @@ export type TestModeTestFixtures = {
export type TestModeWorkerFixtures = {
toImplInWorkerScope: (rpcObject?: any) => any;
playwright: typeof import('@playwright/test');
+ _playwrightImpl: typeof import('@playwright/test');
};
export const testModeTest = test.extend({
mode: ['default', { scope: 'worker', option: true }],
- playwright: [async ({ mode }, run) => {
+ _playwrightImpl: [async ({ mode }, run) => {
const testMode = {
'default': new DefaultTestMode(),
'service': new DefaultTestMode(),
diff --git a/tests/library/browsercontext-add-cookies.spec.ts b/tests/library/browsercontext-add-cookies.spec.ts
index bdfe14ef0a..f35ea8b327 100644
--- a/tests/library/browsercontext-add-cookies.spec.ts
+++ b/tests/library/browsercontext-add-cookies.spec.ts
@@ -24,7 +24,7 @@ it('should work @smoke', async ({ context, page, server }) => {
await context.addCookies([{
url: server.EMPTY_PAGE,
name: 'password',
- value: '123456'
+ value: '123456',
}]);
expect(await page.evaluate(() => document.cookie)).toEqual('password=123456');
});
@@ -224,7 +224,7 @@ it('should have |expires| set to |-1| for session cookies', async ({ context, se
expect(cookies[0].expires).toBe(-1);
});
-it('should set cookie with reasonable defaults', async ({ context, server, browserName }) => {
+it('should set cookie with reasonable defaults', async ({ context, server, defaultSameSiteCookieValue }) => {
await context.addCookies([{
url: server.EMPTY_PAGE,
name: 'defaults',
@@ -239,7 +239,7 @@ it('should set cookie with reasonable defaults', async ({ context, server, brows
expires: -1,
httpOnly: false,
secure: false,
- sameSite: browserName === 'chromium' ? 'Lax' : 'None',
+ sameSite: defaultSameSiteCookieValue,
}]);
});
diff --git a/tests/library/browsercontext-cookies.spec.ts b/tests/library/browsercontext-cookies.spec.ts
index a53f989886..df923ef3ff 100644
--- a/tests/library/browsercontext-cookies.spec.ts
+++ b/tests/library/browsercontext-cookies.spec.ts
@@ -384,7 +384,7 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
server.waitForRequest('/title.html'),
frame.evaluate(() => fetch('/title.html'))
]);
- if (!isMac && browserName === 'webkit')
+ if (isWindows && browserName === 'webkit')
expect(serverRequest.headers.cookie).toBe('name=value');
else
expect(serverRequest.headers.cookie).toBeFalsy();
@@ -396,7 +396,10 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
server.waitForRequest('/title.html'),
frame.evaluate(() => fetch('/title.html'))
]);
- expect(serverRequest.headers.cookie).toBe('name=value');
+ if (isLinux && browserName === 'webkit')
+ expect(serverRequest.headers.cookie).toBe(undefined);
+ else
+ expect(serverRequest.headers.cookie).toBe('name=value');
}
}
});
diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts
index f4088359cf..ba01ccc07c 100644
--- a/tests/library/browsercontext-fetch.spec.ts
+++ b/tests/library/browsercontext-fetch.spec.ts
@@ -435,10 +435,10 @@ it('should return error with wrong credentials', async ({ context, server }) =>
expect(response2.status()).toBe(401);
});
-it('should support HTTPCredentials.sendImmediately', async ({ contextFactory, server }) => {
+it('should support HTTPCredentials.sendImmediately for newContext', async ({ contextFactory, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const context = await contextFactory({
- httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), sendImmediately: true }
+ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
});
{
const [serverRequest, response] = await Promise.all([
@@ -459,6 +459,31 @@ it('should support HTTPCredentials.sendImmediately', async ({ contextFactory, se
}
});
+it('should support HTTPCredentials.sendImmediately for browser.newPage', async ({ contextFactory, server, browser }) => {
+ it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
+ const page = await browser.newPage({
+ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
+ });
+ {
+ const [serverRequest, response] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.request.get(server.EMPTY_PAGE)
+ ]);
+ expect(serverRequest.headers.authorization).toBe('Basic ' + Buffer.from('user:pass').toString('base64'));
+ expect(response.status()).toBe(200);
+ }
+ {
+ const [serverRequest, response] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.request.get(server.CROSS_PROCESS_PREFIX + '/empty.html')
+ ]);
+ // Not sent to another origin.
+ expect(serverRequest.headers.authorization).toBe(undefined);
+ expect(response.status()).toBe(200);
+ }
+ await page.close();
+});
+
it('delete should support post data', async ({ context, server }) => {
const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'),
@@ -1202,7 +1227,7 @@ it('fetch should not throw on long set-cookie value', async ({ context, server }
expect(cookies.map(c => c.name)).toContain('bar');
});
-it('should support set-cookie with SameSite and without Secure attribute over HTTP', async ({ page, server, browserName, isWindows }) => {
+it('should support set-cookie with SameSite and without Secure attribute over HTTP', async ({ page, server, browserName, isWindows, isLinux }) => {
for (const value of ['None', 'Lax', 'Strict']) {
await it.step(`SameSite=${value}`, async () => {
server.setRoute('/empty.html', (req, res) => {
@@ -1213,6 +1238,8 @@ it('should support set-cookie with SameSite and without Secure attribute over HT
const [cookie] = await page.context().cookies();
if (browserName === 'chromium' && value === 'None')
expect(cookie).toBeFalsy();
+ else if (browserName === 'webkit' && isLinux && value === 'None')
+ expect(cookie).toBeFalsy();
else if (browserName === 'webkit' && isWindows)
expect(cookie.sameSite).toBe('None');
else
diff --git a/tests/library/browsercontext-route.spec.ts b/tests/library/browsercontext-route.spec.ts
index a597cf1157..25d965c1b6 100644
--- a/tests/library/browsercontext-route.spec.ts
+++ b/tests/library/browsercontext-route.spec.ts
@@ -109,7 +109,7 @@ it('should fall back to context.route', async ({ browser, server }) => {
await context.close();
});
-it('should support Set-Cookie header', async ({ contextFactory, server, browserName, defaultSameSiteCookieValue }) => {
+it('should support Set-Cookie header', async ({ contextFactory, defaultSameSiteCookieValue }) => {
const context = await contextFactory();
const page = await context.newPage();
await page.route('https://example.com/', (route, request) => {
@@ -152,7 +152,7 @@ it('should ignore secure Set-Cookie header for insecure requests', async ({ cont
expect(await context.cookies()).toEqual([]);
});
-it('should use Set-Cookie header in future requests', async ({ contextFactory, server, browserName, defaultSameSiteCookieValue }) => {
+it('should use Set-Cookie header in future requests', async ({ contextFactory, server, defaultSameSiteCookieValue }) => {
const context = await contextFactory();
const page = await context.newPage();
diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts
index 58dec04519..a2eb629dc5 100644
--- a/tests/library/global-fetch.spec.ts
+++ b/tests/library/global-fetch.spec.ts
@@ -157,7 +157,7 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
it('should support HTTPCredentials.sendImmediately', async ({ playwright, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const request = await playwright.request.newContext({
- httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), sendImmediately: true }
+ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
});
{
const [serverRequest, response] = await Promise.all([
diff --git a/tests/page/page-basic.spec.ts b/tests/page/page-basic.spec.ts
index 38cb5340c0..0b99d0c8c5 100644
--- a/tests/page/page-basic.spec.ts
+++ b/tests/page/page-basic.spec.ts
@@ -255,8 +255,7 @@ it('frame.press should work', async ({ page, server }) => {
expect(await frame.evaluate(() => document.querySelector('textarea').value)).toBe('a');
});
-it('has navigator.webdriver set to true', async ({ page, browserName }) => {
- it.skip(browserName === 'firefox');
+it('has navigator.webdriver set to true', async ({ page }) => {
expect(await page.evaluate(() => navigator.webdriver)).toBe(true);
});
diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts
new file mode 100644
index 0000000000..c3e2aaeb54
--- /dev/null
+++ b/tests/page/page-clock.spec.ts
@@ -0,0 +1,614 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { test, expect } from './pageTest';
+
+declare global {
+ interface Window {
+ stub: (param?: any) => void
+ }
+}
+
+const it = test.extend<{ calls: { params: any[] }[] }>({
+ calls: async ({ page }, use) => {
+ const calls = [];
+ await page.exposeFunction('stub', async (...params: any[]) => {
+ calls.push({ params });
+ });
+ await use(calls);
+ }
+});
+
+it.describe('tick', () => {
+ it('triggers immediately without specified delay', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub);
+ });
+
+ await page.clock.tick(0);
+ expect(calls).toEqual([{ params: [] }]);
+ });
+
+ it('does not trigger without sufficient delay', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 100);
+ });
+ await page.clock.tick(10);
+ expect(calls).toEqual([]);
+ });
+
+ it('triggers after sufficient delay', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 100);
+ });
+ await page.clock.tick(100);
+ expect(calls).toEqual([{ params: [] }]);
+ });
+
+ it('triggers simultaneous timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 100);
+ setTimeout(window.stub, 100);
+ });
+ await page.clock.tick(100);
+ expect(calls).toEqual([{ params: [] }, { params: [] }]);
+ });
+
+ it('triggers multiple simultaneous timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 100);
+ setTimeout(window.stub, 100);
+ setTimeout(window.stub, 99);
+ setTimeout(window.stub, 100);
+ });
+ await page.clock.tick(100);
+ expect(calls.length).toBe(4);
+ });
+
+ it('waits after setTimeout was called', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 150);
+ });
+ await page.clock.tick(50);
+ expect(calls).toEqual([]);
+ await page.clock.tick(100);
+ expect(calls).toEqual([{ params: [] }]);
+ });
+
+ it('triggers event when some throw', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => { throw new Error(); }, 100);
+ setTimeout(window.stub, 120);
+ });
+
+ await expect(page.clock.tick(120)).rejects.toThrow();
+ expect(calls).toEqual([{ params: [] }]);
+ });
+
+ it('creates updated Date while ticking', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setInterval(() => {
+ window.stub(new Date().getTime());
+ }, 10);
+ });
+ await page.clock.tick(100);
+ expect(calls).toEqual([
+ { params: [10] },
+ { params: [20] },
+ { params: [30] },
+ { params: [40] },
+ { params: [50] },
+ { params: [60] },
+ { params: [70] },
+ { params: [80] },
+ { params: [90] },
+ { params: [100] },
+ ]);
+ });
+
+ it('passes 8 seconds', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setInterval(window.stub, 4000);
+ });
+
+ await page.clock.tick('08');
+ expect(calls.length).toBe(2);
+ });
+
+ it('passes 1 minute', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setInterval(window.stub, 6000);
+ });
+
+ await page.clock.tick('01:00');
+ expect(calls.length).toBe(10);
+ });
+
+ it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setInterval(window.stub, 10000);
+ });
+
+ await page.clock.tick('02:34:10');
+ expect(calls.length).toBe(925);
+ });
+
+ it('throws for invalid format', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setInterval(window.stub, 10000);
+ });
+ await expect(page.clock.tick('12:02:34:10')).rejects.toThrow();
+ expect(calls).toEqual([]);
+ });
+
+ it('returns the current now value', async ({ page }) => {
+ await page.clock.install();
+ const value = 200;
+ await page.clock.tick(value);
+ expect(await page.evaluate(() => Date.now())).toBe(value);
+ });
+});
+
+it.describe('jump', () => {
+ it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ window.stub('should not be logged');
+ }, 1000);
+ });
+ await page.clock.jump(500);
+ expect(calls).toEqual([]);
+ });
+
+ it('pushes back execution time for skipped timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ window.stub(Date.now());
+ }, 1000);
+ });
+
+ await page.clock.jump(2000);
+ expect(calls).toEqual([{ params: [2000] }]);
+ });
+
+ it('supports string time arguments', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ window.stub(Date.now());
+ }, 100000); // 100000 = 1:40
+ });
+ await page.clock.jump('01:50');
+ expect(calls).toEqual([{ params: [110000] }]);
+ });
+});
+
+it.describe('runAllAsyn', () => {
+ it('if there are no timers just return', async ({ page }) => {
+ await page.clock.install();
+ await page.clock.runAll();
+ });
+
+ it('runs all timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 10);
+ setTimeout(window.stub, 50);
+ });
+ await page.clock.runAll();
+ expect(calls.length).toBe(2);
+ });
+
+ it('new timers added while running are also run', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ setTimeout(window.stub, 50);
+ }, 10);
+ });
+ await page.clock.runAll();
+ expect(calls.length).toBe(1);
+ });
+
+ it('new timers added in promises while running are also run', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ void Promise.resolve().then(() => {
+ setTimeout(window.stub, 50);
+ });
+ }, 10);
+ });
+ await page.clock.runAll();
+ expect(calls.length).toBe(1);
+ });
+
+ it('throws before allowing infinite recursion', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ const recursiveCallback = () => {
+ window.stub();
+ setTimeout(recursiveCallback, 10);
+ };
+ setTimeout(recursiveCallback, 10);
+ });
+ await expect(page.clock.runAll()).rejects.toThrow();
+ expect(calls).toHaveLength(1000);
+ });
+
+ it('throws before allowing infinite recursion from promises', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ const recursiveCallback = () => {
+ window.stub();
+ void Promise.resolve().then(() => {
+ setTimeout(recursiveCallback, 10);
+ });
+ };
+ setTimeout(recursiveCallback, 10);
+ });
+ await expect(page.clock.runAll()).rejects.toThrow();
+ expect(calls).toHaveLength(1000);
+ });
+
+ it('the loop limit can be set when creating a clock', async ({ page, calls }) => {
+ await page.clock.install({ loopLimit: 1 });
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 10);
+ setTimeout(window.stub, 50);
+ });
+ await expect(page.clock.runAll()).rejects.toThrow();
+ expect(calls).toHaveLength(1);
+ });
+
+ it('should settle user-created promises', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ void Promise.resolve().then(() => window.stub());
+ }, 55);
+ });
+ await page.clock.runAll();
+ expect(calls).toHaveLength(1);
+ });
+
+ it('should settle nested user-created promises', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ void Promise.resolve().then(() => {
+ void Promise.resolve().then(() => {
+ void Promise.resolve().then(() => window.stub());
+ });
+ });
+ }, 55);
+ });
+ await page.clock.runAll();
+ expect(calls).toHaveLength(1);
+ });
+
+ it('should settle local promises before firing timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ void Promise.resolve().then(() => window.stub(1));
+ setTimeout(() => window.stub(2), 55);
+ });
+ await page.clock.runAll();
+ expect(calls).toEqual([
+ { params: [1] },
+ { params: [2] },
+ ]);
+ });
+});
+
+it.describe('runToLast', () => {
+ it('returns current time when there are no timers', async ({ page }) => {
+ await page.clock.install();
+ const time = await page.clock.runToLast();
+ expect(time).toBe(0);
+ });
+
+ it('runs all existing timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 10);
+ setTimeout(window.stub, 50);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(2);
+ });
+
+ it('returns time of the last timer', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 10);
+ setTimeout(window.stub, 50);
+ });
+ const time = await page.clock.runToLast();
+ expect(time).toBe(50);
+ });
+
+ it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 10);
+ setTimeout(window.stub, 10);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(2);
+ });
+
+ it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ window.stub();
+ setTimeout(window.stub, 50);
+ }, 10);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(1);
+ });
+
+ it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 100);
+ setTimeout(() => {
+ setTimeout(window.stub, 50);
+ }, 10);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(2);
+ });
+
+ it('new timers cannot cause an infinite loop', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ const recursiveCallback = () => {
+ window.stub();
+ setTimeout(recursiveCallback, 0);
+ };
+ setTimeout(recursiveCallback, 0);
+ setTimeout(window.stub, 100);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(102);
+ });
+
+ it('should support clocks with start time', async ({ page, calls }) => {
+ await page.clock.install({ now: 200 });
+ await page.evaluate(async () => {
+ setTimeout(function cb() {
+ window.stub();
+ setTimeout(cb, 50);
+ }, 50);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(1);
+ });
+
+ it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ const recursiveCallback = () => {
+ void Promise.resolve().then(() => {
+ setTimeout(recursiveCallback, 0);
+ });
+ };
+ setTimeout(recursiveCallback, 0);
+ setTimeout(window.stub, 100);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(1);
+ });
+
+ it('should settle user-created promises', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ void Promise.resolve().then(() => window.stub());
+ }, 55);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(1);
+ });
+
+ it('should settle nested user-created promises', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ void Promise.resolve().then(() => {
+ void Promise.resolve().then(() => {
+ void Promise.resolve().then(() => window.stub());
+ });
+ });
+ }, 55);
+ });
+ await page.clock.runToLast();
+ expect(calls.length).toBe(1);
+ });
+
+ it('should settle local promises before firing timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ void Promise.resolve().then(() => window.stub(1));
+ setTimeout(() => window.stub(2), 55);
+ });
+ await page.clock.runToLast();
+ expect(calls).toEqual([
+ { params: [1] },
+ { params: [2] },
+ ]);
+ });
+
+ it('should settle user-created promises before firing more timers', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(() => {
+ void Promise.resolve().then(() => window.stub(1));
+ }, 55);
+ setTimeout(() => window.stub(2), 75);
+ });
+ await page.clock.runToLast();
+ expect(calls).toEqual([
+ { params: [1] },
+ { params: [2] },
+ ]);
+ });
+});
+
+it.describe('stubTimers', () => {
+ it('sets initial timestamp', async ({ page, calls }) => {
+ await page.clock.install({ now: 1400 });
+ expect(await page.evaluate(() => Date.now())).toBe(1400);
+ });
+
+ it('replaces global setTimeout', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setTimeout(window.stub, 1000);
+ });
+ await page.clock.tick(1000);
+ expect(calls.length).toBe(1);
+ });
+
+ it('global fake setTimeout should return id', async ({ page, calls }) => {
+ await page.clock.install();
+ const to = await page.evaluate(() => setTimeout(window.stub, 1000));
+ expect(typeof to).toBe('number');
+ });
+
+ it('replaces global clearTimeout', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ const to = setTimeout(window.stub, 1000);
+ clearTimeout(to);
+ });
+ await page.clock.tick(1000);
+ expect(calls).toEqual([]);
+ });
+
+ it('replaces global setInterval', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ setInterval(window.stub, 500);
+ });
+ await page.clock.tick(1000);
+ expect(calls.length).toBe(2);
+ });
+
+ it('replaces global clearInterval', async ({ page, calls }) => {
+ await page.clock.install();
+ await page.evaluate(async () => {
+ const to = setInterval(window.stub, 500);
+ clearInterval(to);
+ });
+ await page.clock.tick(1000);
+ expect(calls).toEqual([]);
+ });
+
+ it('replaces global performance.now', async ({ page }) => {
+ await page.clock.install();
+ const promise = page.evaluate(async () => {
+ const prev = performance.now();
+ await new Promise(f => setTimeout(f, 1000));
+ const next = performance.now();
+ return { prev, next };
+ });
+ await page.clock.tick(1000);
+ expect(await promise).toEqual({ prev: 0, next: 1000 });
+ });
+
+ it('fakes Date constructor', async ({ page }) => {
+ await page.clock.install({ now: 0 });
+ const now = await page.evaluate(() => new Date().getTime());
+ expect(now).toBe(0);
+ });
+
+ it('does not fake methods not provided', async ({ page }) => {
+ await page.clock.install({
+ now: 0,
+ toFake: ['Date'],
+ });
+
+ // Should not stall.
+ await page.evaluate(() => {
+ return new Promise(f => setTimeout(f, 1));
+ });
+ });
+});
+
+it.describe('shouldAdvanceTime', () => {
+ it('should create an auto advancing timer', async ({ page, calls }) => {
+ const testDelay = 29;
+ const now = new Date('2015-09-25');
+ await page.clock.install({ now, shouldAdvanceTime: true });
+ const pageNow = await page.evaluate(() => Date.now());
+ expect(pageNow).toBe(1443139200000);
+
+ await page.evaluate(async testDelay => {
+ return new Promise(f => {
+ const timeoutStarted = Date.now();
+ setTimeout(() => {
+ window.stub(Date.now() - timeoutStarted);
+ f();
+ }, testDelay);
+ });
+ }, testDelay);
+
+ expect(calls).toEqual([
+ { params: [testDelay] }
+ ]);
+ });
+
+ it('should test setInterval', async ({ page, calls }) => {
+ const now = new Date('2015-09-25');
+ await page.clock.install({ now, shouldAdvanceTime: true });
+
+ const timeDifference = await page.evaluate(async () => {
+ return new Promise(f => {
+ const interval = 20;
+ const cyclesToTrigger = 3;
+ const timeoutStarted = Date.now();
+ let intervalsTriggered = 0;
+ const intervalId = setInterval(() => {
+ if (++intervalsTriggered === cyclesToTrigger) {
+ clearInterval(intervalId);
+ const timeDifference = Date.now() - timeoutStarted;
+ f(timeDifference - interval * cyclesToTrigger);
+ }
+ }, interval);
+ });
+ });
+
+ expect(timeDifference).toBe(0);
+ });
+});
diff --git a/tests/playwright-test/playwright.artifacts.spec.ts b/tests/playwright-test/playwright.artifacts.spec.ts
index b666aa4b70..a7c0699c9d 100644
--- a/tests/playwright-test/playwright.artifacts.spec.ts
+++ b/tests/playwright-test/playwright.artifacts.spec.ts
@@ -151,10 +151,8 @@ test('should work with screenshot: on', async ({ runInlineTest }, testInfo) => {
' test-finished-1.png',
'artifacts-shared-shared-failing',
' test-failed-1.png',
- ' test-failed-2.png',
'artifacts-shared-shared-passing',
' test-finished-1.png',
- ' test-finished-2.png',
'artifacts-two-contexts',
' test-finished-1.png',
' test-finished-2.png',
@@ -185,7 +183,6 @@ test('should work with screenshot: only-on-failure', async ({ runInlineTest }, t
' test-failed-1.png',
'artifacts-shared-shared-failing',
' test-failed-1.png',
- ' test-failed-2.png',
'artifacts-two-contexts-failing',
' test-failed-1.png',
' test-failed-2.png',
diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts
index dee65c9a52..13af444edf 100644
--- a/tests/playwright-test/playwright.trace.spec.ts
+++ b/tests/playwright-test/playwright.trace.spec.ts
@@ -569,14 +569,19 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo
expect([...trace.resources.keys()].filter(f => f.startsWith('resources/'))).toHaveLength(0);
});
-test('should record with custom page fixture', async ({ runInlineTest }, testInfo) => {
+test('should record with custom page fixture that closes the context', async ({ runInlineTest }, testInfo) => {
+ // Note that original issue did not close the context, but we do not support such usecase.
+ test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23220' });
+
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
myPage: async ({ browser }, use) => {
- await use(await browser.newPage());
+ const page = await browser.newPage();
+ await use(page);
+ await page.close();
},
});
@@ -1112,3 +1117,121 @@ test('trace:retain-on-first-failure should create trace if request context is di
expect(trace.apiNames).toContain('apiRequestContext.get');
expect(result.failed).toBe(1);
});
+
+test('should record trace in workerStorageState', async ({ runInlineTest }) => {
+ test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30287' });
+
+ const result = await runInlineTest({
+ 'a.spec.ts': `
+ import { test as base, expect } from '@playwright/test';
+ const test = base.extend({
+ storageState: ({ workerStorageState }, use) => use(workerStorageState),
+ workerStorageState: [async ({ browser }, use) => {
+ const page = await browser.newPage({ storageState: undefined });
+ await page.setContent('hello
');
+ await page.close();
+ await use(undefined);
+ }, { scope: 'worker' }],
+ })
+ test('pass', async ({ page }) => {
+ await page.goto('data:text/html,hi
');
+ });
+ `,
+ }, { trace: 'on' });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+
+ const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.zip');
+ const trace = await parseTrace(tracePath);
+ expect(trace.actionTree).toEqual([
+ 'Before Hooks',
+ ' fixture: browser',
+ ' browserType.launch',
+ ' fixture: workerStorageState',
+ ' browser.newPage',
+ ' page.setContent',
+ ' page.close',
+ ' fixture: context',
+ ' browser.newContext',
+ ' fixture: page',
+ ' browserContext.newPage',
+ 'page.goto',
+ 'After Hooks',
+ ' fixture: page',
+ ' fixture: context',
+ ]);
+});
+
+test('should record trace after fixture teardown timeout', async ({ runInlineTest }) => {
+ test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30718' });
+
+ const result = await runInlineTest({
+ 'a.spec.ts': `
+ import { test as base, expect } from '@playwright/test';
+ const test = base.extend({
+ fixture: async ({}, use) => {
+ await use('foo');
+ await new Promise(() => {});
+ },
+ })
+ test('fails', async ({ fixture, page }) => {
+ await page.evaluate(() => console.log('from the page'));
+ });
+ `,
+ }, { trace: 'on', timeout: '4000' });
+ expect(result.exitCode).toBe(1);
+ expect(result.failed).toBe(1);
+
+ const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.zip');
+ const trace = await parseTrace(tracePath);
+ expect(trace.actionTree).toEqual([
+ 'Before Hooks',
+ ' fixture: fixture',
+ ' fixture: browser',
+ ' browserType.launch',
+ ' fixture: context',
+ ' browser.newContext',
+ ' fixture: page',
+ ' browserContext.newPage',
+ 'page.evaluate',
+ 'After Hooks',
+ ' fixture: page',
+ ' fixture: context',
+ ' fixture: fixture',
+ 'Worker Cleanup',
+ ' fixture: browser',
+ ]);
+ // Check console events to make sure that library trace is recorded.
+ expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
+});
+
+test('should take a screenshot-on-failure in workerStorageState', async ({ runInlineTest }) => {
+ test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30959' });
+
+ const result = await runInlineTest({
+ 'playwright.config.ts': `
+ export default {
+ use: {
+ screenshot: 'only-on-failure',
+ },
+ };
+ `,
+ 'a.spec.ts': `
+ import { test as base, expect } from '@playwright/test';
+ const test = base.extend({
+ storageState: ({ workerStorageState }, use) => use(workerStorageState),
+ workerStorageState: [async ({ browser }, use) => {
+ const page = await browser.newPage({ storageState: undefined });
+ await page.setContent('hello world!');
+ throw new Error('Failed!');
+ await use(undefined);
+ }, { scope: 'worker' }],
+ })
+ test('fail', async ({ page }) => {
+ });
+ `,
+ });
+ expect(result.exitCode).toBe(1);
+ expect(result.failed).toBe(1);
+ expect(fs.existsSync(test.info().outputPath('test-results', 'a-fail', 'test-failed-1.png'))).toBeTruthy();
+});
diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh
index 1454e2d9ad..975c66446b 100755
--- a/utils/build/build-playwright-driver.sh
+++ b/utils/build/build-playwright-driver.sh
@@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
-NODE_VERSION="20.13.1" # autogenerated via ./update-playwright-driver-version.mjs
+NODE_VERSION="20.14.0" # autogenerated via ./update-playwright-driver-version.mjs
cd "$(dirname "$0")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
diff --git a/utils/doclint/documentation.js b/utils/doclint/documentation.js
index b93998bd33..7972ba507e 100644
--- a/utils/doclint/documentation.js
+++ b/utils/doclint/documentation.js
@@ -865,6 +865,8 @@ function csharpOptionOverloadSuffix(option, type) {
case 'function': return 'Func';
case 'Buffer': return 'Byte';
case 'Serializable': return 'Object';
+ case 'int': return 'Int';
+ case 'Date': return 'Date';
}
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
}
diff --git a/utils/generate_injected.js b/utils/generate_injected.js
index eccd7ddcad..a39583ab38 100644
--- a/utils/generate_injected.js
+++ b/utils/generate_injected.js
@@ -50,6 +50,12 @@ const injectedScripts = [
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
+ [
+ path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'fakeTimers.ts'),
+ path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'),
+ path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
+ true,
+ ],
[
path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'injected', 'index.ts'),
path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'),