diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index d092ad6..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "extends": ["airbnb-base", ".eslintrc-todo"],
- "env": {
- "mocha": true,
- "browser": true
- },
- "rules": {
- "comma-dangle": ["error", "only-multiline"],
- "max-len": ["error", {"code":120, "comments": 120}],
- "no-template-curly-in-string": "off",
- "no-plusplus": "off",
- "guard-for-in": "off",
- "prefer-destructuring": "off",
- "no-else-return": ["warn", {"allowElseIf": true}],
- "globals": {
- "window": "readonly"
- },
- "import/extensions": [ "always",
- { "js": "always" }
- ],
- "import/prefer-default-export": "off"
- }
-}
diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml
index fe8b6c9..884f7dc 100644
--- a/.github/workflows/on-push.yml
+++ b/.github/workflows/on-push.yml
@@ -11,14 +11,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- - name: Start xvfb on Linux
- if: matrix.os == 'ubuntu-latest'
- run: |
- export DISPLAY=:99.0
- Xvfb -ac :99 -screen 0 1280x1024x16 > /dev/null 2>&1 &
- - uses: actions/checkout@v1
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
- node-version: 12
- - run: yarn
- - run: xvfb-run --auto-servernum yarn test
+ node-version: 20
+ - run: npm ci
+ - run: xvfb-run --auto-servernum npm test
diff --git a/.gitignore b/.gitignore
index 6f954df..1d56386 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ archive/
node_modules/
.nyc_output
test-user-data-dir
+test-results/
+test/e2e/.tmp-profile-*/
diff --git a/Dockerfile.test b/Dockerfile.test
new file mode 100644
index 0000000..d2c9a84
--- /dev/null
+++ b/Dockerfile.test
@@ -0,0 +1,17 @@
+FROM mcr.microsoft.com/playwright:v1.58.0-noble
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ xvfb \
+ x11-utils \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+COPY package.json package-lock.json* ./
+RUN npm ci
+COPY . .
+
+ENV DOCKER=1
+ENV DISPLAY=:99
+ENV PLAYWRIGHT_FORCE_TTY=0
+
+CMD xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24" npx playwright test test/e2e/ --reporter=list 2>&1
diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md
new file mode 100644
index 0000000..b72c1fa
--- /dev/null
+++ b/REFACTORING_PLAN.md
@@ -0,0 +1,203 @@
+# Refactoring Plan — RobotFramework Recorder
+
+> Generated 2026-02-12. Based on full source analysis of all files in `src/`, `test/`, config, and assets.
+
+---
+
+## 1. Code Quality Issues
+
+### 1.1 Dead Code
+- **`src/popup.js:16-30`** — Commented-out Google Analytics block (`gaAccount`, `_gaq`). The `analytics()` function on line 32 is a no-op stub. Remove both entirely.
+- **`src/popup.js:34`** — `analytics()` is called in 5 places but does nothing. Remove all call sites.
+- **`src/content.js:8-17`** — Large commented-out `MutationObserver` block. Either implement or remove.
+- **`src/background.js:8`** — `tab` is imported from constants but shadowed by the destructured `const [tab]` on line 72. The import is unused.
+- **`src/background.js:7`** — `url` import is only used in the `info` operation (line 168) — consider inlining.
+- **`src/background.js:14`** — `maxLength = 5000` is passed as the `length` parameter but `generateOutput` just uses it as a cap on list iteration. The name is misleading — it's max *actions*, not max *length*.
+- **`src/background.js:4`** — `/* global instruction filename statusMessage url tab logo initializeTranslator */` lists globals that are now ES module imports. Remove the comment.
+- **`src/popup.js:1`** — `/* global document chrome IntroTour t getCurrentLanguage setLanguage */` — `IntroTour` is loaded via `