Playwright testing for angular-osf
Playwright/TypeScript migration of the OSF Selenium (pytest) test suite. This is
stage 1 of the migration: project scaffolding, multi-environment/multi-browser
execution, and a full port of test_login.py plus the fixtures and API calls it
(and the wider conftest.py) depend on.
npm install
npx playwright install
cp .env.example .env # then fill in credentialsEnvironment is selected via TEST_ENV (mirrors the old settings.DOMAIN), browser
via Playwright's --project flag.
| Old (pytest/Selenium) | New (Playwright) |
|---|---|
DOMAIN=test4 pytest tests/test_login.py |
npm run test:test4 / TEST_ENV=test4 npx playwright test |
DRIVER=Firefox pytest ... |
npm run test:firefox / npx playwright test --project=firefox |
# Environment shortcuts
npm run test:test # TEST_ENV=test
npm run test:test2 # TEST_ENV=test2
npm run test:test3 # TEST_ENV=test3
npm run test:test4 # TEST_ENV=test4
npm run test:stage1 # TEST_ENV=stage1 (staging.osf.io)
npm run test:stage2
npm run test:stage3 # default when TEST_ENV is unset
npm run test:stage4
npm run test:prod
# Browser shortcuts
npm run test:chromium
npm run test:firefox
npm run test:edge
npm run test:webkit # Safari engine (WebKit) - real Safari automation only exists on macOS
npm run test:all-browsers
# Combine env + browser directly
TEST_ENV=test4 npx playwright test --project=firefox
# Just the login suite
npm run test:login
# View the HTML report after a run
npm run report
# Force trace recording for this run, regardless of the config's trace setting (view after with `npm run report` or `npx playwright show-trace`)
npx playwright test --project=chromium --trace on
# Interactive UI mode (watch/run tests with timeline, DOM, network, console)
npx playwright test --ui
Tests are tagged the same way the old suite used pytest markers:
@smoke (was smoke_test), @core (was core_functionality). Filter with:
npx playwright test --grep @smokedont_run_on_prod is ported as a conditional test.skip(settings.PRODUCTION, ...)
inside a beforeEach, so those tests show as skipped (not absent) when run
against TEST_ENV=prod.
config/
environments.ts # domain map (stage1-4, test, test2, test3, test4, prod) - port of the `domains` dict in settings.py
settings.ts # env-var driven settings - port of settings.py
src/
api/
session.ts # thin JSON:API wrapper around Playwright's APIRequestContext
osfApi.ts # port of the api/osf_api.py functions the fixtures below need
pages/
BasePage.ts # port of pages/base.py (Playwright locators auto-wait, so
# base/locators.py's WebElementWrapper/Locator machinery isn't needed)
LoginPage.ts # LoginPage, Login2FAPage, LoginToSPage, InstitutionalLoginPage,
# generic institution login pages, ForgotPasswordPage,
# UnsupportedInstitutionLoginPage, GenericCASPage,
# CASAuthorizationPage, and the old_login/login/safe_login*/
# accept_cookies/logout helper functions
LandingPage.ts
RegisterPage.ts
fixtures/
index.ts # port of tests/conftest.py fixtures via Playwright's test.extend
utils/
index.ts # port of the utils.py helpers the above depend on
tests/
login.spec.ts # port of tests/test_login.py
user.spec.ts # port of tests/test_user.py
profile.spec.ts # port of tests/test_profile.py
search.spec.ts # port of tests/test_search.py
navbar.spec.ts # port of tests/test_navbar.py
registrationSidebar.spec.ts # port of tests/test_registration_sidebar.py
registrationMetadata.spec.ts # port of tests/test_registration_metadata.py
Sections mirror the checklist in the Selenium repo's PLAYWRIGHT_MIGRATION_RULES.md
(C:\PyCharmProjects\OSF\osf-selenium-tests-develop). When a section lands here,
tick it in both files.
| # | Section | Source (Selenium) | Spec here | Status |
|---|---|---|---|---|
| 1 | Login | tests/test_login.py |
tests/login.spec.ts |
[x] Migrated |
| 2 | User settings | tests/test_user.py |
tests/user.spec.ts |
[x] Migrated |
| 3 | Profile | tests/test_profile.py |
tests/profile.spec.ts |
[x] Migrated |
| 4 | Search | tests/test_search.py |
tests/search.spec.ts |
[x] Migrated |
| 5 | Navbar | tests/test_navbar.py |
tests/navbar.spec.ts |
[x] Migrated |
| 6 | Dashboard | tests/test_dashboard.py |
— | [ ] Not migrated |
| 7 | Collections | tests/test_collections.py |
— | [ ] Not migrated |
| 8 | Registration sidebar | tests/test_registration_sidebar.py |
tests/registrationSidebar.spec.ts |
[x] Migrated |
| 9 | Preprints | tests/test_preprints.py |
— | [ ] Not migrated |
| 10 | Metadata | tests/test_registration_metadata.py |
tests/registrationMetadata.spec.ts |
[x] Migrated |
| 11 | Institutions | tests/test_institutions.py |
— | [ ] Not migrated |
| 12 | My projects | tests/test_my_projects.py |
— | [ ] Not migrated |
| 13 | My registrations | tests/test_my_registrations.py |
— | [ ] Not migrated |
| 14 | My preprints | tests/test_my_preprints.py |
— | [ ] Not migrated |
| 15 | Registration moderation | tests/test_registration_moderation.py |
— | [ ] Not migrated |
| 16 | Registration user permissions | tests/test_registration_user_permissions.py |
— | [ ] Not migrated |
| 17 | Registries | tests/test_registries.py |
— | [ ] Not migrated |
Progress: 7 / 17 sections migrated.
- Cross-browser is a Playwright
project(chromium/firefox/edge/webkit) instead of SeleniumDRIVER/BrowserStack config. BrowserStack/Remote execution was not ported in this stage - only local chromium/firefox/msedge/webkit.webkitis Playwright's own WebKit engine (a Safari stand-in), not real Safari - real Safari automation only runs on macOS. driverfixture is replaced by Playwright's built-inpagefixture; there's nolaunch_driver()equivalent to maintain.- Locator waiting: Playwright locators auto-wait, so page objects expose plain
Locatorgetters instead of the customLocator/WebElementWrapperwait logic frombase/locators.py. check_credentialsused to callpytest.exit()to abort the whole run on bad credentials; here it throws inside a fixture, which fails the current test with a clear message instead of aborting the whole run.login()/safe_login()/safe_login_short()had converged on an identical implementation in the Python source (withlogin()'s extra initialgoto()being a redundant double-navigation) - consolidated into one implementation, re-exported under all three names.defaultProjectPagefixture returns a minimal{ page, guid }stub rather than a fullProjectPageport -pages/project.py(~1600 lines) is a separate, much larger page object out of scope for this login-suite stage.- OAuth API tests (
OauthAPIclass in the original) are ported but skipped: that class was never actually collected by pytest (its name didn't start withTest, so it fell outside pytest's defaultpython_classespattern) - it's been dormant in the existing suite. Ported for parity, left skipped since it exercises real token issuance/revocation. - Only the fixtures and
api/osf_api.pyfunctions thatconftest.py's fixtures actually call have been ported; the rest ofosf_api.py(~2600 lines total) covers pages/tests well outside this stage's scope. - Navbar: the old app had a distinct navbar subclass per service
(
HomeNavbar/EmberNavbar/PreprintsNavbar/RegistriesNavbar/etc. incomponents/navbars.py), each a flat list of direct links. The current app renders one persistent left sidenav shared across every section instead, sosrc/pages/components/Navbar.tsis a single class rather than a hierarchy. Some items (Registries, Preprints, My OSF, Settings) are dropdown parents now - clicking them only expands a submenu, so reaching e.g. Preprints Discover is a two-step expand-then-select instead of one direct link click.test_navbar.py'sNavbarTestLoggedOutMixin/NavbarTestLoggedInMixinare ported as plain shared functions called from eachtest.describeblock, since Playwright has no class-inheritance equivalent. SeeCLAUDE.md's "Known-flaky backend endpoints" section for a residual click-timing flake on the dropdown items. - Profile:
pages/profile.py'sProfilePage(tab/filter/sort checks reused fromsearch.spec.ts's port ofpages/search.py) andSearchPageHelpersare two separate Python objects bound to the samedriver; sinceSearchPage.tsalready carries all thecheckFilteringBy*/tab-link locators,src/pages/ProfilePage.tsjust extends it instead of re-implementing or duplicating them, so oneProfilePageinstance covers both roles.tests/test_profile.py's own_validate_project_card/etc. module-local helpers (near-identical tosearch.spec.ts'sverify*SearchCardfunctions) are likewise duplicated locally intests/profile.spec.tsrather than imported across spec files, matching how the Python source itself duplicates them per test module. - Registration sidebar:
RegistrationPage.ts(already used bysearch.spec.tsas a scoped port of the overview page) is extended in place with the rest ofpages/registries.py/components/registration.pythis section needs, rather than duplicated into a new type -search.spec.ts's existing fields are untouched. Metadata/Files/Components/Links/Analytics page objects stay identity-only, sincetest_registration_sidebar.pyonly ever navigates to them and checksidentity- their fuller Python page objects (metadata editing, file browsing, etc.) are out of scope here. Two Python dead-code bugs were fixed rather than carried over (matching the Section 3 precedent above):get_authors_list/get_affiliations_listreturnfrom inside their loop in Python, so they only ever yielded the first item -getAuthorsList/getAffiliationsListhere return the full list; andRegistrationContributorsPage's Pythonidentitylocator passes an XPath string to a CSSBy.CSS_SELECTOR(never actually matches) - ported asosf-contributorsinstead.test_delete_resource's Python assertion also checked for a lowercased<h2>that could never match real markup (an always-true assertion) - ported as a real check against the actual (capitalized) resource-type heading. Several since-fixed timing races surfaced only under real load and are documented inline where fixed: the contributors table, VOL table, and overview license/authors sections all render asynchronously after their page'sidentitymounts; the contributors search box filters with a debounce (up to ~2s) rather than instantly; and VOL/resource delete confirmations are PrimeNG'sp-confirmdialog(.p-confirmdialog-accept-button), not thep-dialogthe Add Contributor/Create VOL modals use. - Registration metadata: ports
TestRegistrationMetadata(14 tests) - the only live class intest_registration_metadata.py;TestFilesMetadata/TestProjectMetadataare fully commented out in the Python source and weren't ported. The metadata page's cards are each their own Angular component (osf-metadata-title,osf-metadata-contributors, ...) and every Edit opens a namedp-dialog, soRegistrationMetadataPagescopes by those instead of the Python//div[h2[...]]XPaths. The add/remove pairs (contributors, affiliations, subjects, tags) still rely on file order against one shared fixture registration, like the Python class. Changes from the Python source, all verified live:- The Edit contributors dialog's search box doesn't filter the table, so Python's
user_permission/remove_buttonsilently acted on the first row (the admin owner). Rows are now located by contributor name. remove contributorsonly checked the Contributors card, which lists bibliographic contributors only - and the previous test already made that user non-bibliographic, so the assertion was always true. It now also checks the API contributor list.- The API refuses to leave a registration with zero subjects (400, "Registration must
have at least one subject to be registered"), so with
Businessas the fixture's only subject the add/remove subject pair was stuck.add top level subjectnow resets the subjects toEngineeringvia the API first (osfApi.updateRegistrationSubjects). - The Resource language select's filter box always shows "No results found"
(
filterBy="label"on options that only havename/code), which looks like an app bug. The test scrolls the virtual-scrolled list to the option instead (selectFromVirtualScrollList). - Removing the only funder entry closes the Funding dialog - Python's reopen step is kept, with an explicit wait for the dialog to close first.
edit support funding informationno longer fetches its award title/URI/number from the SHARE funder index (get_funder_information). SHARE staging search can hang for minutes, and the values were only used as text to type in. The same values every run could also match what an earlier run already saved, so the test could pass even if Save did nothing. It now types unique values generated each run. The Funder Name dropdown itself searches ROR (api.ror.org), not SHARE.- Subjects/tags/affiliations save as you toggle them, so the tests wait for that save request before reloading instead of reloading straight away.
- The Edit contributors dialog's search box doesn't filter the table, so Python's