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
4 changes: 4 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN apk add --no-cache \
bash \
build-base \
chromium \
curl \
git \
harfbuzz \
libxml2-dev \
libxslt-dev \
nodejs \
nss \
npm \
openssl-dev \
python3 \
ttf-freefont \
yaml-dev \
tzdata

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This document defines execution constraints for AI agents. For general contribut
## Agent-Specific Verification Rules

- Always run Dev Container smoke + `make ready` for changes.
- For frontend changes or API contract/spec changes, run `make ci-ready` to mirror CI parity checks.
- For frontend changes, also verify in `chrome-devtools` MCP at `http://127.0.0.1:4001/` while the Dev Container is running.
- Capture a quick state check for all affected UI states (e.g., guest/member/result) to enforce state parity and avoid duplicate actions.

Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

.PHONY: help test lint lint-js lint-ruby lintfix lintfix-js lintfix-ruby setup dev clean frontend-setup check-frontend quick-check ready yard-verify-public-docs openapi openapi-verify openapi-client openapi-client-verify openapi-lint openapi-lint-redocly openapi-lint-spectral openai-lint-spectral test-frontend-e2e
.PHONY: help test lint lint-js lint-ruby lintfix lintfix-js lintfix-ruby setup dev clean frontend-setup check-frontend quick-check ready ci-ready yard-verify-public-docs openapi openapi-verify openapi-client openapi-client-verify openapi-lint openapi-lint-redocly openapi-lint-spectral openai-lint-spectral test-frontend-e2e

RUBOCOP_FLAGS ?= --cache false

# Default target
help: ## Show this help message
Expand Down Expand Up @@ -60,7 +62,7 @@ lint: lint-ruby lint-js ## Run all linters (Ruby + Frontend) - errors when issue

lint-ruby: ## Run Ruby linter (RuboCop) - errors when issues found
@echo "Running RuboCop linting..."
bundle exec rubocop
bundle exec rubocop $(RUBOCOP_FLAGS)
@echo "Running Zeitwerk eager-load check..."
bundle exec rake zeitwerk:verify
@echo "Running YARD public-method docs check..."
Expand Down Expand Up @@ -105,6 +107,13 @@ ready: ## Pre-commit gate (quick checks + RSpec)
bundle exec rspec
@echo "Pre-commit checks complete!"

ci-ready: ## CI parity gate (ready + OpenAPI verify + frontend e2e smoke)
@echo "Running CI parity checks..."
$(MAKE) ready
$(MAKE) openapi-verify
$(MAKE) test-frontend-e2e
@echo "CI parity checks complete!"

yard-verify-public-docs: ## Verify essential YARD docs for all public methods in app/
bundle exec rake yard:verify_public_docs

Expand Down
111 changes: 89 additions & 22 deletions frontend/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,45 +26,112 @@ test.describe('frontend smoke', () => {
});
});

await page.route(/\/api\/v1\/strategies$/, async (route) => {
await page.goto('/');

await expect(page.getByLabel('Page URL')).toBeVisible();
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();

await page.getByLabel('Page URL').fill('https://example.com/articles');
await page.getByRole('button', { name: 'Generate feed URL' }).click();

await expect(page.getByRole('heading', { name: 'Enter access token' })).toBeVisible();
await expect(page.getByRole('textbox', { name: 'Access token' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Save and continue' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Back' })).toBeVisible();

await page.getByRole('button', { name: 'Back' }).click();
await expect(page).toHaveURL(/#\/create(?:\?.*)?$/);
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
await expect(page.locator('.form-shell')).toHaveAttribute('data-state', 'create');
await expect(page.getByLabel('Utilities')).toBeVisible();
});

test('shows result after successful feed creation without snapshot recovery', async ({ page }) => {
await page.route(/\/api\/v1$/, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
strategies: [
{ id: 'faraday', name: 'faraday', display_name: 'Default' },
{
id: 'browserless',
name: 'browserless',
display_name: 'JavaScript pages (recommended)',
api: {
name: 'html2rss-web API',
description: 'RESTful API for converting websites to RSS feeds',
openapi_url: 'http://example.test/openapi.yaml',
},
instance: {
feed_creation: {
enabled: true,
access_token_required: true,
},
],
featured_feeds: [],
},
},
meta: { total: 2 },
}),
});
});

await page.goto('/');
await page.route(/\/api\/v1\/feeds$/, async (route) => {
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
feed: {
id: 'feed-123',
name: 'Example Feed',
url: 'https://example.com/articles',
feed_token: 'generated-token',
public_url: '/api/v1/feeds/generated-token',
json_public_url: '/api/v1/feeds/generated-token.json',
created_at: '2026-04-05T08:59:00.000Z',
updated_at: '2026-04-05T09:00:00.000Z',
},
},
}),
});
});

await expect(page.getByLabel('Page URL')).toBeVisible();
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
await expect(page.getByLabel('Utilities')).toBeVisible();
await expect(page.getByRole('link', { name: 'Bookmarklet' })).toBeVisible();
await page.route(/\/api\/v1\/feeds\/generated-token\.json$/, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/feed+json',
body: JSON.stringify({
items: [
{
title: 'Sample preview item',
content_text: 'Current preview fetch includes rendered content.',
date_published: '2026-04-05T09:00:00.000Z',
url: 'https://example.com/articles/sample-preview-item',
},
],
}),
});
});

await page.addInitScript(() => {
sessionStorage.setItem('html2rss_access_token', 'token-123');
});

await page.goto('/');
await page.getByLabel('Page URL').fill('https://example.com/articles');
await page.getByRole('button', { name: 'Generate feed URL' }).click();

await expect(page.getByRole('heading', { name: 'Enter access token' })).toBeVisible();
await expect(page.getByRole('textbox', { name: 'Access token' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Save and continue' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Back' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Feed ready' })).toBeVisible();
await expect(page.locator('.result-shell')).toHaveAttribute('data-state', 'result');
await expect(page.getByText('Example Feed')).toBeVisible();
await expect(page.getByRole('link', { name: 'Open feed' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Open JSON Feed' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Open in feed reader' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Create another feed' })).toBeVisible();
await expect(page.getByText('Sample preview item')).toBeVisible();
await expect(page.getByText('Current preview fetch includes rendered content.')).toBeVisible();

await page.getByRole('button', { name: 'Back' }).click();
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
await expect(page.getByLabel('Utilities')).toBeVisible();
await expect(page.getByRole('link', { name: 'Bookmarklet' })).toBeVisible();
await page.goto('/#/result/missing-token');

await expect(page.getByLabel('Page URL')).toBeVisible();
await expect(page.getByText('Saved result unavailable')).toHaveCount(0);
await expect(page.locator('.result-recovery')).toHaveCount(0);
});
});
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"stylelint-config-standard": "^40.0.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.59.1",
"vite": "^6.4.2",
"vite": "^7.3.2",
"vitest": "^3.2.4"
}
}
14 changes: 13 additions & 1 deletion frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { existsSync } from 'node:fs';
import { defineConfig, devices } from '@playwright/test';

const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
const chromiumExecutablePath =
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || resolveChromiumExecutablePath();

function resolveChromiumExecutablePath(): string | undefined {
const candidates = [
'/usr/bin/chromium-browser',
'/usr/bin/chromium',
'/home/vscode/.cache/ms-playwright/chromium-1217/chrome-linux/chrome',
];

return candidates.find((candidate) => existsSync(candidate));
}

export default defineConfig({
testDir: './e2e',
Expand Down
Loading
Loading