Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/actions/setup-node/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Setup Node Environment
description: Shared setup for Node.js scripts using Yarn

runs:
using: 'composite'
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'

- name: Install Yarn
run: |
corepack enable
corepack prepare yarn@stable --activate
yarn --version
shell: bash

- name: Install dependencies
run: yarn install --immutable
shell: bash
44 changes: 44 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: E2E Tests

on:
workflow_dispatch:
inputs:
base_url:
description: 'URL of the environment to test (e.g. https://staging.example.com)'
required: true
type: string

jobs:
e2e:
name: Playwright E2E
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Node Environment
uses: ./.github/actions/setup-node

- name: Cache Playwright browsers
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ hashFiles('yarn.lock') }}

- name: Install Playwright browsers
run: yarn playwright install chromium --with-deps

- name: Run E2E tests
env:
E2E_BASE_URL: ${{ inputs.base_url }}
E2E_USERNAME: ${{ secrets.E2E_USERNAME }}
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
run: yarn playwright test

- name: Upload test report
uses: actions/upload-artifact@v7
if: always()
with:
name: playwright-report
path: tmp/playwright-report/
retention-days: 14
15 changes: 14 additions & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ on:
workflow_call: {}

jobs:
lint-js:
name: ESLint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Node Environment
uses: ./.github/actions/setup-node

- name: Run ESLint
run: yarn lint

lint-ruby:
name: RuboCop Lint
env:
Expand All @@ -32,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup Ruby Environment
uses: ./.github/actions/setup-ruby
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
# Ignore node_modules
node_modules/

# Track the Yarn release binary but ignore other Yarn cache/state
.yarn/*
!.yarn/releases

# Ignore precompiled javascript packs
/public/packs
/public/packs-test
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
940 changes: 940 additions & 0 deletions .yarn/releases/yarn-4.15.0.cjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
approvedGitRepositories:
- "**"

enableScripts: true

nodeLinker: pnpm

yarnPath: .yarn/releases/yarn-4.15.0.cjs
65 changes: 62 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ wiki](https://github.com/epimorphics/ppd-explorer/wiki).
| Data | SPARQL endpoint queried via the `data_services_api` gem |
| Error tracking | Sentry |
| Metrics | Prometheus (`prometheus-client`, `puma-metrics`) |
| E2E testing | Node 24 + Yarn 4 + Playwright (Chromium) |

This is a server-rendered Rails application with no Node.js build tooling. There is
no package.json, no Vite, and no Webpack.
This is a server-rendered Rails application with no Node.js build tooling — there is
no Vite and no Webpack. Node is present **only** to run Playwright E2E automations;
it plays no part in the application build or asset pipeline.

## Developer setup

Expand Down Expand Up @@ -56,7 +58,8 @@ Configure Bundler with your token:
./bin/bundle install
```

No Node or Yarn installation is required.
No Node or Yarn installation is required to run or build the application. Node is
only needed if you intend to run Playwright E2E tests — see [E2E testing](#e2e-testing).

### 4. Environment variables

Expand Down Expand Up @@ -111,6 +114,62 @@ View the coverage report after a test run:
open coverage/index.html
```

## E2E testing

Playwright is used for end-to-end browser automation. Node is required only for this
— it is not used in the application build.

**Prerequisites:** Node 24 via [nvm](https://github.com/nvm-sh/nvm):

```bash
nvm install # reads .nvmrc automatically
nvm use
```

**Install dependencies** (yarn resolves from `.yarn/releases/yarn-4.14.1.cjs` via
`yarnPath` in `.yarnrc.yml` — no separate corepack step needed):

```bash
yarn install
```

**Run against the local server** (starts Rails automatically on port 3001):

```bash
yarn test:e2e
```

**Run against a remote environment:**

```bash
E2E_BASE_URL=https://staging.example.com yarn test:e2e
```

Optional HTTP basic auth for protected environments:

```bash
E2E_BASE_URL=https://staging.example.com \
E2E_USERNAME=user \
E2E_PASSWORD=secret \
yarn test:e2e
```

**Interactive UI mode:**

```bash
yarn test:e2e:ui
```

**Open the last HTML report:**

```bash
yarn test:e2e:report
```

The CI workflow (`.github/workflows/e2e.yml`) is `workflow_dispatch` only — it does
not run automatically on push. Trigger it manually from the Actions tab, supplying the
target URL.

## Linting

```bash
Expand Down
61 changes: 61 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { defineConfig } from 'eslint/config'
import globals from 'globals'
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import stylistic from '@stylistic/eslint-plugin'
import playwright from 'eslint-plugin-playwright'

export default defineConfig([
{
ignores: ['.yarn/**', 'node_modules/**', 'tmp/**'],
},

js.configs.recommended,
...tseslint.configs.strict,
...tseslint.configs.stylistic,

{
languageOptions: {
globals: { ...globals.node },
},
},

{
name: 'custom-ts',
rules: {
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': ['error', { allowShortCircuit: true }],
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'separate-type-imports' }],
},
},

stylistic.configs.recommended,
{
name: 'custom-stylistic',
rules: {
'@stylistic/arrow-parens': ['warn', 'always'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/indent': ['error', 2],
'@stylistic/member-delimiter-style': [
'error',
{
multiline: { delimiter: 'none', requireLast: false },
singleline: { delimiter: 'comma', requireLast: false },
multilineDetection: 'brackets',
},
],
'@stylistic/quotes': ['error', 'single'],
'@stylistic/semi': ['error', 'never'],
'@stylistic/space-before-function-paren': ['error', 'always'],
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/no-trailing-spaces': 'warn',
'@stylistic/padded-blocks': ['warn', 'never'],
},
},

{
...playwright.configs['flat/recommended'],
files: ['test/playwright/**/*.ts'],
},
])
26 changes: 26 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "ppd-explorer",
"private": true,
"author": "Epimorphics Ltd",
"license": "ISC",
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report tmp/playwright-report",
"lint": "eslint playwright.config.ts 'test/playwright/**/*.ts'",
"lint:fix": "eslint --fix playwright.config.ts 'test/playwright/**/*.ts'"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.60.0",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^24.12.4",
"eslint": "^10.4.0",
"eslint-plugin-playwright": "^2.10.4",
"globals": "^17.6.0",
"jiti": "^2.7.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4"
},
"packageManager": "yarn@4.15.0"
}
41 changes: 41 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineConfig, devices } from '@playwright/test'

const rawBaseURL = process.env['E2E_BASE_URL'] ?? 'http://localhost:3001/'
const baseURL = rawBaseURL.endsWith('/') ? rawBaseURL : `${rawBaseURL}/`

export default defineConfig({
testDir: 'test/playwright',
outputDir: 'tmp/test-results',
reporter: [['html', { outputFolder: 'tmp/playwright-report', open: 'never' }]],
timeout: 90_000,
expect: { timeout: 30_000 },

use: {
baseURL,
screenshot: 'only-on-failure',
...(process.env['E2E_USERNAME']
? {
httpCredentials: {
username: process.env['E2E_USERNAME'],
password: process.env['E2E_PASSWORD'] ?? '',
},
}
: {}),
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],

webServer: process.env['E2E_BASE_URL']
? undefined
: {
command: 'bin/rails server -p 3001',
url: 'http://localhost:3001',
reuseExistingServer: true,
timeout: 60_000,
},
})
Loading
Loading