kunai-runner 6.10.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.env.example +68 -0
  2. package/README.md +17 -0
  3. package/docs/01_shibboleth_auth.md +122 -0
  4. package/docs/02_versioning_and_release.md +146 -0
  5. package/docs/backlog.md +71 -0
  6. package/docs/combine_videos.md +109 -0
  7. package/docs/test_data.md +34 -0
  8. package/lib/auth-file.ts +22 -0
  9. package/lib/login-adapters/builtin.ts +42 -0
  10. package/lib/login-adapters/duo-mfa.ts +41 -0
  11. package/lib/login-adapters/incommon-seamlessaccess.ts +56 -0
  12. package/lib/login-adapters/index.ts +68 -0
  13. package/lib/login-adapters/shibboleth-direct.ts +34 -0
  14. package/lib/login-adapters/types.ts +21 -0
  15. package/package.json +51 -0
  16. package/playwright.config.ts +206 -0
  17. package/scripts/combine_videos.py +250 -0
  18. package/tests/suite/01-preflight.spec.ts +147 -0
  19. package/tests/suite/02-account-management.spec.ts +113 -0
  20. package/tests/suite/03-create-dataverse.spec.ts +114 -0
  21. package/tests/suite/04-publish-dataverse.spec.ts +33 -0
  22. package/tests/suite/05-theme-widgets.spec.ts +65 -0
  23. package/tests/suite/06-theme-widgets-edit.spec.ts +60 -0
  24. package/tests/suite/10-assign-user-group-roles.spec.ts +34 -0
  25. package/tests/suite/11-create-edit-metadata-template.spec.ts +61 -0
  26. package/tests/suite/12-create-dataverse-collection.spec.ts +27 -0
  27. package/tests/suite/13-dataset-actions.spec.ts +105 -0
  28. package/tests/suite/14-browse-dataset-records.spec.ts +32 -0
  29. package/tests/suite/15-search-dataset-records.spec.ts +26 -0
  30. package/tests/suite/16-view-dataset-version-history.spec.ts +28 -0
  31. package/tests/suite/17-download-dataset-files.spec.ts +35 -0
  32. package/tests/suite/assets/footer.png +0 -0
  33. package/tests/suite/assets/logo.png +0 -0
  34. package/tests/suite/assets/thumbnail.png +0 -0
  35. package/tests/suite/auth.setup.ts +71 -0
  36. package/tests/suite/s02-state.ts +35 -0
  37. package/tests/suite/test-data/replaced-sample-dataset-file.txt +7 -0
  38. package/tests/suite/test-data/sample-dataset-file-2.txt +7 -0
  39. package/tests/suite/test-data/sample-dataset-file.txt +7 -0
  40. package/tests/suite/tsconfig.json +10 -0
  41. package/tsconfig.json +15 -0
