Repository: microsoft/playwright-pytest (language: python)

Issue:
[pytest-bdd] def browser_type -- getattr(): attribute name must be string
Hi there. Thanks a lot for the plugin!
Would like to try it with pytest-bdd, however got the following error:
```
    @pytest.fixture(scope="session")
    def browser_type(playwright: Playwright, browser_name: str) -> BrowserType:
>       return getattr(playwright, browser_name)
E       TypeError: getattr(): attribute name must be string
```

related step definition:
```
@given("test_playwright")
def test_playwright(page):
    pass
```

env:
```
Python: 3.9.0, 
Packages: {'pytest': '6.2.2', 'py': '1.10.0', 'pluggy': '0.13.1'}, 
Plugins:  Faker-6.5.0, progress-1.2.2, base-url-1.4.2, bdd-4.0.2, xdist-2.2.1, metadata-1.11.0, playwright-0.1.1, repeat-0.9.1, forked-1.3.0, allure-pytest-bdd-2.8.36, rerunfailures-10.0
^```

please advise
thanks!



Discussion / hints:
Are you using class-based tests like the `unittest.Testcase` class? If so, then its a duplicate of https://github.com/microsoft/playwright-pytest/issues/55
> Are you using class-based tests like the `unittest.Testcase` class? If so, then its a duplicate of #55

No, we don't.

Moreover, it works OK with adding to conftest.py browser_type fixture copy with specific browser name:
```
_conftest.py_

@pytest.fixture(scope="session")
def browser_type(playwright, browser_name: str):
    return getattr(playwright, 'chromium')
```
looks like it isn't compatible with pytest-bdd where a test consists of a set of steps (special fixtures).
it stops working when 'page' is the argument of a bdd step.

