Skip to content

Migrate to collections - #3

Open
kpenfound wants to merge 9 commits into
mainfrom
collections
Open

kpenfound wants to merge 9 commits into
mainfrom
collections

Conversation

@kpenfound

@kpenfound kpenfound commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Migrates the module to Dagger collections: Playwright projects keyed by config directory, each with a nested collection of test files.

Requires Dagger v1.0.0-beta.15 or later. The Dang SDK points at github.com/dagger/dang-sdk@collections.

Shape

  • Playwright (constructor): settings only (baseCtr, service, serviceHostname, baseImageAddress, packageManager, localhostProxy, args, shards). It no longer takes or stores the Workspace.
  • Playwright.projects(ws): PlaywrightProjects! (@collection):
    • paths (@keys): root-relative directories that hold a playwright.config.{ts,js,mjs,cjs,mts,cts}, from one Workspace.findRoots walk with node_modules and hidden directories pruned. Unique and sorted.
    • project(path) (@get), and test(ws), a plain function that runs the selected projects whole.
  • Playwright.project(ws, path): the project containing a path. It replaces sourcePath.
  • PlaywrightProject: path, tests(ws), test(ws) (plain: a whole, sharded run), report(ws), base(ws), imageAddress(ws), installDir(ws).
  • PlaywrightProject.tests(ws): PlaywrightTestFiles! (@collection):
    • files (@keys): project-relative test file paths, unique and byte-sorted;
    • delta: CollectionDelta, file(path) (@get);
    • test(ws) (@check, the batch);
    • runs: the containers the batch would start.
  • PlaywrightTestFile: path, test(ws) (@check, "Run this test file.").
  • PlaywrightRun: files, shard, label.

Only the test-file level carries checks, which avoids the double-run trap. dagger check -l --all -f=link with the module installed:

dag+check://playwright/projects/tests/test?playwright-project=fixture&playwright-test-file=tests/smoke.spec.ts
dag+check://playwright/projects/tests/test?playwright-project=fixture/nested&playwright-test-file=tests/nested.spec.ts

The flags are --playwright-project PATH, --playwright-projects, --playwright-test-file PATH and --playwright-tests. Checks are selected with --check test. base and report remain per-project dagger shell / dagger list containers|directories artifacts. There are no stale or duplicate checks.

Working-directory scoping

Project keys follow findRoots semantics:

  • Inside a project's subdirectory: exactly the enclosing project.
  • At a project root: that project and any projects below it.
  • Anywhere else: the projects below the cwd.

So cd apps/web/src && dagger check runs only apps/web. Test-file keys are the same from any cwd.

Test-file discovery (static, no containers)

Each project costs three cheap calls during listing: one Workspace.glob for its config, one Workspace.file read, and one Workspace.search (ripgrep, with node_modules and .git pruned by the search). There are no withExec calls. The search's glob is Playwright's default pattern when every testMatch is the default, and every script file otherwise. Results are then filtered in Dang. Files under a nested project's config belong to that project.

Only literal config values are honoured:

  • testDir: a string, or path.join/resolve(__dirname, '…'), resolved against the config directory.
  • testMatch / testIgnore: a string glob, a regex literal, or an array of those.
    • Globs are translated to RE2 with minimatch semantics (case-insensitive, dot, **/ prepended, {a,b}, @(), ?(), +(), *()).
    • Regexes use their JS flags and are matched against /app/<path>.
  • Per-project overrides: the same settings inside projects: [{ … }], inheriting the top level. Keys are the union across Playwright projects.
  • Where the config object is found:
    • defineConfig({…}), export default {…} or module.exports = {…};
    • export default <ident> naming a const in the file, TypeScript annotation included (React Router's shape);
    • one hop of a relative import or re-export, e.g. export { config as default } from '../../utils.js';
    • one spread, defineConfig({ ...config, webServer }), resolved the same way; the config's own keys override it (SvelteKit's shape).
  • Everything else falls back to Playwright's defaults: non-literal values (variables, env, calls, ${} templates), package imports, more than one hop, regexes RE2 rejects (lookaround, backreferences), !() globs, and unreadable or unparseable configs.
    • Parsing masks comments, strings, template strings and regex literals, then reads top-level keys by flattening nested brackets. It never raises, so a config can't break listing.
  • A project where nothing is found statically gets one key, ., which runs the whole project natively, so every project can be checked.

