diff --git a/docs/test-conventions.md b/docs/test-conventions.md
new file mode 100644
index 0000000..2439ebe
--- /dev/null
+++ b/docs/test-conventions.md
@@ -0,0 +1,170 @@
+# Test Conventions Reference
+
+Concrete selector and ARIA conventions used across this repository.
+Intended for Copilot agent sessions (heal, code review, new tests) so they
+do not need to re-read every source file.
+
+---
+
+## 1. Login message — ARIA roles and `aria-live`
+
+**Source:** `src/pages/Login.js`
+
+The message `
` rendered below the form uses a **dynamic role**:
+
+| State | `role` | CSS class |
+|---------|----------|------------------|
+| Success | `status` | `message success` |
+| Error | `alert` | `message error` |
+
+Both states also carry `aria-live="polite"` (static attribute — it does not
+change with the role).
+
+```jsx
+
+ {message}
+
+```
+
+---
+
+## 2. How Playwright specs assert on login messages
+
+**Source:** `tests/login.spec.ts`
+
+Specs use `getByRole` — never a CSS selector or `#message` ID — because the
+role itself encodes the pass/fail state.
+
+```ts
+// Success
+const message = page.getByRole('status');
+await expect(message).toBeVisible();
+await expect(message).toHaveClass(/success/);
+await expect(message).toContainText('Welcome');
+
+// Error
+const message = page.getByRole('alert');
+await expect(message).toBeVisible();
+await expect(message).toHaveClass(/error/);
+```
+
+Form field selectors use `getByRole('textbox', { name: … })` and
+`getByRole('button', { name: … })`, matching the `