Fix patch:
diff --git a/README.md b/README.md
index 57ab39b..9338838 100644
--- a/README.md
+++ b/README.md
@@ -217,6 +217,30 @@ def context(
 
 When using that all pages inside your test are created from the persistent context.
 
+### Using with `unittest.TestCase`
+
+See the following example for using it with `unittest.TestCase`. This has a limitation,
+that only a single browser can be specified and no matrix of multiple browsers gets
+generated when specifying multiple.
+
+```py
+import pytest
+import unittest
+
+from playwright.sync_api import Page
+
+
+class MyTest(unittest.TestCase):
+    @pytest.fixture(autouse=True)
+    def setup(self, page: Page):
+        self.page = page
+
+    def test_foobar(self):
+        self.page.goto("https://microsoft.com")
+        self.page.click("#foobar")
+        assert self.page.evaluate("1 + 1") == 2
+```
+
 ## Debugging
 
 ### Use with pdb


Test patch:
diff --git a/pytest_playwright/pytest_playwright.py b/pytest_playwright/pytest_playwright.py
index 0ee236e..1ec4f86 100644
--- a/pytest_playwright/pytest_playwright.py
+++ b/pytest_playwright/pytest_playwright.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 import asyncio
+import warnings
 from asyncio import AbstractEventLoop
 from typing import Any, Callable, Dict, Generator, List, Optional
 
@@ -183,8 +184,18 @@ def is_chromium(browser_name: str) -> bool:
 
 
 @pytest.fixture(scope="session")
-def browser_name() -> None:
-    return None
+def browser_name(pytestconfig: Any) -> Optional[str]:
+    # When using unittest.TestCase it won't use pytest_generate_tests
+    # For that we still try to give the user a slightly less feature-rich experience
+    browser_names = pytestconfig.getoption("--browser")
+    if len(browser_names) == 0:
+        return "chromium"
+    if len(browser_names) == 1:
+        return browser_names[0]
+    warnings.warn(
+        "When using unittest.TestCase specifying multiple browsers is not supported"
+    )
+    return browser_names[0]
 
 
 @pytest.fixture(scope="session")
diff --git a/tests/test_playwright.py b/tests/test_playwright.py
index e09cef0..208b5ff 100644
--- a/tests/test_playwright.py
+++ b/tests/test_playwright.py
@@ -13,12 +13,11 @@
 # limitations under the License.
 
 import sys
-from typing import Any
 
 import pytest
 
 
-def test_default(testdir: Any) -> None:
+def test_default(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         import pytest
@@ -35,7 +34,7 @@ def test_default(page, browser_name):
     result.assert_outcomes(passed=1)
 
 
-def test_slowmo(testdir: Any) -> None:
+def test_slowmo(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         from time import monotonic
@@ -59,7 +58,7 @@ def test_slowmo(page):
         "msedge",
     ],
 )
-def test_browser_channel(channel: str, testdir: Any) -> None:
+def test_browser_channel(channel: str, testdir: pytest.Testdir) -> None:
     if channel == "msedge" and sys.platform == "linux":
         pytest.skip("msedge not supported on linux")
     testdir.makepyfile(
@@ -75,7 +74,7 @@ def test_browser_channel(page, browser_name, browser_channel):
     result.assert_outcomes(passed=1)
 
 
-def test_invalid_browser_channel(testdir: Any) -> None:
+def test_invalid_browser_channel(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         import pytest
@@ -89,7 +88,51 @@ def test_browser_channel(page, browser_name, browser_channel):
     assert "channel: expected one of " in "\n".join(result.outlines)
 
 
-def test_multiple_browsers(testdir: Any) -> None:
+def test_unittest_class(testdir: pytest.Testdir) -> None:
+    testdir.makepyfile(
+        """
+        import pytest
+        import unittest
+        from playwright.sync_api import Page
+
+
+        class MyTest(unittest.TestCase):
+            @pytest.fixture(autouse=True)
+            def setup(self, page: Page):
+                self.page = page
+
+            def test_foobar(self):
+                assert self.page.evaluate("1 + 1") == 2
+    """
+    )
+    result = testdir.runpytest("--browser", "chromium")
+    result.assert_outcomes(passed=1)
+
+
+def test_unittest_class_multiple_browsers(testdir: pytest.Testdir) -> None:
+    testdir.makepyfile(
+        """
+        import pytest
+        import unittest
+        from playwright.sync_api import Page
+
+
+        class MyTest(unittest.TestCase):
+            @pytest.fixture(autouse=True)
+            def setup(self, page: Page):
+                self.page = page
+
+            def test_foobar(self):
+                assert "Firefox" in self.page.evaluate("navigator.userAgent")
+                assert self.page.evaluate("1 + 1") == 2
+    """
+    )
+    result = testdir.runpytest("--browser", "firefox", "--browser", "webkit")
+    result.assert_outcomes(passed=1)
+    assert any("multiple browsers is not supported" in line for line in result.outlines)
+
+
+def test_multiple_browsers(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_multiple_browsers(page):
@@ -103,7 +146,7 @@ def test_multiple_browsers(page):
     result.assert_outcomes(passed=3)
 
 
-def test_browser_context_args(testdir: Any) -> None:
+def test_browser_context_args(testdir: pytest.Testdir) -> None:
     testdir.makeconftest(
         """
         import pytest
@@ -123,7 +166,7 @@ def test_browser_context_args(page):
     result.assert_outcomes(passed=1)
 
 
-def test_chromium(testdir: Any) -> None:
+def test_chromium(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_is_chromium(page, browser_name, is_chromium, is_firefox, is_webkit):
@@ -137,7 +180,7 @@ def test_is_chromium(page, browser_name, is_chromium, is_firefox, is_webkit):
     result.assert_outcomes(passed=1)
 
 
-def test_firefox(testdir: Any) -> None:
+def test_firefox(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_is_firefox(page, browser_name, is_chromium, is_firefox, is_webkit):
@@ -151,7 +194,7 @@ def test_is_firefox(page, browser_name, is_chromium, is_firefox, is_webkit):
     result.assert_outcomes(passed=1)
 
 
-def test_webkit(testdir: Any) -> None:
+def test_webkit(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_is_webkit(page, browser_name, is_chromium, is_firefox, is_webkit):
@@ -165,7 +208,7 @@ def test_is_webkit(page, browser_name, is_chromium, is_firefox, is_webkit):
     result.assert_outcomes(passed=1)
 
 
-def test_goto(testdir: Any) -> None:
+def test_goto(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_base_url(page, base_url):
@@ -180,7 +223,7 @@ def test_base_url(page, base_url):
     result.assert_outcomes(passed=1)
 
 
-def test_skip_browsers(testdir: Any) -> None:
+def test_skip_browsers(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         import pytest
@@ -196,7 +239,7 @@ def test_base_url(page, browser_name):
     result.assert_outcomes(passed=2, skipped=1)
 
 
-def test_only_browser(testdir: Any) -> None:
+def test_only_browser(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         import pytest
@@ -212,7 +255,7 @@ def test_base_url(page, browser_name):
     result.assert_outcomes(passed=1, skipped=2)
 
 
-def test_parameterization(testdir: Any) -> None:
+def test_parameterization(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_all_browsers(page):
@@ -235,7 +278,7 @@ def test_without_browser():
     assert "test_without_browser PASSED" in "\n".join(result.outlines)
 
 
-def test_headed(testdir: Any) -> None:
+def test_headed(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_base_url(page, browser_name):
@@ -247,7 +290,7 @@ def test_base_url(page, browser_name):
     result.assert_outcomes(passed=1)
 
 
-def test_invalid_browser_name(testdir: Any) -> None:
+def test_invalid_browser_name(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
         def test_base_url(page):
@@ -259,7 +302,7 @@ def test_base_url(page):
     assert "'test123' is not allowed" in "\n".join(result.outlines)
 
 
-def test_django(testdir: Any) -> None:
+def test_django(testdir: pytest.Testdir) -> None:
     testdir.makepyfile(
         """
     from django.test import TestCase
@@ -273,7 +316,7 @@ def test_one(self):
     result.assert_outcomes(passed=1)
 
 
-def test_device_emulation(testdir: Any) -> None:
+def test_device_emulation(testdir: pytest.Testdir) -> None:
     testdir.makeconftest(
         """
         import pytest
@@ -294,7 +337,7 @@ def test_browser_context_args(page):
     result.assert_outcomes(passed=1)
 
 
-def test_launch_persistent_context(testdir: Any) -> None:
+def test_launch_persistent_context(testdir: pytest.Testdir) -> None:
     testdir.makeconftest(
         """
         import pytest