Documentation
README
Migrate Poshi Tests
A playbook for migrating a Poshi .testcase file to the appropriate modern test layer. Each test is routed to Playwright, Jest, JUnit unit, or Java integration based on what it actually validates. Compatible Playwright tests sharing setup may be consolidated into a single test at write time.
This skill assumes the Poshi suite has already been triaged and shrunk if needed (/poshi-shrink). It does not run a Poshi side consolidation pass β consolidation happens only when writing the modern test, and only when the merged form is clearly cleaner than separate tests.
Preconditions
The working tree must be clean. Abort and ask the user to commit or stash first when dirty.
Inputs
Argument
${ARGUMENTS} is either:
-
A
@component-namevalue. Scan the Poshi tests root for.testcasefiles whose@component-namematches, list them with their test counts, and ask the user to pick one. -
An absolute or repo relative path to a single
.testcasefile. Use it directly.
When ${ARGUMENTS} is empty, resolve both the @component-name and the .testcase file interactively in two steps.
Resolve @component-name
-
Read the root
test.propertiesfile at the repository root. Parse thecomponent.names=...property, which is multiline and uses\continuations with comma separated values. Strip whitespace and trailing backslashes; the result is a flat list of component identifiers (portal-acceptance,portal-analytics-cloud,portal-bpm, β¦). -
Read
config.json(when it exists) and check fordefaultComponentName.-
When the value is set and still appears in the parsed list, ask the user with
AskUserQuestionwhether to keep${defaultComponentName}or pick another. When the user keeps the default, skip to resolving the.testcasefile. -
When the value is missing or no longer appears in the list, skip the question.
-
-
Present the full parsed list to the user as plain text, one component per line, sorted alphabetically, prefixed by an index (
1. portal-acceptance,2. portal-analytics-cloud, β¦). Append a final option such as0. Type a custom component-name (not in the list). Ask the user to either pick an index or type a custom value. Validate the entered value against the regex^[a-z0-9-]+$; do not reject when the value is not intest.propertiesbecause product teams sometimes add component-names that are still pending registration. -
Persist the choice to
config.jsonunderdefaultComponentNameso the next invocation can suggest it as the default. Createconfig.jsonfromconfig.example.jsonwhen it does not exist yet.
Resolve the .testcase File
-
Search
${poshiTestsRoot}recursively for.testcasefiles containing the line@component-name = "${chosenComponent}". Capture, for each match, the repo relative path and the test count (number oftest <Name> {blocks). -
Sort the matches alphabetically by repo relative path.
-
Present the first 20 matches to the user as plain text with
<index>. <relative-path> (<N> tests). Append a final option such as0. Type a repo relative path (not in the list). Ask the user to either pick an index or type a repo relative path manually (for files nested under a different root or not yet committed). Validate that the entered path resolves to an existing.testcasefile before continuing.
Use the chosen file path as the input for the feasibility phase onwards.
Configuration
Two repository roots are fixed, not configurable, and are referred to by these names throughout the skill:
-
poshiTestsRoot β
portal-web/test/functional/com/liferay/portalweb/tests/enduser, the root searched for Poshi.testcasefiles. -
playwrightTestsRoot β
modules/test/playwright/tests, the root for Playwright specs; the sharedhelpers,fixtures, andutilsfolders sit beside it atplaywrightTestsRoot/...
The remaining settings live in config.json at .claude/skills/poshi-migrate/config.json, which is intentionally not committed to the repository β each developer maintains their own. A config.example.json sits next to it with the suggested defaults; copy it and personalize when the defaults do not fit.
Resolve configuration in this order:
-
Read
config.jsonwhen it exists. -
Otherwise, fall back to the defaults noted with each key below.
The keys are:
-
componentNamePlaywrightModules: a mapping from
@component-nameto the list of Playwright modules underplaywrightTestsRootthat own its existing specs. The inventory phase scans these modules to detect tests that already cover, or partially cover, the Poshi tests being migrated, and asks the user which modules to scan when the mapping for the source@component-nameis missing. Defaults to{}(empty). -
defaultComponentName: the last
@component-namethe user chose interactively, suggested as the default on subsequent runs when${ARGUMENTS}is empty. The argument resolver prompts the user with the full list fromtest.propertiesand remembers the choice from then on. Unset by default. -
referencePlaywrightModulePath: the Playwright module folder read as the style reference. Defaults to
modules/test/playwright/tests/layout-content-page-editor-web.
After resolving the values, show them to the user via AskUserQuestion and offer to keep them or override any value for this run. Persist overrides only when the user explicitly asks (write config.json, never modify config.example.json).
Workflow
The skill runs in five phases: feasibility, inventory, plan, implement, validate. Each phase is gated on user approval.
Feasibility
Before classifying anything, screen each test in the file for blockers that would make a migration to Playwright unviable today. Record the result for use in the inventory phase.
For each test, scan the body and every macro it transitively calls. Flag the test when it depends on any of the following:
-
Synthetic events on a backend ingestion pipeline. The test produces signals through real UI interactions (page visits, asset views, submissions) and waits for a flush job to surface them in the destination system. Without an API to seed those events directly, the migration cannot run in the Playwright budget.
-
Multiuser sign out / sign in flows. The test asserts behavior under a different identity using a chain of
logoutPG/loginPGcalls, often relying on seed users that are not isolated per run. -
Fresh tenants, workspaces, or instances created during the test. The test provisions a new top-level container (workspace, virtual instance, isolated portal) as part of its setup, which the existing fixtures do not provision.
-
Specialized seed sites or catalogs. The test depends on an accelerator-provisioned site, a commerce catalog, or a template configuration that is not exposed through
apiHelpers. -
Wizard UI shape as the assertion. The test exists to verify the wizard UI itself (button labels, step order, banner copy). Replacing the wizard with an API setup defeats the test, so the migration must still drive the wizard β confirm that the wizard interactions are stable.
Output of this phase, per test:
| Classification | Meaning | Inventory Follow Up |
|---|---|---|
| Migrate | No blockers, or the only blockers are addressable by an API helper that already exists or that the migration will add. | Route to a layer in the inventory phase. |
| Skip | Blocked on a feature the modern test layer cannot reproduce today and that needs an API endpoint that does not exist yet. | Document the blocker and leave the test in the .testcase. Surface it in the plan so the team can prioritize the endpoint. |
| Drop | Obsolete: already covered by an existing spec, or the product contract changed and the assertion no longer applies. | Remove the test from the .testcase in the implementation phase with a commit that names the covering spec or the contract change. |
When a test is Skip, propose what the missing endpoint should look like (HTTP verb, path, payload, what UI flow it would replace) so the team can decide whether to build it. Do not improvise UI workarounds.
Inventory
-
Read the
.testcasefile. Capture thesetUp,tearDown, and everytest <Name>block with its@description,@priority, and the macros or helper calls it invokes. -
Scan the Playwright modules listed in
componentNamePlaywrightModulesfor the source@component-name. For every Poshitestnot already marked Skip in the feasibility phase, classify the existing coverage as one of:-
Already covered β an existing spec exercises the same behavior end to end. Record the spec path. The test becomes Drop in the plan; it does not become a migration.
-
Partially covered β an existing spec exercises part of the behavior or shares the same setup. Record the spec path so the plan can propose extending that spec instead of a new file.
-
Not covered β no existing spec touches the behavior. Continue to routing.
Compare each Poshi test against candidate specs across these signals (the more that align, the higher the confidence):
-
Description and comments β does the Poshi
@descriptionor any header comment match the spec'stest(...)title or section comments? -
Functionality β do both validate the same feature path (same panel, same flow, same final state)?
-
Selectors β do the Poshi macro arguments (IDs, classes, and labels passed into
Click,AssertTextEquals, and friends) overlap with the spec'sgetByRole,getByLabel, andgetByTextarguments? Resolve the macro to its.macrodefinition when the selector is not inline. -
Structure β same setup, same step order, same assertions in roughly the same place?
This is the most fragile part of the workflow; expect to iterate on the heuristic as false positives and false negatives surface. When the signals disagree, default to Partially covered and let the user resolve the call in the plan.
When
componentNamePlaywrightModulesdoes not list the source@component-name, ask the user which Playwright modules to scan before continuing. Do not skip this step. -
-
For every test whose
@descriptionreferences an LPS or LPD ticket, recover the original code change and use its diff as the primary signal for the layer decision. From the repo root:git log \ --all \ --format="%H %s" \ --grep="^${TICKET}"Run
git show <sha>on each match to read the diff, and let the changed files drive the layer choice in the next step:-
Java
*Util, formatter, parser, or pure-logic class with no service or runtime touch β JUnit unit. -
.js,.jsx,.ts, or.tsxmodule that does not import portal-runtime globals β Jest. -
*LocalServiceImpl, persistence layer, listener, or OSGi@Componentβ Java integration. -
JSP, taglib output, multi-module UI wiring, or anything that only manifests in the rendered page β Playwright.
Record the matching commit shas and the touched files in the per-test notes so the plan can cite them. When
git logreturns no match (test predates the convention or the ticket never landed in this repo) recordticket not in treeand fall back to the heuristic table in the next step. -
-
For every
testnot classified as Drop or Skip, decide the routing in this order. Pick the first match. When the previous step recovered a ticket diff, treat its signal as the layer choice unless the diff and the test's actual assertions clearly disagree β in that case, record both signals and mark the testunsure.Layer Pick When JUnit unit The test validates pure Java logic that does not need the portal runtime, services, or persistence. Common signal: assertions over plain values produced by a utility class, or behavior already reproducible at the package level. Jest The test validates frontend behavior that lives in a JavaScript or React module and can be reproduced without the portal β component rendering, state transitions, formatting, validation logic. Java integration The test exercises services, persistence, OSGi components, listeners, or portal-runtime behavior. Anything that needs a deployed bundle but does not need a real browser. Playwright Anything left: complete UI flows, multi-page navigation, browser-only behavior, JavaScript-driven interactions that a unit or integration test cannot cover. If you cannot place a test confidently, mark it
unsureand flag it for the user in the plan. Do not invent a destination. -
For each test routed to Playwright, locate the destination spec folder under
playwrightTestsRoot. The destination is the folder whose owning module renders the feature under test. When in doubt, search the Playwright tests root for an existing spec that imports or interacts with the same UI surface.
Plan
Build the plan with EnterPlanMode. Format:
## Source
`<.testcase path>` β `@component-name = <value>`, `testray.main.component.name = <value>`, <N> tests.
## Feasibility
| Test | LPS/LPD | Decision | Blocker / Reason |
| --- | --- | --- | --- |
| `<TestName>` | `<TICKET>` | Migrate / Skip / Drop | <missing API endpoint / obsolete contract / etc> |
## Existing Coverage
| Test | LPS/LPD | Status | Covered By | Action |
| --- | --- | --- | --- | --- |
| `<TestName>` | `<TICKET>` | Already covered / Partially covered / Not covered | `<repo relative spec path or β>` | Drop / Extend `<spec>` / Migrate |
When every test is **Not covered**, write `None β no overlap with existing Playwright specs`.
## Per-Test Routing
Only includes tests classified as **Migrate** or **Partially covered** in earlier phases.
| Test | LPS/LPD | Layer | Destination | Notes |
| --- | --- | --- | --- | --- |
| `<TestName>` | `<TICKET>` | Playwright / Jest / JUnit unit / Java integration | `<repo relative path>` | <Consolidates with X / extends `<spec>` / new fixture needed / etc> |
## Consolidations (Playwright)
> **`<final-spec-name>.spec.ts`** (consolidates `<TestA>` + `<TestB>`):
>
> - Shared setup: <site, page, user>.
> - Sequence: <step list>, ending with assertions for both LPS tickets.
> - Tags: `@<TICKET-A>`, `@<TICKET-B>`.
When no consolidation applies, write `None β one spec per test`.
## New Helpers, Utils, or Fixtures
List any new file under `helpers/`, `utils/`, or `fixtures/` you plan to add or extend, and the reason. When existing files cover the need, write `None β reuse existing`.
## Proposed API Endpoints
List every backend endpoint the migration would need but that does not exist yet. For each one, capture the HTTP verb, the path, the payload, and the UI flow it replaces. Mark dependent tests as **Skip** until the endpoint lands.
## Skipped Tests
| Test | LPS/LPD | Blocker | Proposed Unblocker |
| --- | --- | --- | --- |
| `<TestName>` | `<TICKET>` | <missing API / multi-tenant setup / etc> | <propose endpoint or fixture> |
## Cleanup
- Remove the **Drop** `test` blocks from `<.testcase path>` in their own commits, before any migration commits.
- Remove the migrated `test` blocks from `<.testcase path>`.
- Delete the `.testcase` file when it ends up empty.
## Commits
In order:
1. One commit per **Drop** test (or per group sharing a covering spec), removing the test from the `.testcase` file. The commit message names the covering spec or the contract change that retired the test.
1. New helper, util, or fixture commits, when any.
1. One commit per migrated test or consolidated group.
The plan must be exhaustive: every test from the inventory phase appears in either the Feasibility, Existing Coverage, or Per-Test Routing table. Tests marked unsure go on their own row and the user must confirm a layer before continuing.
Implement
After ExitPlanMode returns the user's approval, execute the plan in this order:
-
Drop cleanup commits first. For every test classified as Drop in earlier phases, remove the
testblock from the source.testcasefile (delete the file when it ends up empty), and commit. The commit message must name the spec that already covers the test or the contract change that retired it. One commit per drop, or per group of drops sharing a single cause. -
New helper, util, or fixture commits, one per artifact, following the New Helpers, Utils, Fixtures rules below.
-
Migration commits, one per row in the routing table or per consolidation group. Each commit:
-
Writes the new test file at the planned destination.
-
Removes the migrated
testblocks from the.testcasefile. When the file ends up empty after the removals, delete the file in the same commit. -
Commits the change.
-
When implementing each layer, follow the rules below.
Playwright
Follow .claude/rules/playwright.md for the general conventions (layout, spec placement, anatomy, page classes, locators, setup, cleanup, flake proofing utilities, feature flags). Do not duplicate or restate those rules here β read the file and apply it.
Read at least one spec from the configured referencePlaywrightModulePath folder before writing the first migrated spec to internalize the local idiom.
The subsections below cover decisions that are specific to migrating from Poshi.
API First Setup Substitution
When the Poshi setUp drives a UI flow (wizard, OAuth, sync between two services, signing in as another user, populating a configuration page), the migration must replace it with an API call. Climb the decision tree top-down, stopping at the first level that applies:
-
Existing Playwright helper. Look under
<playwrightTestsRoot>/../{helpers,fixtures,utils}for a helper that already replaces the flow (look for the verb in the macro name and synonyms:connect,sync,assign,add,enable). Use it. Compose withmergeTests(...)when a fixture exists. -
No helper, but a backend endpoint exists. When the product exposes a REST resource, a JSON web service, a headless endpoint, or a GraphQL mutation that performs the same effect as the UI flow, write a new helper in
<playwrightTestsRoot>/../helpers/<X>ApiHelper.ts(or extend the existing one), register it onApiHelpers, and consume it from the spec. Ship the helper in its own commit before the migration commit that uses it. Source the URL, payload shape, headers, and any envelope encoding from the backend code that owns the endpoint, not from a guess β wrong payloads typically surface as 500 errors with non-obvious messages. -
No endpoint either. Stop. Propose to the team adding the headless / JSON web services / REST endpoint that would unblock the test. Document the proposed contract (HTTP verb, path, payload, response shape) and what UI flow it replaces, then mark the test as Skip in the plan and return to the caller. Do not improvise UI workarounds β clicking through the wizard from the spec piles up flake and rots the moment the UI changes.
Validate every new helper end-to-end against the running backend (yarn test on a one-test spike spec) before committing it. The connection envelope, payload shape, and headers must match what the backend expects.
State Reset
Modern specs cannot assume a clean starting state. Whatever the Poshi tearDown was clearing (preferences, configurations, per-tenant settings, sessions, lingering data sources from previous runs), the Playwright spec must reset before the assertion, not after β a previous run might have left residue.
Two patterns work:
-
API reset in
test.beforeEach. Call the same REST / GraphQL mutation the UI uses to wipe the setting. Source the mutation name and shape from the frontend code that drives the UI. -
Filter by dynamic state instead of hardcoded identifiers. When the system creates entities with autoincrementing names or environment specific suff