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
8 changes: 6 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ on:
branches: [main]

jobs:
tests:
uses: ./.github/workflows/reusable-test.yml

build:
needs: [tests]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npm run build
55 changes: 55 additions & 0 deletions .github/workflows/reusable-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Reusable Tests

on:
workflow_call:

permissions:
contents: read

jobs:
test-unit:
runs-on: ubuntu-latest
steps:
- name: checkout 🛒
uses: actions/checkout@v6
- name: node 22 ❇️
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- name: install dependencies 📦
run: npm ci
- name: lint 🧹
run: npm run lint
- name: unit tests 🧪
run: npm run test-coverage
- name: build 🏗️
run: npm run build
- name: Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

test-e2e:
runs-on: ubuntu-latest
steps:
- name: checkout 🛒
uses: actions/checkout@v6
- name: node 22 ❇️
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- name: install dependencies 📦
run: npm ci
- name: install Playwright browsers 🎭
run: npx playwright install --with-deps chromium firefox webkit
- name: e2e tests 🌐
run: npm run test:e2e
- name: upload Playwright report 📋
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report/
retention-days: 7
32 changes: 6 additions & 26 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,10 @@ on:
pull_request:
branches: ['main']

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'

- run: npm install
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

- name: Check Prettier formatting
run: npx prettier --check vite.config.ts src/

- name: Run ESLint
run: npx eslint vite.config.ts src/

- run: npm run test-coverage
- run: npm run build
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
jobs:
tests:
uses: ./.github/workflows/reusable-test.yml
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"serve": "serve public"
"serve": "serve public",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@playwright/test": "^1.59.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
Expand Down
24 changes: 24 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests/e2e',
testMatch: '**/*.spec.ts',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [['list'], ['html', { open: 'never' }]],
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
15 changes: 15 additions & 0 deletions tests/e2e/fixture.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TSML UI E2E</title>
</head>
<body>
<div
id="tsml-ui"
data-src="http://tsml.e2e.test/meetings.json"
></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>
95 changes: 95 additions & 0 deletions tests/e2e/tsml.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { test, expect, type Page } from '@playwright/test';

const MEETINGS = [
{
slug: 'monday-morning-group',
name: 'Monday Morning Group',
day: 1,
time: '09:00',
timezone: 'America/New_York',
formatted_address: '1 Water St, Vineyard Haven, MA 02568, USA',
location: 'Steamship Terminal',
regions: ['Martha\'s Vineyard'],
types: ['O'],
},
{
slug: 'wednesday-evening-group',
name: 'Wednesday Evening Group',
day: 3,
time: '19:00',
timezone: 'America/New_York',
formatted_address: '188 Meeting St, Charleston, SC 29401, USA',
location: 'City Market',
regions: ['Charleston'],
types: ['C'],
},
{
slug: 'friday-online-group',
name: 'Friday Online Group',
day: 5,
time: '20:00',
timezone: 'America/Los_Angeles',
conference_url: 'https://zoom.us/j/999000999',
formatted_address: 'Online',
regions: ['Remote'],
types: ['O'],
},
];

async function mockApi(page: Page) {
await page.route('http://tsml.e2e.test/**', route =>
route.fulfill({ json: MEETINGS })
);
}

async function loadApp(page: Page) {
await mockApi(page);
await page.goto('/tests/e2e/fixture.html');
// Wait until at least one meeting row is visible
await expect(
page.getByRole('cell', { name: 'Monday Morning Group', exact: true })
).toBeVisible({ timeout: 15000 });
}

const meetingCell = (page: Page, name: string) => page.getByRole('cell', { name, exact: true });

test.describe('meeting list', () => {
test('renders all meetings from the API', async ({ page }) => {
await loadApp(page);
await expect(meetingCell(page, 'Monday Morning Group')).toBeVisible();
await expect(meetingCell(page, 'Wednesday Evening Group')).toBeVisible();
await expect(meetingCell(page, 'Friday Online Group')).toBeVisible();
});

test('search filters meetings by name', async ({ page }) => {
await loadApp(page);
await page.getByRole('searchbox').fill('wednesday');
// Wait for exactly 1 name cell, confirms filter fully applied before checking negatives
await expect(page.locator('td.tsml-name')).toHaveCount(1);
await expect(meetingCell(page, 'Wednesday Evening Group')).toBeVisible();
await expect(meetingCell(page, 'Monday Morning Group')).not.toBeVisible();
await expect(meetingCell(page, 'Friday Online Group')).not.toBeVisible();
});
});

test.describe('meeting detail', () => {
test('clicking a meeting opens the detail view', async ({ page }) => {
await loadApp(page);
await meetingCell(page, 'Monday Morning Group').click();
await expect(
page.getByRole('link', { name: 'Back to Meetings' })
).toBeVisible();
await expect(page.getByText('Monday Morning Group')).toBeVisible();
});

test('navigating directly to a meeting slug loads the detail view', async ({
page,
}) => {
await mockApi(page);
await page.goto('/tests/e2e/fixture.html#/wednesday-evening-group');
await expect(
page.getByRole('link', { name: 'Back to Meetings' })
).toBeVisible({ timeout: 15000 });
await expect(page.getByText('Wednesday Evening Group')).toBeVisible();
});
});
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Meeting } from '../../src/types';
import type { Meeting } from '../../../src/types';

export const mockMeeting: Meeting = {
slug: 'foo',
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fireEvent, render, screen, cleanup } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import Button from '../../src/components/Button';
import Button from '../../../src/components/Button';

describe('<Button />', () => {
afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { describe, expect, it } from 'vitest';

import Controls from '../../src/components/Controls';
import { defaults, SettingsProvider } from '../../src/hooks';
import Controls from '../../../src/components/Controls';
import { defaults, SettingsProvider } from '../../../src/hooks';

describe('<Controls />', () => {
const settings = defaults;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { render } from '@testing-library/react';
import { expect, it } from 'vitest';

import Loading from '../../src/components/Loading';
import Loading from '../../../src/components/Loading';

it('<Loading />', () => {
const { container } = render(<Loading />);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { render } from '@testing-library/react';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import Map from '../../src/components/Map';
import Map from '../../../src/components/Map';

// Mock react-leaflet
vi.mock('react-leaflet', () => ({
Expand Down
Loading