Skip to content
Back to skills

E2e

ASecurity

Use when user asks to write, create, run, or debug Playwright e2e tests in the Gramax project. Triggers on keywords like e2e, playwright, test, spec, QA.

  • 608 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 20, 2026
documentationtypescriptgobashgitapi

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add Gram-ax/gramax --skill e2e --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of E2e?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for E2e
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gram-ax-e2e/badge)](https://www.skillsdirectory.com/skills/gram-ax-e2e)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: e2e
description: Use when user asks to write, create, run, or debug Playwright e2e tests in the Gramax project. Triggers on keywords like e2e, playwright, test, spec, QA.
---

# Writing E2E Tests

Framework: Playwright + custom fixtures/POMs. All tests in `e2e-pw/`.

## Workflow (MANDATORY)

The user is a QA tester who describes tests in human language. You translate intent into code. You decide ALL technical details (fixtures, POMs, locators).

### 1. Explore

Before anything else:
- Check similar tests in `e2e-pw/platforms/`
- Read relevant POMs for available methods
- Read app UI components to understand elements, roles, text content

### 2. Clarify ambiguities

If the request has gaps, present a **numbered questionnaire** in a single message. The user answers by number. Only ask about behavior/expectations, never about implementation.

**Must ask when:** platform unclear, scope ambiguous, expected result vague, multiple UI paths exist, locale matters.

Example questionnaire:
```
Before I write the test, I need to clarify a few things:

1. Platform: web or docportal?
2. You said "create catalog" — verify it appears in the list only, or also that it opens?
3. Should the test check the modal content, or just that the action completes?
```

Skip questions only if the request is unambiguous with explicit steps and expected results.

### 3. Plan in Plan Mode

Use `EnterPlanMode` to create the test plan. The plan must translate user's words into technical steps:

```
Test: <name>
Platform: web / docportal
Fixture: editorTest / homeTest / catalogTest / baseTest
Test data: <files/catalogs to create>

Steps:
1. <action>
2. <action>

Assertions:
- <assertion 1>
- <assertion 2>
```

Wait for user approval, then exit plan mode and write the test.

## Fixture Selection

```
Editor content/formatting     → editorTest
Home page / catalog list      → homeTest
Navigation inside a catalog   → catalogTest
Custom setup (web)            → baseTest
Docportal                     → baseTest (docportal)
```

### Hierarchy (web)

```
baseTest            — web context, files, localStorage
  ├─ homeTest       — + homePage (HomePage)
  ├─ catalogTest    — + catalogPage (CatalogPage)
  │    └─ editorTest — + editor (ArticleEditorPom)
  └─ (direct use for custom setups)
```

### Hierarchy (docportal)

```
baseTest — web context, API login/source setup
```

## Running Tests

```bash
cd e2e-pw
# Build & start: web (6001) / docportal (6003)
bun run web:build && bun run web:start
bun run docportal:build && bun run docportal:start

# Run
bunx playwright test --project=web                    # all web
bunx playwright test --project=web basic.spec.ts      # single file
bunx playwright test --project=web -g "test name"     # by name
bun run ui                                            # interactive UI
bunx playwright test --project=docportal-prepare --project=docportal
```

Projects: `web`, `web-enterprise`, `static` (port 6002), `docportal-prepare`, `docportal`, `docportal-enterprise`.

## Platforms

| Platform | Port | State | Setup |
|----------|------|-------|-------|
| web | 6001 | Reset each test (isolated) | `files` fixture via DOM |
| docportal | 6003 | Persistent | API setup, `docportal-prepare` |
| static | 6002 | Static build | Pre-built |

## Locators

**Only `getByRole` and `getByText`.** `getByPlaceholder` and `getByLabel` are fine where they read
naturally — they resolve through accessible names too. `getByTestId` is a last resort. CSS and
XPath selectors — `locator(".foo")`, `locator("[data-qa=…]")`, class names, tag chains, `:nth-child`
— are **not allowed**, and neither is reaching for a `data-*` attribute as a hook.

When an element has no usable role or accessible name, the answer is to **fix the component**, not
to work around it in the test. Give the button a real `<button>`, an `aria-label`, a visible text —
whatever the element is actually missing. A test that reaches into markup is a test that breaks on
the next restyle, and it hides an accessibility gap in the product.

Editing app code to make an element addressable is expected and welcome. It is not scope creep.

```typescript
// GOOD — the element says what it is
page.getByRole("button", { name: "Save" }).click();
page.getByRole("menuitemradio", { name: "master" }).click();
page.getByText("Article Title").click();

// LAST RESORT — only when the element genuinely cannot carry a role or a name
page.getByTestId("add-catalog").click();

// NOT ALLOWED — fix the component instead
page.locator(".btn-primary > span").click();
page.locator(".right-extensions i").first().click();
page.locator("[data-qa='qa-publish-tab']").click();
```

If you find yourself writing a comment that explains which CSS class the app happens to render, stop:
that is the moment to add the role or the label to the component.

### Fixing the component: use the real element, not an ARIA patch

Making an element addressable means giving it the **right tag**, not decorating the wrong one.

```tsx
// GOOD
<button type="button" aria-label={t("git.branch.actions")} onClick={…}>

// BAD — a div wearing a button costume
<div role="button" tabIndex={0} aria-label={…} onClick={…}>
```

`role="button"` plus `tabIndex={0}` on a `<div>` or `<span>` reproduces by hand what `<button>` gives
for free — keyboard activation, focus order, Enter/Space handling — and it always reproduces some of
it wrong. Use `<button type="button">`; `type` is required, or the button submits any form it lands in.

**Never silence an a11y lint.** `biome-ignore lint/a11y/*` is not an accepted way out: that rule
firing *is* the review telling you the element is wrong. If a semantic element genuinely does not
fit — an existing click target would nest, a styled wrapper would break — change the surrounding
markup so it does, or say plainly in the MR why it cannot be done. Do not commit the suppression.

Suppressions in general are a smell here. The only one currently accepted in `e2e-pw` is
`lint/correctness/noEmptyPattern` on fixture signatures, because Playwright *requires* the
`async ({}, use)` form to detect fixture dependencies and biome has no way to know that.

## Test Examples

**Editor:**
```typescript
import { editorTest } from "@web/fixtures/editor.fixture";
editorTest("does something", async ({ editor }) => {
  await editor.setMarkdown("Hello(*)");
  await editor.press("ControlOrMeta+B");
  await editor.type("bold");
  await editor.assertMarkdownContains("**bold**");
});
```

**Home/catalog:**
```typescript
import { homeTest } from "@web/fixtures/home.fixture";
homeTest.use({ files: { "my-catalog": { "article.md": "# Hello", "doc-root.yml": "title: My Catalog\n" } } });
homeTest("shows catalog", async ({ homePage }) => {
  await homePage.workspace.assertHasCatalogs(["my-catalog"]);
});
```

**Docportal:**
```typescript
import { baseTest as test } from "@docportal/fixtures/base.fixture";
test.use({ source: "env", user: "env" });
test("navigates", async ({ basePage }) => {
  const page = basePage.raw;
  await page.getByRole("button", { name: "Catalog" }).click();
  await basePage.waitForLoad();
});
```

## Fixtures

### Test data setup

**`files`** — inline file tree (primary method):
```typescript
homeTest.use({ files: { "catalog": { "art.md": "# Title", "doc-root.yml": "title: Cat\n" } } });
```
Keys = dirs/files, strings = content, objects = subdirs, `number[]` = binary.

**`dir`** — load from filesystem: `catalogTest.use({ dir: new URL("./test-catalog", import.meta.url) });`

**`zip`** — upload and extract ZIP file.

### Options (web)

`files`, `zip`, `dir`, `source` (`"env"` | SourceData), `isolated` (true), `isReadOnly` (false), `experimentalFeatures` (string[]), `startUrl` ("/").

Editor extras: `initMd` (string), `firstEnter` (true).

### Options (docportal)

`source`, `user` (`"env"` | AuthCredentials), `isolated` (true), `startUrl` ("/").

### Serial mode

`test.describe.configure({ mode: "serial" })` — when tests depend on each other.

### Step grouping

`await test.step("label", async () => { ... })` — for better reporting.

## POMs

**BaseSharedPage** (`shared-pom/page.ts`): `goto(path)`, `waitForLoad()`, `waitForUrl(url)`, `assertUrl(url)`, `assertNoModal()`, `navigate(path)` (pushState), `copyFileToClipboard(path)`, `raw` (Page), `modal` (Locator).

**HomePage** (`web/pom/home.page.ts`): `topBar` (TopMenu: `getAddCatalog()`, `getSwitchWorkspace()`, `getSwitchUiLanguage()`, `getSwitchTheme()`), `workspace` (WorkspacePom).

**CatalogPage** (`web/pom/catalog.page.ts`): `currentArticleContent(markdownOnly?)`, `getCatalogActions()`, `getCatalogProperties()`, `getNavTreeState(catalogName)`.

**ArticleEditorPom** (`web/pom/editor.pom.ts`): `setMarkdown(md, opts?)`, `markdown()`, `type(text)`, `press(keys)`, `focus()`, `pasteHtml(html)`, `pasteText(text)`, `forceSave()`, `clickToolbar(button)`, `hoverToolbar(button)`, `assertMarkdown(expected, opts?)`, `assertMarkdownContains(expected)`, `assertMarkdownValid()`, `bottom()`.

**WorkspacePom**: `assertHasCatalogs(names[])`, `assertCurrentWorkspace(config)`, `assertWorkspaces(configs[])`.

**CatalogPom** (`web/pom/catalog.pom.ts`): `props()`.

**Theme** (`shared-pom/theme.ts`): `assertHasTheme(theme)`, `assertThemeChanged()`, `switch(locator?)`.

### Atoms (shared-pom/atoms/)

**Dropdown**: `open()`, `close()`, `isOpen()`, `getItems()`, `findItemByTitle(title)`, `assertHasItem(item)`, `assertHasItems(items[])`, `assertTriggerVisible()`, `assertContentVisible()`, `assertContentHidden()`.

**DropdownItem**: `click()`, `hover()`, `assertVisible()`, `assertHidden()`. Props: `title`, `description`, `hasSubContent`.

**Button**: `click()`, `hover()`, `text()`, `assertVisible()`, `assertHasText(text)`.

**Select** (extends Dropdown): uses `select-content`/`select-item` test IDs.

## Editor Specifics

`(*)` in `setMarkdown` = cursor position. Keys: space-separated, modifiers with `+` (`ControlOrMeta+B`).

Toolbar buttons: `bold`, `italic`, `strikethrough`, `heading-2/3/4`, `bullet-list`, `ordered-list`, `task-list`, `code`, `table`, `note`, `diagrams`, `pencil-ruler`.

`md` template tag (`@utils/utils`) removes indentation from template literals.

## Conventions

1. One behavior per test
2. Use POMs, not raw locators
3. `getByRole`/`getByText` only — no CSS, no XPath, no `data-*` hooks. Missing role or name means the
   component gets fixed; `getByTestId` is the last resort, not the default
4. `waitForLoad()` after navigation
5. `serial` only when order matters
6. `files` fixture for test data, not manual setup
7. Files: `<feature>.spec.ts`, group in `describe` blocks
8. **Do not modify test infrastructure (fixtures, POMs, utils, config) without asking.** Only `*.spec.ts` files can be created/edited freely.
9. For examining playwright's trace files use playwright skill

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.