Runs

  • Nothing filtered out (the delta is empty): Playwright runs the project natively with its own config, one container per shard (--shard=i/N, N = shards).
  • Filtered: only the selected files run.
    • Each file is passed as ^<escaped absolute path>$, because Playwright treats positional args as case-insensitive regexes over absolute paths. I verified that anchored absolute paths select exactly one file and relative ones match nothing.
    • The files are split in key order into up to min(N, #files) contiguous, near-equal groups, one container per group, run concurrently.
    • The failure lists every failing group.
  • Empty selection: nothing runs.
  • Selecting every key of a project by name also counts as nothing filtered out, so it runs the project whole.
  • Proxy mode: the socat wrapper now passes arguments as "$@", so the shell never re-parses them.

Dependencies and image

This matches the other Node modules (prettier's implementation).

  • Install root: the nearest workspace root (a directory with pnpm-workspace.yaml, or a package.json with "workspaces"), else the nearest lockfile, else the nearest package.json.
  • Package manager: the packageManager setting (default: detect), else the "packageManager" field, else the lockfile. corepack is installed if missing; pnpm gets --store-dir explicitly.
  • Caching:
    • The install sees only its inputs: package.json files, lockfiles, workspace and package-manager config, patches/, file:/link:/portal: and injected dependency directories, and workspace bin files. The rest of the source is laid over afterwards, so editing source leaves the install CACHED.
    • The npm, pnpm, yarn, bun and corepack caches are on cache volumes.
  • Own binary: tests run the project's own node_modules/.bin/playwright, or yarn playwright under Plug'n'Play. It exits 127 with a message if missing; npx is used only when there is no package.json at all.
  • New settings: installFlags and environment (KEY=VALUE).
  • Failure messages name the step: install failed (pnpm install …, exit N): or playwright test failed (exit N):, each followed by the end of the output.
  • Image: the @playwright/test version comes from pnpm-lock.yaml, yarn.lock, bun.lock or package-lock.json. Otherwise it comes from the nearest package.json that declares it, with catalog: and catalog:<name> resolved from pnpm-workspace.yaml. Anything that isn't a version falls back to the pinned image, never to an invalid reference.
  • report runs with --reporter=list,html into a fixed directory, so the HTML report always exists.

CLI

$ dagger list playwright-test-files -a --playwright-project=apps/web
$ dagger check playwright/projects/tests/test --playwright-project=apps/web
$ dagger check playwright/projects/tests/test --playwright-project=apps/web --playwright-test-file=tests/login.spec.ts
$ cd apps/web/src && dagger check
$ dagger shell playwright/projects/base --playwright-project=apps/web
$ dagger api call playwright project --path=apps/web report export --path=./playwright-report

Breaking changes

  • The check is now playwright/projects/tests/test, with --playwright-project and --playwright-test-file. It replaces playwright:test.
  • PlaywrightProjects.test and PlaywrightProject.test are plain functions, not checks.
  • sourcePath and the constructor's ws are removed. test, report, base and imageAddress move onto each project and take ws. path and installDir are root-relative.
  • Wired settings use DAG addresses on beta.15: service = "dag://myapp/serve".
  • packageManager now defaults to detection, and installs happen at the workspace root. installDir reports that root.

Tests

The e2e module (dagger check, all passing on the released beta.15 engine):

  • check-fixture: service binding, localhost proxy, 2 shards, via the test-file batch; asserts the whole run is 1/2,2/2.
  • discovery-check: project cwd scopes; test-file keys, identical from any cwd.
  • config-check, one synthetic project per rule:
    • default patterns, literal testDir, path.join(__dirname, 'e2e');
    • testMatch glob, array and regex, and testIgnore;
    • a per-project testDir union;
    • non-literal values, a lookaround regex and an unparseable config all fall back to defaults;
    • node_modules and nested-project files are excluded;
    • SvelteKit's spread and re-export shapes, and React Router's const config: PlaywrightTestConfig;
    • an opaque config gets the . key.
  • collection-check: both collections, covering get inside and outside a subset, subset order, unknown and duplicate keys rejected, and an empty subset (no runs).
  • run-check, on a synthetic project with a failing aa.spec.ts:
    • filtered a.spec.ts passes, both via the batch and the item;
    • three files with 2 shards run as groups [a,b] and [c];
    • a failing group is reported and a passing one is not;
    • the whole run is 2 shards and fails;
    • a .-keyed project runs whole;
    • report returns index.html with a config that has no html reporter.
  • image-check (static): install root, package manager and image for a pnpm catalog, a named catalog, a pnpm lockfile, yarn.lock with workspaces, and workspace:* falling back cleanly.
  • install-run-check:
    • a pnpm workspace via corepack, with a file: dependency and a workspace bin used by the tests, and environment applied;
    • a failing install is named;
    • installFlags: ["--ignore-scripts"] gets past it;
    • a missing @playwright/test fails clearly.
    • Without the install-input fix, the file: fixture fails with Cannot find module …/local-dep/index.js.
  • Existing checks for lookup, install, projects batch and report.

I ran every README CLI example in a small scratch pnpm workspace on the released engine. I also checked caching there:

  • after a source edit, pnpm install is CACHED;
  • after an install-input change, pnpm reports reused 3, downloaded 0;
  • there are no bin-link warnings.

Field-trial config shapes. I built a scratch copy with the trial repos' real config files and test file names (no dependencies). Listing now gives:

  • SvelteKit: 39/39 expected keys, exactly (was 21, plus bogus ones);
  • React Router: 94/94 (was 0).

🤖 Generated with Claude Code

kpenfound and others added 2 commits September 25, 2026 16:17
Playwright projects become a collection keyed by config directory. The
constructor holds only settings; projects(ws) discovers every directory with a
playwright.config.{ts,js,mjs,cjs,mts,cts} at or below the cwd (plus the
enclosing one) with Workspace.findRoots, skipping node_modules and hidden
directories. Per-project logic (image derivation, install dir, service
binding, localhost proxy, sharding) moves onto PlaywrightProject; the batch
test runs the selected projects concurrently and reports every failure.

sourcePath is replaced by project(ws, path). Engine v1.0.0-beta.15 and the
dang-sdk collections branch are required. The e2e module gains a nested
fixture project and discovery, lookup, install and collection checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: kpenfound <[email protected]>
Drop the post-walk visibility filter: findRoots' excludes already prune
node_modules and hidden directories from the walk. Add e2e cases for the
three cwd scopes (project root, inside a project, outside any project), and
update the README for cwd-aware selection, discovery rules and limits, the
dev-engine requirement, and calling the collection from another module.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: kpenfound <[email protected]>
@kpenfound
kpenfound changed the base branch from migrate_1.0 to main September 28, 2026 21:25
kpenfound and others added 6 commits September 28, 2026 17:32
Resolve the dagger.lock conflict by keeping this branch's dang-sdk
collections pin and main's sdk-helpers v1.0.5 resolution.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kyle Penfound <[email protected]>
Each project's test files are a nested collection (PlaywrightTestFiles),
keyed by project-relative path from one Workspace.search per project and
filtered by the literal testDir, testMatch and testIgnore read statically
from playwright.config (per Playwright project too; anything non-literal
falls back to Playwright's defaults). Only the test-file level carries
checks: with nothing filtered out the batch runs the project natively,
sharded with --shard=i/N; a filtered run passes each file as an anchored,
escaped absolute-path regex and splits the files into up to N groups, one
container each, reporting every failing group.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: kpenfound <[email protected]>
Test-file discovery follows the config shapes found in the field:
export default <const> (TypeScript annotations included), one hop of a
relative import or re-export (export { config as default } from '../x.js'),
and a spread (defineConfig({ ...config, webServer })) whose keys the
config's own override. Template strings with ${} are masked whole. A
project where nothing is found still gets a '.' key that runs it whole.

Installs happen at the workspace root (pnpm-workspace.yaml / workspaces,
else lockfile, else package.json) with the package manager detected from
packageManager or the lockfile, corepack installed when missing, pnpm's
store passed with --store-dir, and only install inputs mounted (package.json
files, lockfiles, config, patches, file:/link:/injected dependency dirs and
workspace bin files) before the source is laid over. Tests run the
project's own node_modules/.bin/playwright. New settings: installFlags,
environment. Failures name the step (install vs playwright test) with the
end of its output.

The image version comes from pnpm-lock.yaml, yarn.lock, bun.lock or
package-lock.json, else the declaring package.json with catalog: resolved
from pnpm-workspace.yaml, and never yields an invalid reference. report
adds --reporter=list,html so the HTML report always exists.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: kpenfound <[email protected]>
Concurrent yarn 1 installs in one batch can corrupt a shared cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: kpenfound <[email protected]>
Batch failures now read "Playwright failed in …", since the cause may be the install. The environment example no longer uses a variable that picks a browser. The README explains that lifecycle scripts run before the source is mounted, and how to skip them with installFlags.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: kpenfound <[email protected]>
- setup: shell commands run after install with the full source, before
  the tests, e.g. building a workspace package the tests import. With a
  wired service there is no webServer to do it in.
- args go after the module's own arguments: a variadic flag such as
  --project web consumed the file filters of a filtered run, and
  Playwright ignores filters after "--".
- The localhost proxy listens dual-stack, so [::1] reaches the service as
  well as 127.0.0.1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant