Easier debugging / test-specific configurations
jhildenbiddle opened this issue ยท 20 comments
Is your feature request related to a problem? Please describe.
I typically run all tests headless, but I often have failing tests that that require visually inspecting the page. Currently, this involves the following:
- Find my
jest-playwright.config.js
file - Modify my
browsers
to include only one browser (since I generally don't need to visually inspect in multiple browsers) - Modify my
launchOptions
to setheadless:false
and occasionallydevtools:true
Once the test passes, I then have to undo all of the changes made above.
What would greatly speed up this process is a way to launch a single test in non-headless mode, possibly with devtools enabled, for one-or-more browsers and/or devices. Ideally, this would be as quick as adding .only
or .skip
to a test, although this would obviously take some pre-configuration.
Describe the solution you'd like
The verbose-but-flexible way would be to allow passing configuration options with each test. Using the existing jestPlaywright.skip
implementation as inspiration, perhaps something like this could work:
jestPlaywright.config({
browser: 'chromium',
launchOptions: {
headless: false,
devtools: true,
}
}, () => {
// Tests...
})
This could be made less obnoxious by storing the non-headless/debug configuration separately and passing it to the .config()
method:
const jestPlaywrightDebugConfig = {
browser: 'chromium',
launchOptions: {
headless: false,
devtools: true,
}
};
jestPlaywright.config(jestPlaywrightDebugConfig, () => {
// Tests...
})
Better yet, the non-headless/debug configuration could be set in jest-playwright.config.js
, allowing a much simpler .debug()
method to be used without passing additional parameters:
// jest-playwrite.config.js
module.exports = {
browsers: [
'chromium',
'firefox',
'webkit',
],
debugOptions: {
browsers: [
'chromium'
],
launchOptions: {
headless: false,
devtools: true
}
}
};
// my.test.js
jestPlaywright.debug(() => {
// Tests...
})
@jhildenbiddle thank you for your suggestion. Seems like that it can be very helpful. I'll take it
Excellent! Thanks for considering the change, @mmarkelov.
@jhildenbiddle just look through this issue and made #233
You will be able to do something like that:
test('test1', async () => {
await jestPlaywright.config({
browser: 'chromium',
launchType: "LAUNCH",
launchOptions: {
headless: false,
devtools: true,
}
}, async ({page}) => {
// page is coming from function params
await page.goto('https://github.com/')
const title = await page.title()
expect(title).toBe('Google')
})
})
So you can config playwright only inside the specific test. Personally I don't think that it's perfect decision, I just tried some another opportunities, like using jestPlaywright.config
function inside describe
, but cause it's sync. there is no ability to use it with async config function.
Also I don't know how we should config it, should we merge passing options with existing?
@jhildenbiddle also we just discussed some another ways to implement this feature with @mxschmitt
Maybe we can test it with CLI, for example:
// tests:
describe('Simple tests', () => {
beforeAll(async () => {
await page.goto('https://google.com/')
})
test('passed', async () => {
const title = await page.title()
await expect(title).toBe('Google')
})
test('failed', async () => {
const title = await page.title()
await expect(title).toBe('')
})
afterAll(async () => {
await page.close()
});
})
So you got one failed test for chromium
for example.
After it you can run only failed test with DEBUG
flag:
DEBUG=true BROWSER=chromium jest -t 'failed'
So jest-playwright
will run with some constant debug options:
launchOptions: {
headless: false,
devtools: true,
}
So you will be able to also run tests for failed describe block for example.
But you couldn't pass another playwright options with your CLI, I suppose it won't be critical
First, thank you @mmarkelov for your efforts. They are very much appreciated. Whatever we end up with, having the ability to modify configurations for describe and test blocks will be extremely helpful.
Both of the proposed implementations (new method & CLI option) are interesting because they serve different purposes: the config()
method allows initiating debug mode for individual tests within test files while the CLI option allows scripts and IDE helpers to initiate debug mode for multiple describe and/or test blocks.
Implementation 1
test('test1', async () => {
await jestPlaywright.config({
// ...
}, async ({page}) => {
// ...
})
});
This is essentially what I was hoping for. While it may be limited to single test blocks only, I still think this is approach has value because it allows me to run my tests in watch+headless mode, have a test fail, wrap the failed test in the code described above, then have access to a browser with dev tools ready for debugging. I didn't have to stop my watch task or deal with browser windows for every test after changing my main configuration. After I fix the test I just remove the jestPlaywright
method and continue working.
As stated in the first comment though, I'd want to reuse a debug configuration automatically and simplify the code I need to wrap my tests with using a separate method. Passing a config is still useful in other scenarios though (like trying a different screen size or specifying a browser), so I can see value in both jestPlaywright.config
(for adjusting the standard configuration per test) and a jestPlaywright.debug
methods (for using a separate debug-focused config). Ideally the debug()
method would also (optionally) accept a configuration as the first argument, allowing overrides of properties for the debug-focused config as well:
// Default debug configuration (stored in jest-playwright settings)
test('test1', async () => {
await jestPlaywright.debug(async ({page}) => {
// ...
})
});
// Debug configuration with `browser` override
test('test2', async () => {
await jestPlaywright.config({ browser: 'webkit' }, async ({page}) => {
// ...
})
});
// Regular configuration with `browser` override
test('test2', async () => {
await jestPlaywright.config({ browser: 'chromium' }, async ({page}) => {
// ...
})
});
Other thoughts:
-
What is the value of
this
inside of thejestPlaywright
method? My concern is thatthis
is often referred to within tests to access things like the test title, so it's important to maintain the value ofthis
fromtest()
when executing test code inside of thejestPlaywright
method. -
If the
page
object is global and theconfig()
method is synchronous (viaawait
), why do we need to receive({page})
inside theconfig
method? Why can't we do this:await jestPlaywright.debug(async () => { await page.goto('https://google.com/'); // ... })
-
Would it be possible to extend Jest's
test
object to allow something like this:// Defaults debug configuration test.jestPlaywrightDebug('test1', async () => { // ... }); // Modified debug configuration test.jestPlaywrightDebug({ /* ... */ }, 'test2', async () => { // ... }); // Modified standard configuration test.jestPlaywrightConfig({ /* ... */ }, 'test3', async () => { // ... });
Implementation 2
DEBUG=true BROWSER=chromium jest -t 'failed'
I would use this functionality to create VSCode debug scripts which already contain both the file context and (when selected) the describe/test name context, allowing me to click a button in VSCode to debug the current file or describe/test block without touching the CLI:
// .vscode/launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug current test file",
"env": {
"DEBUG": "true",
"BROWSER": "chromium"
},
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"test:jest"
],
"args": [
"--",
"-i",
"${relativeFile}",
],
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
"name": "Debug selected test name",
"env": {
"DEBUG": "true",
"BROWSER": "chromium"
},
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"test:jest"
],
"args": [
"--",
"-i",
"${relativeFile}",
"-t",
"${selectedText}"
],
"console": "integratedTerminal"
}
]
}
Obviously this hasn't been tested, but I don't see why it wouldn't work. A few additional thoughts:
-
I'm a macOS user so I can't confirm this, but I believe the CLI syntax above won't work on Windows. I'm sure there's a wy to accomplish the same thing, but I thought it was worth mentioning if/when docs are updated.
-
Using a generic
DEBUG
name for an environment variable could have unintended side effects. For example, Playwright already supports a DEBUG environment variable for debugging browser launches. Something more specific likeJEST_PLAYWRIGHT_DEBUG
would be safer even though it's more to type. -
Where would the debug settings be stored? Can I modify them (I hope so), or are they fixed (aside from the BROWSER list shown in the example)? How would I set nested options like
{ launchOptions: { headless: false, devtools: true } }
on the CLI? My preference would be define debug-focused options injest.config.js
(per #226) and just do this:JEST_PLAYWRIGHT_DEBUG=true jest -t 'failed'
Apologies for the lengthy post. Hope some of this is helpful. :)
If the page object is global and the config() method is synchronous (via await), why do we need to receive ({page}) inside the config method? Why can't we do this:
Because if it shared the same page
as with other tests, it would have to make the browser not headless for the particular test using jestPlaywright.debug
. How would it dynamically make the browser non-headless for that test, then headless again after that test is finished?
My guess is that may not be possible without some OS-specific hacking beyond the current capabilities. The easy solution is to make a new non-headless browser instance, and give you that page
.
test.jestPlaywrightDebug('test1', async () => { // ... });
An API like that would be much more ergonomic! I'd say make it just test.playwrightDebug
as we know we're in Jest already
@jhildenbiddle thank you for your comment. I think that we can start from the first implementation. It's almost done.
I think that it will be possible to use two helper functions: config and debug:
// Default debug configuration
test('test1', async () => {
await jestPlaywright.debug(async ({page}) => {
// ...
})
});
// Configuration will be used from the first argument
test('test2', async () => {
await jestPlaywright.config({ ...options }, async ({page}) => {
// ...
})
});
Would it be possible to extend Jest's
test
object
Yeah, I agree that it will be much cleaner. But I'm not sure that it's possible to override test
. I didn't find any way to do it. But I asked about this opportunity, so I'll make it if I can.
If we are talking about CLI implementation is about your playwright configuration. So you will be able to use just custom debug options. But I think that it will be possible to use jest.config.js
or jest-playwright.config.js
to pass them. Maybe something like this:
// jest-playwright.config.js
module.exports = {
...,
debugOptions: { ... }
}
Something more specific like
JEST_PLAYWRIGHT_DEBUG
would be safer even though it's more to type.
Yeah, sure!
So to sum up, I will work on the first implementation. It will have two helper functions config and debug. For now you should use them inside test
block, but if I find out any ways to extend test
I will rewrite this in this way. The next step will be some additional research to use CLI for debugging, cause there is some extra question about this implementation
Excellent! I'm happy to test an in-progress branch before release if you'd like.
Thank you for all the hard work. Much appreciated.
@jhildenbiddle I just find out the way to extend global test
. So there will be just two helpers on it:
// Defaults debug configuration
test.jestPlaywrightDebug('test1', async () => {
// ...
});
// Modified standard configuration
test.jestPlaywrightConfig({ /* ... */ }, 'test2', async () => {
// ...
});
I hope that we can merge the first version of it soon. But it will be a bit unstable - I need to implement some checks on these helpers and also I should check if test.jestPlaywrightConfig
will work fine - it should override jest-playwright
configuration
@jhildenbiddle And also I need to find out the way to run these kind of test only once if the browser
param is passed:
// jest-playwright.config.js
module.exports = {
...,
browsers: ['chromium', 'firefox'],
}
// test
// This test should run only once
test.jestPlaywrightConfig({ browser: 'chromium' }, 'test2', async () => {
// ...
});
@jhildenbiddle just published new pre-release version. You can try jest-playwright-preset@next to use this helper functions. Also I suppose that we can simplify this two functions in one. Like this:
// Will use some custom debug options
test.jestPlaywrightDebug('test2', async () => {
// ...
});
// Will use custom debug options and passed options
test.jestPlaywrightDebug({ ...options }, 'test2', async () => {
// ...
});
@mmarkelov --
That's great. I'll check out the prerelease as soon as possible and post feedback here. In the meantime, a few thoughts...
Also I suppose that we can simplify this two functions in one.
I'm not sure which two functions your referring to, but if you're referring to test.jestPlaywrightConfig
and test.jestPlaywrightDebug
keep in mind that these serve different purposes (specifically, which jest-playwright configuration they use as their base):
// DEBUG without configuration object uses `debugOptions` config
test.jestPlaywrightDebug('test1', async () => {
// ...
});
// DEBUG with configuration object merges (overrides) options with `debugOptions` config
test.jestPlaywrightDebug({ /* ... */ }, 'test2', async () => {
// ...
});
// CONFIG must pass a configuration object and merges (overrides) options with primary config
test.jestPlaywrightConfig({ /* ... */ }, 'test3', async () => {
// ...
});
Another thing to consider when extending test()
is if/how .only()
and .skip()
are handled. Ideally, these same methods would be available on both jestPlaywrightConfig
and jestPlaywrightDebug
to allow for this:
// Run specific test(s) only
test.jestPlaywrightDebug.only('test1', async () => {
// ...
});
// Skip specific test(s)
test.jestPlaywrightConfig.skip({ /* ... */ }, 'test2', async () => {
// ...
});
If these methods cannot be added to the jestPlaywrightXXX
methods, then perhaps only
and skip
options can be added to the configuration object to serve the same purpose:
// Run specific test(s) only
test.jestPlaywrightDebug({ only: true }, 'test1', async () => {
// ...
});
// Skip specific test(s)
test.jestPlaywrightConfig({ skip: true }, 'test2', async () => {
// ...
});
This is less than ideal because it presents a new method of isolating and skipping tests. Better to offer .only()
and .skip()
methods on the jestPlaywrightXXX
options if possible.
@jhildenbiddle thank for getting back.
Another thing to consider when extending test() is if/how .only() and .skip() are handled.
I also will take it in, and try to implement it in next PRs
Another thing to consider when extending
test()
is if/how.only()
and.skip()
are handled. Ideally, these same methods would be available on bothjestPlaywrightConfig
andjestPlaywrightDebug
@jhildenbiddle I'm not sure that I can understand what we need to do for this cases
@mmarkelov --
Consider how devs are able to run or skip selected tests using test.only()
or test.skip()
. This same functionality--the ability to run or skip selected tests--should be available when using test.jestPlaywrightConfig()
and test.justPlaywrightDebug()
methods.
As stated above, ideally both the skip()
and only()
methods would be available on test.jestPlaywrightXXX
methods:
// Run specific test(s) only
test.jestPlaywrightDebug.only('test1', async () => {
// ...
});
// Skip specific test(s)
test.jestPlaywrightConfig.skip({ /* ... */ }, 'test2', async () => {
// ...
});
This would require adding custom skip()
and .only()
methods to jestPlaywrightXXX
because these methods need to handle a configuration object as the first argument for jestPlaywrightConfig
and optionally as the first argument for jestPlaywrightDebug
.
Hope that helps explain a bit better.
This would require adding custom
skip()
andonly()
methods tojestPlaywright
@jhildenbiddle just added this ability in next release
@mmarkelov. Amazing. Looking forward to checking it out. I've been busy setting up and configuring jest-image-snapshot
for visual regression testing across platforms, but once that's in place I'll give the @next
release a try.
As always, thanks for your efforts and the quick turnaround!
@mmarkelov --
Just gave jest-playwright-preset@next
a test drive. Here's what I found:
-
Calling
test.jestPlaywrightConfig()
with a configuration did not have any effect (no browsers launched in non-headless mode).test.jestPlaywrightConfig( { browsers: ['chromium'], launchOptions: { headless: false }, }, 'test name', async () => { /* ... */ } );
-
Calling
test.jestPlaywrightConfig.only()
ortest.jestPlaywrightConfig.skip()
result inInvalid first argument
errors. I'm assuming these methods just aren't implemented yet. -
Calling
test.jestPlaywrightDebug()
without a configuration ordebugOptions
set had the following issue(s):- Three browser windows opened: Chromium with devTools open, Nightly (Firefox) w/o devtools, and Playwright (Webkit) w/o devtools. I'm assuming this is due to a default
debugOptions
configuration? What are these default settings? - All three open browsers were loaded with
about:blank
instead of the URL loaded in the test, so they failed to show the page content rendered in the test. The tests passed, so it seems like these browser windows just aren't properly associated with the tests. I am callingjestPlaywright.resetPage()
usingbeforeEach()
, so perhaps this is related?
test.jestPlaywrightDebug( 'test name', async () => { /* ... */ } );
- Three browser windows opened: Chromium with devTools open, Nightly (Firefox) w/o devtools, and Playwright (Webkit) w/o devtools. I'm assuming this is due to a default
-
Calling
test.jestPlaywrightDebug()
with a configuration was the same as calling it without a configuration. The custom configuration I passed was ignored.test.jestPlaywrightDebug( { browsers: ['chromium'], launchOptions: { headless: false }, }, 'test name', async () => { /* ... */ } );
-
Adding
debugOptions
tojest-playwright.config.js
had no effect on the behavior oftest.jestPlaywrightDebug()
. I would expect these options to serve as the base for alltest.jestPlaywrightDebug()
calls.// jest-playwright.config.js module.exports = { browsers: ['chromium', 'firefox', 'webkit'], debugOptions: { browsers: ['chromium'], launchOptions: { headless: false, }, }, };
-
Calling
test.jestPlaywrightDebug.only()
properly runs only the specified test. -
Calling
test.jestPlaywrightDebug.skip()
properly skips the specified test. -
The new methods falsely trigger some ESLint warnings/errors because
test.jestPlaywright___()
code is no longer seen as being nested inside of atest()
block. For example, when usingeslint-plugin-jest
andeslint-plugin-jest-playwright
allexpect()
statements insidetest.jestPlaywrightConfig()
andtest.jestPlaywrightDebug()
blocks produce the following warning/error by default:Expect must be inside of a test block. eslintjest/no-standalone-expect
@jhildenbiddle thank you for your feedback!
Calling
test.jestPlaywrightConfig()
with a configuration did not have any effect (no browsers launched in non-headless mode).
There is some problems with it right now. jest-playwright
start browser with launchServer
and for now I'm not sure that it will be able to configure it. Also jest-playwright
starts separate playwright
instance with debug
and config
helper functions. So you need pass launchType
to make this start. IDK how to make it clear for now. Maybe I should make it underhood.
test.jestPlaywrightConfig(
{
browsers: ['chromium'],
launchType: 'LAUNCH',
launchOptions: { headless: false },
},
'test name',
async () => {
/* ... */
}
);
Calling
test.jestPlaywrightConfig.only()
ortest.jestPlaywrightConfig.skip()
result inInvalid first argument errors
. I'm assuming these methods just aren't implemented yet.
This should work. I added it in rc9
test.jestPlaywrightConfig.skip({
/* your options here */
}, 'failed', async ({page}) => {
await page.goto('https://github.com/')
const title = await page.title()
await expect(title).toBe('Google')
})
Three browser windows opened: Chromium with devTools open, Nightly (Firefox) w/o devtools, and Playwright (Webkit) w/o devtools. I'm assuming this is due to a default
debugOptions
configuration? What are these default settings?
It's using this config:
const DEBUG_OPTIONS = {
launchType: 'LAUNCH',
launchOptions: {
headless: false,
devtools: true,
},
}
All three open browsers were loaded with
about:blank
instead of the URL
You should use page
from arguments
test.jestPlaywrightDebug('failed', async ({page}) => {
await page.goto('https://github.com/')
const title = await page.title()
await expect(title).toBe('Google')
})
Calling
test.jestPlaywrightDebug()
with a configuration was the same as calling it without a configuration. The custom configuration I passed was ignored.
I'll check it
Adding
debugOptions
tojest-playwright.config.js
had no effect on the behavior oftest.jestPlaywrightDebug()
. I would expect these options to serve as the base for alltest.jestPlaywrightDebug()
calls.
Yeah! There is no implementation to support debugOptions
in jest-playwright.config.js
for a while
Calling
test.jestPlaywrightDebug.only()
properly runs only the specified test.
Calling
test.jestPlaywrightDebug.skip()
properly skips the specified test.
This is expected behavior. Is it wrong?
The new methods falsely trigger some ESLint warnings/errors because
I will take a look
@mmarkelov, @jhildenbiddle
Hey mates, sorry for interrupting your discussion, but to me the entire flow looks a bit broken. I mean, you should not modify your code to enter debug mode. You should probably modify it when you actually want to fix it.
Saying that, implementing most of the API presented here is a bit overkill to me.
Let me explain how could one address the actual issue in the OP.
Instead of keeping all of your test run parameters in jest-playwright.config.js
, you should keep some/most of them in some JSON file, which will present your environment.
{
"APP_PROTOCOL": "http:",
"APP_HOST": "app.in.test",
"APP_PATH": "app/client",
"SLOW_MO": 0,
"VIEWPORT": "1920x1080",
"HEADLESS": true,
"SCREEN_FAILED_TESTS": true,
"DEFAULT_TIMEOUT": 600,
"RETRY_TIMES": 3,
"TYPING_DELAY": 0,
"BROWSERS": "chromium,firefox,webkit",
"DEVICES": "",
"TEST_NAME_PATTERN": ""
}
And you can read that JSON in your jest-playwright.config.js
, and set it's values as env vars, but only those that are not yet defined:
const defaults = require('./tests.config.json');
// push config variables to env
Object.keys(defaults).forEach(key => {
if (!process.env[key]) {
process.env[key] = defaults[key];
}
});
// here you consume your env vars, something like
const [width, height] = (process.env.VIEWPORT || '1920x1080').split('x').map(v => +v);
module.exports = {
launchOptions: {
headless: process.env.HEADLESS === 'true',
slowMo: +process.env.SLOW_MO
},
contextOptions: {
ignoreHTTPSErrors: true,
bypassCSP: true,
viewport: {
width,
height
}
},
browsers: process.env.BROWSERS ? process.env.BROWSERS.split(',') : ['chromium'],
devices: process.env.DEVICES ? process.env.DEVICES.split(',') : []
};
This approach gives you an ability to set env vars for you particular test run, and run your tests with no code modification.
set BROWSERS=chromium
set HEADLES=false
set RETRY_TIMES=0
jest -t "my test name I want to run"
So far I made zero changes to run my particular test with particular parameters. And you can parametrize your setup up to your needs.
Again, if you want to skip a test - can't you use Jest's it.skip()
, and it.only()
for selected run? Why you introduce new hooks in this runner?
And in order to inject a breakpoint, since you mentioned VSCode - it has super nice JavaScript Debug Terminal, which attaches to any Node code/process, and you can place your breakpoints directly in your test sources. No need for explicit debugger
statements or similar.
What do you think?