package/.env.example ADDED
@@ -0,0 +1,68 @@
1
+ # =============================================================================
2
+ # Dataverse VAST Suite — Environment Variables
3
+ # =============================================================================
4
+ # Copy this file to .env and fill in your values.
5
+ # The .env file is gitignored and will never be committed.
6
+
7
+ # -----------------------------------------------------------------------------
8
+ # Base URL (no trailing slash)
9
+ # -----------------------------------------------------------------------------
10
+ # The single endpoint the entire suite will run against.
11
+ BASE_URL=https://your-dataverse.example.edu
12
+
13
+ # -----------------------------------------------------------------------------
14
+ # Credentials
15
+ # -----------------------------------------------------------------------------
16
+ # Login credentials for the account used to run the test suite.
17
+ # These replace the previous sensitive-data/user.json file.
18
+ #
19
+ # DV_USERNAME — username or email for the Dataverse account
20
+ # DV_PASSWORD — password for the Dataverse account
21
+ # DV_FULL_NAME — display name shown in the Dataverse navbar after login
22
+ # (used to detect an existing authenticated session)
23
+ #
24
+ DV_USERNAME=your-username
25
+ DV_PASSWORD=your-password
26
+ DV_FULL_NAME=Your Full Name
27
+
28
+ # -----------------------------------------------------------------------------
29
+ # Login Adapter
30
+ # -----------------------------------------------------------------------------
31
+ # Select the authentication flow for this endpoint.
32
+ #
33
+ # Available adapters:
34
+ # shibboleth-direct — Direct IdP dropdown on the Dataverse login page
35
+ # (e.g. #idpSelectSelector with UNC SSO)
36
+ # incommon-seamlessaccess — InCommon / SeamlessAccess waypoint flow
37
+ # builtin — Dataverse built-in username / password form
38
+ #
39
+ LOGIN_ADAPTER=incommon-seamlessaccess
40
+
41
+ # -----------------------------------------------------------------------------
42
+ # Root dataverse collection path (optional)
43
+ # -----------------------------------------------------------------------------
44
+ # Path used by the @standard "Create Dataverse" test as the parent collection.
45
+ # Default: /dataverse/unc
46
+ # Set to "/" for instances whose root IS the top-level dataverse.
47
+ ROOT_DATAVERSE=/dataverse/unc
48
+
49
+ # -----------------------------------------------------------------------------
50
+ # Adapter-specific options (optional — sensible defaults are built in)
51
+ # -----------------------------------------------------------------------------
52
+
53
+ # shibboleth-direct: the <option> value to select in #idpSelectSelector
54
+ # IDP_SELECTOR_VALUE=https://sso.unc.edu/idp
55
+
56
+ # incommon-seamlessaccess: text to type into the SeamlessAccess search box
57
+ # INCOMMON_INSTITUTION_SEARCH=chapel hill
58
+
59
+ # incommon-seamlessaccess: accessible name (or partial) of the institution link
60
+ # INCOMMON_INSTITUTION_LINK=University of North Carolina
61
+
62
+ # -----------------------------------------------------------------------------
63
+ # Preflight control
64
+ # -----------------------------------------------------------------------------
65
+ # Set to "true" to skip the preflight project dependency when the target
66
+ # endpoint does not have the standard UNC Dataverse header/footer checks
67
+ # (e.g. when BASE_URL points at a 21 CFR instance).
68
+ # SKIP_PREFLIGHT=true
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # kunai-runner
2
+ High-performance Dataverse Playwright frontend testing framework and E2E automation scaffolding.
3
+
4
+ kunai-runner is the foundational open-source automation engine and testing scaffolding for IQSS Dataverse. Built for speed, reliability, and developer ergonomics, it provides the core test runner, DOM assertion utilities, and CI/CD integration pipelines needed to validate complex frontend architectures. Designed to be highly extensible, it serves as the close-quarters framework for writing, structuring, and executing robust end-to-end web UI tests.
5
+
6
+ ## Steps to Use Kunai Runner
7
+ 1. Clone the git repository into an empty folder
8
+ 2. `cd` into the working directory (the root directory where `playwright.config.ts` exists)
9
+ 3. `cp .env.example .env`
10
+ 4. Fill `.env` with your installation-specific values:
11
+ - `BASE_URL` — the Dataverse instance URL
12
+ - `DV_USERNAME` / `DV_PASSWORD` / `DV_FULL_NAME` — login credentials and navbar display name
13
+ - `LOGIN_ADAPTER` — authentication flow (`shibboleth-direct`, `incommon-seamlessaccess`, or `builtin`)
14
+ - See `.env.example` for all available options
15
+ 5. `npm install`
16
+ 6. `npx playwright install`
17
+ 7. `npx playwright test`
@@ -0,0 +1,122 @@
1
+ Title: "01. Authentication Adapters"
2
+ Author: "Snehashish Reddy Manda"
3
+ Email: "msreddy@unc.edu"
4
+ Date: "June 2026"
5
+ ```
6
+
7
+ # 01. Authentication Adapters
8
+
9
+ The unified VAST Suite supports multiple authentication flows via **login adapters**. The active adapter is selected by setting `LOGIN_ADAPTER` in `.env` — no source code changes required.
10
+
11
+ ---
12
+
13
+ ## Available Adapters
14
+
15
+ | Adapter | `LOGIN_ADAPTER` value | When to use |
16
+ |---|---|---|
17
+ | **Shibboleth Direct** | `shibboleth-direct` | Dataverse instances with a custom IdP dropdown (`#idpSelectSelector`) on the login page. Used for HPO / 21 CFR instances at UNC. |
18
+ | **InCommon / SeamlessAccess** | `incommon-seamlessaccess` | Standard Dataverse instances accessed via the InCommon federation ("Log In via Your Institution" → SeamlessAccess waypoint). |
19
+ | **Built-in** | `builtin` | Dataverse instances with the built-in username/password form enabled. Useful for local dev or non-federated instances. |
20
+
21
+ ---
22
+
23
+ ## Shibboleth Direct Flow (`shibboleth-direct`)
24
+
25
+ Used by HPO Dataverse and similar Shibboleth-protected instances.
26
+
27
+ 1. Navigate to the Dataverse homepage
28
+ 2. Click **Log In**
29
+ 3. Select `https://sso.unc.edu/idp` from the `#idpSelectSelector` dropdown
30
+ 4. Click **Continue**
31
+ 5. Fill in ONYEN in the username field → click **Next**
32
+ 6. Fill in password → click **Submit**
33
+ 7. Handle Duo 2FA (see below)
34
+ 8. Land back on Dataverse as the authenticated user
35
+
36
+ The IdP selector value defaults to `https://sso.unc.edu/idp`. Override with `IDP_SELECTOR_VALUE` in `.env` for other institutions.
37
+
38
+ ---
39
+
40
+ ## InCommon / SeamlessAccess Flow (`incommon-seamlessaccess`)
41
+
42
+ Used by the standard UNC Dataverse instance.
43
+
44
+ 1. Navigate to the Dataverse homepage
45
+ 2. Click **Log In**
46
+ 3. Click **Log In via Your Institution**
47
+ 4. Redirect to InCommon waypoint → click the SeamlessAccess button
48
+ 5. Search for institution (default: `chapel hill`) → click the result link
49
+ 6. Redirect to UNC SSO → fill ONYEN and password
50
+ 7. Handle Duo 2FA (see below)
51
+ 8. Land back on Dataverse as the authenticated user
52
+
53
+ Override the institution search text with `INCOMMON_INSTITUTION_SEARCH` and the institution link name with `INCOMMON_INSTITUTION_LINK` in `.env`.
54
+
55
+ ---
56
+
57
+ ## Duo MFA Challenge
58
+
59
+ Both Shibboleth-based flows may encounter a Duo 2FA challenge after password submission. The shared [`lib/login-adapters/duo-mfa.ts`](../lib/login-adapters/duo-mfa.ts) helper handles both branches automatically:
60
+
61
+ - **(A) "Yes, trust this browser" button appears** — click it and wait for redirect back to Dataverse
62
+ - **(B) Device already trusted** — Duo auto-redirects, nothing to do
63
+
64
+ ### Why choose "Yes" (trusted device)?
65
+
66
+ Clicking **Yes** grants a **7-day** Duo session cookie. Clicking **No** gives a 24-hour cookie.
67
+
68
+ Because the test suite always re-injects saved cookies before checking whether login is needed (see [Auth State Persistence](#auth-state-persistence) below), the longer cookie lifetime means you only need to manually approve a Duo push **once every 7 days**, regardless of how many individual tests you run.
69
+
70
+ > **Tip:** Anecdotally, Duo cookies become flaky before the full 7 days expire. Deleting `playwright/.auth/<endpoint-slug>.json` and re-authenticating every 24 hours is the most reliable approach when running tests frequently.
71
+
72
+ ---
73
+
74
+ ## Auth State Persistence
75
+
76
+ The auth setup step ([`tests/suite/auth.setup.ts`](../tests/suite/auth.setup.ts)) stores session cookies in a per-endpoint file:
77
+
78
+ ```
79
+ playwright/.auth/<endpoint-slug>.json
80
+ ```
81
+
82
+ The slug is derived from `BASE_URL` by stripping the scheme and replacing non-alphanumeric characters with dashes:
83
+
84
+ ```
85
+ https://dataverse-plus-staging.rdmc.unc.edu
86
+ → playwright/.auth/dataverse-plus-staging-rdmc-unc-edu.json
87
+ ```
88
+
89
+ **Before each run** the setup step:
90
+ 1. Loads the saved cookies (if the file exists) into the browser context
91
+ 2. Navigates to the homepage
92
+ 3. Checks whether `DV_FULL_NAME` is visible inside the navbar user display element
93
+ 4. If visible → session is live, authentication skipped
94
+ 5. If not visible → runs the full adapter login flow and saves fresh cookies
95
+
96
+ This means the Duo push only needs to be approved once per valid cookie window.
97
+
98
+ ---
99
+
100
+ ## Credentials
101
+
102
+ Credentials are supplied via `.env` (gitignored, never committed):
103
+
104
+ ```dotenv
105
+ DV_USERNAME=your-onyen
106
+ DV_PASSWORD=your-password
107
+ DV_FULL_NAME=Your Full Name
108
+ ```
109
+
110
+ - `DV_USERNAME` / `DV_PASSWORD` — credentials for the target Dataverse instance via the configured adapter
111
+ - `DV_FULL_NAME` — the display name Dataverse shows in the navbar after login (used to detect an existing session)
112
+
113
+ ---
114
+
115
+ ## Forcing a Fresh Login
116
+
117
+ Delete the endpoint's auth file and re-run:
118
+
119
+ ```bash
120
+ rm -f playwright/.auth/dataverse-plus-staging-rdmc-unc-edu.json
121
+ npx playwright test
122
+ ```
@@ -0,0 +1,146 @@
1
+ Title: "02. Versioning & Release"
2
+ Author: "Snehashish Reddy Manda"
3
+ Email: "msreddy@unc.edu"
4
+ Date: "June 2026"
5
+ ```
6
+
7
+ # 02. Versioning & Release
8
+
9
+ This document covers the versioning scheme, how releases are published to npm, and how downstream consumers pin to a specific Dataverse version.
10
+
11
+ ---
12
+
13
+ ## Versioning Philosophy
14
+
15
+ `kunai-runner` tracks the Dataverse version it is designed to test. This makes it easy for an instance operator to find the correct suite for their deployment without reading changelogs.
16
+
17
+ ### Git tag format
18
+
19
+ ```
20
+ DV_MAJOR.DV_MINOR.DV_PATCH.SUITE_PATCH
21
+ ```
22
+
23
+ | Segment | Meaning |
24
+ |---|---|
25
+ | `DV_MAJOR.DV_MINOR.DV_PATCH` | The Dataverse release this suite targets (mirrors the upstream version tag) |
26
+ | `SUITE_PATCH` | Increments with each new kunai-runner release for the same Dataverse version (new tests, bug fixes, etc.) |
27
+
28
+ **Examples:**
29
+
30
+ | Git tag | Meaning |
31
+ |---|---|
32
+ | `6.10.1.1` | First kunai-runner release for Dataverse 6.10.1 |
33
+ | `6.10.1.2` | Second kunai-runner release, still targeting Dataverse 6.10.1 |
34
+ | `6.11.0.1` | First kunai-runner release for Dataverse 6.11.0 |
35
+
36
+ ---
37
+
38
+ ## npm Versioning
39
+
40
+ npm only accepts 3-part semver (`MAJOR.MINOR.PATCH`). The [publish workflow](../.github/workflows/publish.yml) automatically derives a valid npm version from the 4-part Git tag:
41
+
42
+ ```
43
+ npm version = DV_MAJOR.DV_MINOR.(DV_PATCH × 100 + SUITE_PATCH)
44
+ ```
45
+
46
+ | Git tag | npm version |
47
+ |---|---|
48
+ | `6.10.1.1` | `6.10.101` |
49
+ | `6.10.1.2` | `6.10.102` |
50
+ | `6.10.1.10` | `6.10.110` |
51
+ | `6.11.0.1` | `6.11.1` |
52
+ | `6.11.0.2` | `6.11.2` |
53
+
54
+ > **Constraint:** `SUITE_PATCH` must stay below 100 (i.e. up to 99 suite releases per Dataverse version) to avoid collisions with the next `DV_PATCH`.
55
+
56
+ The `version` field in [`package.json`](../package.json) is kept at `0.0.0` and is overwritten in-place by the workflow before packing. **Never edit it manually.**
57
+
58
+ ---
59
+
60
+ ## dist-tags
61
+
62
+ In addition to the versioned npm publish, the workflow moves a **dist-tag** that always points at the latest kunai-runner build for a given Dataverse version.
63
+
64
+ | dist-tag | Resolves to |
65
+ |---|---|
66
+ | `dv-6.10.1` | Latest `6.10.1.x` suite build |
67
+ | `dv-6.11.0` | Latest `6.11.0.x` suite build |
68
+ | `latest` | Absolute latest suite build across all DV versions |
69
+
70
+ Inspect the full mapping at any time:
71
+
72
+ ```bash
73
+ npm dist-tag ls kunai-runner
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Installing
79
+
80
+ ```bash
81
+ # Latest suite for a specific Dataverse version (recommended for pinned instances)
82
+ npm install kunai-runner@dv-6.10.1
83
+
84
+ # Absolute latest (always up to date)
85
+ npm install kunai-runner
86
+
87
+ # Exact npm version (maximum reproducibility)
88
+ npm install kunai-runner@6.10.101
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Release Workflow
94
+
95
+ ### Prerequisites (one-time setup)
96
+
97
+ 1. Create an npm **Classic Automation Token** at https://www.npmjs.com/settings/~/tokens
98
+ 2. Add it as a GitHub Actions secret named `NPM_TOKEN`:
99
+ **GitHub repo → Settings → Secrets and variables → Actions → New repository secret**
100
+
101
+ ### Publishing a new release
102
+
103
+ ```bash
104
+ # 1. Commit and push all changes
105
+ git push origin main
106
+
107
+ # 2. Create and push the 4-part tag
108
+ git tag 6.10.1.1
109
+ git push origin 6.10.1.1
110
+ ```
111
+
112
+ GitHub Actions automatically:
113
+ - Derives the 3-part npm version (`6.10.101`)
114
+ - Overwrites `package.json` version in the checkout
115
+ - Runs `npm ci`
116
+ - Publishes with provenance attestation
117
+ - Moves the `dv-6.10.1` dist-tag to the new version
118
+
119
+ Watch the run under **Actions** in the GitHub repository.
120
+
121
+ ---
122
+
123
+ ## Recovery: Bad Release
124
+
125
+ npm versions are immutable after 72 hours. If a bad version ships:
126
+
127
+ **1. Immediately move `latest` back to the last good version:**
128
+
129
+ ```bash
130
+ npm dist-tag add kunai-runner@6.10.101 latest
131
+ ```
132
+
133
+ **2. Deprecate the bad version:**
134
+
135
+ ```bash
136
+ npm deprecate kunai-runner@6.10.102 "Breaking change — use dv-6.10.1 or upgrade to 6.10.103"
137
+ ```
138
+
139
+ **3. Fix, commit, and tag a new patch:**
140
+
141
+ ```bash
142
+ git tag 6.10.1.3
143
+ git push origin 6.10.1.3
144
+ ```
145
+
146
+ The `dv-6.10.1` dist-tag will automatically move to `6.10.103` once the workflow completes.
@@ -0,0 +1,71 @@
1
+ # Test Automation Backlog
2
+
3
+ > Tracks test cases that are **out of scope for Playwright**, **not suitable for automation**, or **deferred** due to complexity or sprint constraints.
4
+ > Last updated: June 2026
5
+
6
+ ---
7
+
8
+ ## Legend
9
+
10
+ | Badge | Meaning |
11
+ |-------|---------|
12
+ | 🚫 **Not Automatable** | Requires human judgement, physical hardware, or a third-party system that cannot be driven programmatically |
13
+ | 🧩 **Playwright Limitation** | Technically automatable but blocked by a known Playwright constraint (e.g. file-system dialogs, browser extensions, OAuth pop-ups) |
14
+ | ⏳ **Deferred** | In scope and automatable — deprioritized due to time, complexity, or dependency on a feature not yet stable |
15
+ | ✅ **Resolved** | Was backlogged; now covered |
16
+
17
+ ---
18
+
19
+ ## @standard Tests
20
+
21
+ ### Preflight
22
+
23
+ | # | Test Case | Badge | Reason / Notes | Priority |
24
+ |---|-----------|-------|----------------|----------|
25
+ | S-PF-01 | **Contact Email** — Submit a support email via the Support link (navbar) and the Contact link (landing page); verify delivery in the TDX UNC Dataverse support portal inbox; verify the "Contact us for support at…" address | 🚫 **Not Automatable** | Submitting real tickets against production TDX endpoints via automated tooling would risk triggering Falcon Sensor / DDoS protection on production infrastructure. Layout instability of the external portal makes reliable selectors impractical. Manual verification recommended on each release cycle. | Low — manual only |
26
+
27
+ ### Account Creation + Management
28
+
29
+ | # | Test Case | Badge | Reason / Notes | Priority |
30
+ |---|-----------|-------|----------------|----------|
31
+ | S-AC-01 | **Log in using Username/Email** — Authenticate with a native Dataverse username/email credential | ⏳ **Deferred** | A `builtin` login adapter is implemented (`lib/login-adapters/builtin.ts`) but has not been verified against a live instance. Once confirmed working, set `LOGIN_ADAPTER=builtin` in `.env` to activate this flow. | Low |
32
+ | S-AC-02 | **Log in using ORCID** — Authenticate via the ORCID OAuth provider | ⏳ **Deferred** | ORCID uses a full OAuth redirect flow through an external IdP. Automating it reliably requires either intercepting the OAuth callback (brittle) or scripting the ORCID login page itself (subject to ORCID's bot-detection and ToS). Not impossible — deprioritized. | Low |
33
+
34
+ ### Dataverse Management
35
+
36
+ | # | Test Case | Badge | Reason / Notes | Priority |
37
+ |---|-----------|-------|----------------|----------|
38
+ | S-DM-01 | **Dataverse Contact Email** — Submit a contact message via the contact popup on a Dataverse collection page; verify the relay email is delivered to the collection author without exposing their address | 🧩 **Playwright Limitation** | The contact popup is protected by a server-side arithmetic CAPTCHA (e.g. "What is 2 + 8?") introduced specifically to prevent automated abuse of the email-relay endpoint. Solving it programmatically requires either OCR/DOM-scraping the challenge value at runtime or patching the CAPTCHA out in a test environment — neither is practical against production. Not automatable on the live environment. | Low — manual only |
39
+ | S-DM-02 | **Theme + Widgets — Header/Link/Text Color Selection** — Click the three color-picker checkboxes on the Theme customization form, select a specific header color, link color, and text color, then save and verify the changes are reflected | 🧩 **Playwright Limitation** | Each checkbox opens a full 2D color-canvas + 1D hue-slider RGB palette. Playwright has no native color-picker API; targeting a precise color requires computing pixel coordinates on the canvas and dispatching raw mouse events — fragile across browser/OS rendering differences. The remaining Theme + Widgets customization (logo, footer, etc.) is covered by tests 05 and 06. | Medium — deferred to future sprint |
40
+
41
+ ### Permissions
42
+
43
+ | # | Test Case | Badge | Reason / Notes | Priority |
44
+ |---|-----------|-------|----------------|----------|
45
+ | S-PM-01 | **Permissions — Edit Access & Cross-Account Verification** — Change the Dataverse collection's default access settings (e.g. restrict to specific users/groups), then log in as a second account that was not used for collection creation and verify the permission change is enforced | 🚫 **Not Automatable** (this sprint) | The suite is built around a single authenticated session. Multi-account scenarios require either a second identity or full login/logout cycles with careful cookie and storage-state management. All Permissions sub-tests are out of scope for this sprint. | High — revisit when multi-account auth is available |
46
+
47
+ ---
48
+
49
+ ## @21cfr Tests
50
+
51
+ | # | Test Case | Badge | Reason / Notes | Priority |
52
+ |---|-----------|-------|----------------|----------|
53
+ | — | _No items yet_ | — | — | — |
54
+
55
+ ---
56
+
57
+ ## Cross-Suite / General
58
+
59
+ | # | Test Case | Badge | Reason / Notes | Priority |
60
+ |---|-----------|-------|----------------|----------|
61
+ | — | _No items yet_ | — | — | — |
62
+
63
+ ---
64
+
65
+ ## Resolved Items
66
+
67
+ > Items moved here once automation coverage is added.
68
+
69
+ | # | Test Case | Resolution | Closed |
70
+ |---|-----------|------------|--------|
71
+ | X-CS-01 | **Unified single-suite architecture** — Merge the `standard` and `21cfrpart11` suites into one suite driven by single `BASE_URL`, `LOGIN_ADAPTER`, and `ROOT_DATAVERSE` env vars. | ✅ Completed in commit `f89862c`. All specs now live in `tests/suite/`. Three Playwright projects: `preflight`, `setup`, `suite`. Tests tagged `@standard` / `@21cfr` for per-profile filtering. | June 2026 |
@@ -0,0 +1,109 @@
1
+ # Combining Test-Run Videos into a Single MP4
2
+
3
+ Playwright records a separate `.webm` clip for every test it runs.
4
+ The [`scripts/combine_videos.py`](../scripts/combine_videos.py) utility stitches all of those clips into one continuous MP4 file suitable for sharing with leadership or stakeholders.
5
+
6
+ ---
7
+
8
+ ## Prerequisites
9
+
10
+ | Requirement | Install |
11
+ |-------------|---------|
12
+ | Python 3.8+ | ships with macOS / available via `brew install python` |
13
+ | ffmpeg | `brew install ffmpeg` (macOS) · `sudo apt install ffmpeg` (Ubuntu) |
14
+
15
+ > The script uses only Python standard-library modules (`os`, `pathlib`, `subprocess`, `tempfile`, `argparse`) — no `pip install` required.
16
+
17
+ ---
18
+
19
+ ## Quick Start
20
+
21
+ Run the Playwright suite first so that `test-results/` is populated, then:
22
+
23
+ ```bash
24
+ # Unified suite → test-results/suite_combined.mp4
25
+ python scripts/combine_videos.py --suite suite
26
+
27
+ # Custom output filename
28
+ python scripts/combine_videos.py --suite suite --output leadership_demo.mp4
29
+
30
+ # Absolute path
31
+ python scripts/combine_videos.py --suite suite --output ~/Desktop/demo.mp4
32
+ ```
33
+
34
+ ---
35
+
36
+ ## CLI Reference
37
+
38
+ ```
39
+ python scripts/combine_videos.py [OPTIONS]
40
+
41
+ Options:
42
+ --suite NAME REQUIRED. Playwright project name to collect videos for.
43
+ For the unified suite use 'suite'.
44
+ Default output: test-results/<suite>_combined.mp4
45
+ --output FILE Custom output filename.
46
+ Relative paths resolve inside test-results/.
47
+ Absolute paths save wherever you point.
48
+ --results-root DIR Override the test-results root directory.
49
+ Default: <repo-root>/test-results
50
+ -h, --help Show this help message and exit.
51
+ ```
52
+
53
+ ### Examples
54
+
55
+ ```bash
56
+ # Default output
57
+ python scripts/combine_videos.py --suite suite
58
+
59
+ # Custom relative output (saved inside test-results/)
60
+ python scripts/combine_videos.py --suite suite --output leadership_demo.mp4
61
+
62
+ # Absolute output path
63
+ python scripts/combine_videos.py --suite suite --output ~/Desktop/leadership_demo.mp4
64
+
65
+ # Point at a CI artifact download
66
+ python scripts/combine_videos.py --suite suite --results-root /tmp/downloaded-results
67
+ ```
68
+
69
+ ---
70
+
71
+ ## How It Works
72
+
73
+ 1. **Discovery** — `os.walk()` recursively scans `test-results/` and collects every `.webm` file whose parent folder **ends with** `-<suite>`. Playwright names result folders `<spec-slug>-<test-title>-<project>`, so the project name is always the suffix.
74
+ 2. **Ordering** — Files are sorted by their full path, which preserves the lexicographic spec order (`01-preflight`, `02-account-management`, `10-assign-…`, etc.).
75
+ 3. **Concat list** — A temporary [ffconcat](https://ffmpeg.org/ffmpeg-formats.html#concat-1) text file is written listing all clips in order. Using the concat *demuxer* (rather than the concat *filter*) keeps the command simple and handles clips with different durations gracefully.
76
+ 4. **Encode** — ffmpeg runs a single-pass re-encode:
77
+
78
+ | Setting | Value | Rationale |
79
+ |---------|-------|-----------|
80
+ | Video codec | `libx264` (H.264) | Universal compatibility |
81
+ | Quality | CRF 22 | Good balance of quality vs. file size |
82
+ | Preset | `fast` | Reasonable encode time on a laptop |
83
+ | Audio codec | `aac` | Standard AAC at 128 kbps |
84
+ | Container | MP4 with `+faststart` | MOOV atom moved to front for web streaming |
85
+
86
+ 5. **Cleanup** — The temporary concat list is deleted whether the encode succeeds or fails.
87
+
88
+ ---
89
+
90
+ ## Output Location
91
+
92
+ By default the finished file is written to:
93
+
94
+ ```
95
+ test-results/<suite>_combined.mp4
96
+ ```
97
+
98
+ The `test-results/` directory is already listed in [`.gitignore`](../.gitignore), so the MP4 will not be committed accidentally.
99
+
100
+ ---
101
+
102
+ ## Troubleshooting
103
+
104
+ | Symptom | Fix |
105
+ |---------|-----|
106
+ | `[ERROR] No .webm files found` | Run the Playwright suite first; make sure `test-results/` is populated with folders ending in `-suite`. |
107
+ | `[ERROR] ffmpeg not found on PATH` | Install ffmpeg (see Prerequisites above). |
108
+ | Clips appear out of order | The sort is lexicographic on the full path. The existing `01-`, `02-`, `10-` naming convention handles ordering correctly. |
109
+ | Audio/video out of sync | Some `.webm` clips may have been recorded without audio. ffmpeg will insert silence for those segments automatically. |
@@ -0,0 +1,34 @@
1
+ Title: "Test Data"
2
+ Author: "Snehashish Reddy Manda"
3
+ Email: "msreddy@unc.edu"
4
+ Date: "June 2026"
5
+ ```
6
+
7
+ # What are these data files?
8
+
9
+ There are three test data files in [`tests/suite/test-data/`](../tests/suite/test-data/) used by the `@21cfr` dataset action tests.
10
+
11
+ The content inside these files is dummy data and does not matter for the purposes of testing as long as the files themselves are not empty.
12
+
13
+ ---
14
+
15
+ ## Why do these three files matter?
16
+
17
+ They are used in the combined **Dataset Actions** test (`13-dataset-actions.spec.ts`) which covers tests #7 through #11 of the 21 CFR Part 11 compliance suite.
18
+
19
+ ### Test #7 — Create Dataset
20
+
21
+ | File | Purpose |
22
+ |---|---|
23
+ | `sample-dataset-file.txt` | First file uploaded during dataset creation |
24
+ | `sample-dataset-file-2.txt` | Second file uploaded during dataset creation |
25
+
26
+ Two files are used rather than one for a specific reason: **zip downloads**. Downloading a zip file containing multiple `.txt` files is easier to automate than downloading a single file because Dataverse renders a distinct **Download** button when two or more files are selected. That button is straightforward to target as a Playwright selector and lets the download verification test (test #15) work reliably with minimal flakiness. The button is only enabled at a minimum of two files, so we use the bare minimum to verify the user flow.
27
+
28
+ ### Test #10 — Replace File
29
+
30
+ | File | Purpose |
31
+ |---|---|
32
+ | `replaced-sample-dataset-file.txt` | Replacement for `sample-dataset-file.txt` in the existing dataset |
33
+
34
+ After the replace operation the dataset should still contain two files: `replaced-sample-dataset-file.txt` and `sample-dataset-file-2.txt`. All other dataset behavior should remain unaffected.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Derives a per-endpoint, per-browser auth storage-state file path from
3
+ * BASE_URL and an optional browser name.
4
+ *
5
+ * Example:
6
+ * BASE_URL=https://dataverse-staging.rdmc.unc.edu, browser="firefox"
7
+ * → playwright/.auth/dataverse-staging-rdmc-unc-edu-firefox.json
8
+ *
9
+ * This ensures switching BASE_URL between instances, or running against
10
+ * multiple browsers, never overwrites a previously saved session — each
11
+ * endpoint + browser combination keeps its own cookie jar.
12
+ */
13
+ export function authFilePath(browser?: string): string {
14
+ const url = (process.env.BASE_URL ?? "default").trim();
15
+ const slug = url
16
+ .replace(/^https?:\/\//, "") // strip scheme
17
+ .replace(/\/$/, "") // strip trailing slash
18
+ .replace(/[^a-z0-9]/gi, "-") // any non-alphanumeric → dash
19
+ .toLowerCase();
20
+ const suffix = browser ? `-${browser}` : "";
21
+ return `playwright/.auth/${slug}${suffix}.json`;
22
+ }
@@ -0,0 +1,42 @@
1
+ import type { Page } from "@playwright/test";
2
+ import type { LoginAdapter, LoginCredentials } from "./types";
3
+
4
+ /**
5
+ * BuiltinAdapter
6
+ *
7
+ * Logs in using Dataverse's **built-in** Username/Email form.
8
+ *
9
+ * Flow (on /loginpage.xhtml):
10
+ * 1. Click the "Username/Email" button to reveal the built-in credential form
11
+ * 2. Fill #loginForm:credentialsContainer:0:credValue (username / email)
12
+ * 3. Fill #loginForm:credentialsContainer:1:sCredValue (password)
13
+ * 4. Click the "Log In" button
14
+ * 5. Wait until the login page URL is gone
15
+ *
16
+ * NOTE: credentials for this adapter are not currently verified against a
17
+ * live instance. The selectors were provided directly from the Dataverse
18
+ * login page source.
19
+ */
20
+ export class BuiltinAdapter implements LoginAdapter {
21
+ async login(page: Page, credentials: LoginCredentials): Promise<void> {
22
+ await page.waitForURL(/loginpage/);
23
+
24
+ // Reveal the built-in username/password form
25
+ await page.getByRole("button", { name: "Username/Email" }).click();
26
+
27
+ await page
28
+ .locator("#loginForm\\:credentialsContainer\\:0\\:credValue")
29
+ .fill(credentials.username);
30
+
31
+ await page
32
+ .locator("#loginForm\\:credentialsContainer\\:1\\:sCredValue")
33
+ .fill(credentials.password);
34
+
35
+ await page.getByRole("button", { name: "Log In" }).click();
36
+
37
+ // Wait until we have left the login page
38
+ await page.waitForURL((url) => !/loginpage/.test(url.href), {
39
+ timeout: 30_000,
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,41 @@
1
+ import type { Page } from "@playwright/test";
2
+
3
+ /**
4
+ * Shared Duo MFA challenge handler.
5
+ *
6
+ * After a Shibboleth / InCommon login submits credentials, Duo may intercept
7
+ * with a push/device-trust challenge. Call this helper from any adapter that
8
+ * goes through UNC SSO (or any Duo-backed IdP) to handle both branches:
9
+ *
10
+ * (A) "Yes, trust this browser" button appears → click it and wait
11
+ * (B) Device is already trusted → Duo auto-redirects, nothing to do
12
+ */
13
+ export async function handleDuoMfa(page: Page): Promise<void> {
14
+ // Wait until we have left sso.unc.edu
15
+ await page.waitForURL((url) => !/sso\.unc\.edu/.test(url.href), {
16
+ timeout: 30_000,
17
+ });
18
+
19
+ if (!/duosecurity/.test(page.url())) return;
20
+
21
+ const yesBtn = page.getByText("Yes");
22
+ const yesAppeared = await Promise.race([
23
+ yesBtn.waitFor({ state: "visible", timeout: 30_000 }).then(() => true),
24
+ page
25
+ .waitForURL(
26
+ (url) =>
27
+ !/duosecurity/.test(url.href) && !/sso\.unc\.edu/.test(url.href),
28
+ { timeout: 30_000 },
29
+ )
30
+ .then(() => false),
31
+ ]);
32
+
33
+ if (yesAppeared) {
34
+ await yesBtn.click();
35
+ await page.waitForURL(
36
+ (url) => !/duosecurity/.test(url.href) && !/sso\.unc\.edu/.test(url.href),
37
+ { timeout: 60_000 },
38
+ );
39
+ }
40
+ // If !yesAppeared, waitForURL already resolved — nothing to do
41
+ }