Concepts
The three-layer architecture, naming conventions, and how the pieces fit together.
← Back to overview · 🇩🇪 Deutsch · ← Quickstart · API Reference →
The three layers
CC-Testframework organizes E2E test code into three strict layers, scaffolded inside a single pm/ project folder:
pm/
├── 2_Apps/ ← Your app-specific helpers
│ ├── 1_Global/ ← Cross-app configuration + the master References barrel
│ └── <YourApp>/ ← One folder per application under test
│ ├── 1_Controls/ ← Low-level UI element wrappers
│ ├── 2_Steps/ ← Business-level test steps
│ └── 3_Blocks/ ← Composite step sequences (optional)
└── 3_Cases/ ← Your actual test cases — TC_*.spec.ts files
💡 Where’s Core? Framework code (
Action,Check,Step,SearchEngine, …) ships as the@meintest/cc-testframeworknpm package. Sincepm/is a self-contained npm project (its ownpackage.json, installed withcd pm && npm install), Core lands atpm/node_modules/@meintest/cc-testframework/— insidepm/, not at your repo’s outer root. You never edit it — see Layer 1 below.
Each layer has a clear contract about what belongs in it and what doesn’t. Test authors writing a TestCase (Layer 3) only ever call into TestSteps (Layer 2), which in turn call into Controls (Layer 2) and Core (Layer 1). This discipline produces tests that read like business specifications, not like Playwright code.
The full project layout
pm/ scaffolds with all 13 numbered folders from minute zero — most projects only actively use a handful of them day-to-day, but the slots exist so any tooling that reads or writes them has one canonical place to look.
| Folder | Purpose |
|---|---|
pm/2_Apps/ | Your app-specific helpers — Controls, TestSteps, Blocks (Layer 2, below) |
pm/3_Cases/ | Your TestCases — TC_*.spec.ts (Layer 3, below) |
pm/4_Sets/ | Test-set groupings — which TestCases belong together for a given run |
pm/5_Plans/ | Test plans |
pm/6_Requirements/ | Requirements / traceability records |
pm/7_Assets/ | Reference screenshots for Self-Healing, keyed by <uid>/<refId>.png — see Self-Healing Locators |
pm/8_Defects/ | Defect records |
pm/9_Scheduler/ | Scheduled/automated run definitions |
pm/10_Downloads/ | Default download target (Core.Constant.downloadDir) |
pm/11_TestData/ | Test-data fixtures (Core.Constant.testDataDir) |
pm/12_Logging/ | Log output (Core.Constant.logDir) |
pm/13_RemoteAgents/ | Remote-automation agent files — committed task-request files |
pm/14_Results/ | Durable, committed run-history records (one file per execution) |
pm/ also carries its own config files, as a self-contained npm project — siblings of the 13 numbered folders, not one of them:
| File | Purpose |
|---|---|
pm/package.json | The project manifest — @meintest/cc-testframework, @playwright/test, and the runtime dependencies your shipped Apps need |
pm/tsconfig.json | TypeScript config — baseUrl: './' and the @Apps/*/@Cases/*/@TestData/* path aliases, all relative to pm/ itself |
pm/playwright.config.ts | Playwright config — testDir: './3_Cases', your baseURL, projects |
pm/.npmrc | GitHub Packages registry + auth-token reference, so npm install inside pm/ can resolve @meintest/... packages |
pm/.cc-scaffold.json | Baseline marker written by init/add-app — the pristine SHA-256 of every scaffolded Control/Step file. Lets cc-testframework-update-app detect template drift without guessing. Commit it. |
See Quickstart — Step 3 for how these get scaffolded, and Migrating to v0.25.0 below if you’re upgrading an existing project.
Migrating to v0.24.0 (project layout)
⚠ Breaking change, no backward-compat shim — the framework is still in active development. If you scaffolded a project before v0.24.0, apply these moves by hand.
| Before (< v0.24.0) | After (≥ v0.24.0) |
|---|---|
tests/ (project root) | pm/ |
4_Download/ | pm/10_Downloads/ |
5_TestData/ | pm/11_TestData/ |
6_Logging/ | pm/12_Logging/ |
3_Cases/Assets/<TestCase-title>/<refId>.png | pm/7_Assets/<uid>/<refId>.png (requires Core.setTestCaseId('tc_<slug>') — see Self-Healing Locators — Giving a TestCase a stable id) |
At the time of this migration, playwright.config.ts and package.json still lived at your repo’s outer root, with testDir: './pm' and projectDir = './pm' pointing inward — see Migrating to v0.25.0 below for where those files live now.
Migrating to v0.25.0 (self-contained pm/ project)
⚠ Breaking change, no backward-compat shim — the framework is still in active development.
v0.25.0 moves package.json, tsconfig.json, playwright.config.ts, and .npmrc from your repo’s outer root into pm/ itself — pm/ becomes a self-contained npm project, and your repo’s outer root gets none of these files at all:
| Before (< v0.25.0) | After (≥ v0.25.0) |
|---|---|
package.json, node_modules/, .npmrc (outer root) | pm/package.json, pm/node_modules/, pm/.npmrc |
tsconfig.json (outer root) | pm/tsconfig.json (path aliases drop the pm/ prefix — @Apps/* → 2_Apps/*, and so on) |
playwright.config.ts (outer root, testDir: './pm') | pm/playwright.config.ts (testDir: './3_Cases') |
npm install (outer root) | cd pm && npm install |
npx playwright test pm/3_Cases/TC_*.spec.ts (outer root) | npx playwright test 3_Cases/TC_*.spec.ts (from inside pm/) |
This also changes where the framework’s own repo-root detection lands: it now resolves to pm/ (the directory carrying playwright.config.ts and package.json), not the outer folder containing pm/. Everything the framework writes relative to that root — .self-healing-report.json, .cc-testframework.local.json, the Playwright HTML report, reference-screenshot resolution — now lands inside pm/ as a result. See Self-Healing Locators — Migrating to v0.25.0 for what this means for reference-asset paths specifically.
To migrate an existing project: move your outer-root package.json’s @meintest/cc-testframework/@playwright/test/app dependencies into a new pm/package.json, move tsconfig.json/playwright.config.ts/.npmrc into pm/ with the path adjustments above, delete the outer-root copies, then run cd pm && npm install.
Layer 1: Core (the framework)
Located at pm/node_modules/@meintest/cc-testframework/dist/ after cd pm && npm install. You never edit this — it’s the framework.
What’s in it:
Action— clicks, fills, hovers, scrolls, browser navigationCheck— assertions on labels, URLs, element presence, dialog statesStep— block structures, numbered steps, parameter loggingSearchEngine— locator strategies (by name, by XPath, by hierarchy)AppReady— page/browser lifecycle helpersFilesystem,Logger,PasswordManager, utility classes- Re-exports of Playwright’s
test,expect, plus useful Node-stdlib bits (fs,path, etc.) baseConfig— a Playwright configuration object you spread into your ownplaywright.config.ts
The contract: Core exports are stable across patch + minor versions. Breaking changes happen only on major bumps with a clear changelog entry. This is what you can confidently rely on.
Layer 2: Apps (your wiring)
This is where you write app-specific helpers. The structure has three sub-layers:
1_Controls/ — Low-level element wrappers
A Control wraps the locator strategies for a single logical UI element or component. Example:
// 2_Apps/MyApp/1_Controls/Control_Button.ts
import * as Core from '@meintest/cc-testframework';
export async function findButton(page: Core.Page, viewName: string, viewType: string, label: string) {
return Core.SearchEngine.findLocators(page, {
view: { name: viewName, type: viewType },
element: { tag: 'button', text: label },
});
}
💡 Why a separate Control layer? If your application changes a button’s selector (e.g., from
<button>to<a role="button">), you update one Control file — all TestSteps using that Control automatically benefit. Without this layer, every TestStep would have to know UI implementation details, and a small UI refactor would cascade through dozens of test files.
Locators tagged with Core.xpath (instead of a raw template-literal string) become eligible for Self-Healing: if a tagged locator ever fails at runtime and a reference screenshot is bound for that TestStep, the framework can automatically find a replacement and persist it back into this Control file. This is optional and purely additive — see Self-Healing Locators for the full setup.
2_Steps/ — Business-level actions
A TestStep combines Controls into a single business-meaningful action. Naming convention: TS_<Region>_<ElementType>_<Verb>.
// 2_Apps/MyApp/2_Steps/Step_Button.ts
import * as Core from '@meintest/cc-testframework';
import * as Control from '@MyAppControls/Control_Button';
export async function TS_Main_Button_Click(
viewName: string,
viewType: string,
label: string,
) {
const locator = await Control.findButton(Core.page, viewName, viewType, label);
await Core.Action.click(locator, `${viewName} → ${label}`);
}
A TestStep reads like an instruction to a human tester: “on the main view, click the button with label X”. The TestCase author doesn’t care how the button is found — that’s Control’s job.
3_Blocks/ — Composite step sequences (optional)
A Block is a reusable sequence of TestSteps for higher-level scenarios. Useful when you have repetitive multi-step setups across many TestCases (e.g., “log in as test user”, “create a sample document”). Naming: TSB_<Scenario>.
// 2_Apps/MyApp/3_Blocks/TSB_Login.ts
import * as Steps from '@MyAppSteps';
export async function TSB_Login_AsTestUser() {
await Steps.TS_Main_Textfield_Fill('Login', '', 'Username', 'testuser');
await Steps.TS_Main_Textfield_Fill('Login', '', 'Password', 'testpassword');
await Steps.TS_Main_Button_Click('Login', '', 'Sign in');
}
If you don’t have repetitive sequences, skip Blocks — they’re optional.
1_Global/ — Cross-app configuration
Contains:
References.ts— the barrel file that aggregates all app exports for use in TestCasesGlobalConfig.ts— environment URLs, timeouts, etc.GlobalSetup.ts— Playwright global setup hook
Layer 3: Cases (your tests)
A TestCase is a Playwright test()-block that composes TestSteps (and optionally Blocks) into one business scenario. Naming: TC_<Scenario>.spec.ts.
// 3_Cases/TC_UserCreation.spec.ts
import * as Project from '@GlobalRef';
Project.Core.test('TC_UserCreation', async ({ page }) => {
Project.Core.Step.setCurrentTestCaseName('TC_UserCreation');
await Project.Core.Step.numberedStepBlock('Login', async () => {
await Project.MyApp.TSB_Login_AsTestUser();
});
await Project.Core.Step.numberedStepBlock('Open user creation dialog', async () => {
await Project.MyApp.TS_Main_Menu_ClickItem('Dashboard', '', 'Users');
await Project.MyApp.TS_Main_Button_Click('Users', '', 'Add User');
});
await Project.Core.Step.numberedStepBlock('Fill user form', async () => {
await Project.MyApp.TS_Dialog_Textfield_Fill('Add User', '', 'Email', 'jane@example.com');
await Project.MyApp.TS_Dialog_Button_Click('Add User', '', 'Create');
});
await Project.Core.Step.numberedStepBlock('Verify success message', async () => {
await Project.MyApp.TS_Dialog_Label_ByXpath_CheckIsEqual(
'Confirmation',
'',
'.//p',
'User created.',
);
});
});
TestCases do not contain low-level locators or raw Playwright calls. Everything goes through TestSteps. This is the discipline that makes the framework valuable.
Naming conventions at a glance
| Layer | Prefix | Example | Reads as |
|---|---|---|---|
| Control | (file name) Control_* | Control_Button.ts | “wrappers for button elements” |
| TestStep | TS_<Region>_<ElementType>_<Verb> | TS_Main_Button_Click | “in the main region, click a button” |
| Step Block | TSB_<Scenario> | TSB_Login_AsTestUser | “the block for logging in as test user” |
| TestCase | TC_<Scenario> | TC_UserCreation.spec.ts | “the test case for user creation” |
The prefixes aren’t decorative — they tell other team members at a glance which layer a file belongs to and what kind of code to expect inside.
This discipline keeps refactoring local (a UI change touches one Control, a business-logic change touches one TestStep) and keeps TestCases readable as business specs rather than Playwright code. A new team member starts writing TestCases by knowing only the TS_-prefix convention, without needing Playwright locator semantics up front.
Where to go next
- Add a New App — register your Application-Under-Test and pick a platform tool
- API Reference — what’s available in Core (
Action,Check,Step, …) for use in your TestSteps and Controls - Self-Healing Locators — automatic locator repair with reference screenshots
- FAQ — answers to “do I really need Blocks?”, “can I use page.locator() directly?”, “what if my app is React/Vue/Angular-specific?”
📧 Questions? Contact: jens.szelag@itsbusiness.ch
itsbusiness AG · Bern · Switzerland