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: 5 additions & 3 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ name: backend
on:
push:
branches: [main]
paths: ["backend/**", ".github/workflows/backend.yml"]
# The tests build their database from the role scripts in infra/compose/postgres/initdb (tests/conftest.py)
paths: ["backend/**", "infra/compose/postgres/initdb/**", ".github/workflows/backend.yml"]
pull_request:
paths: ["backend/**", ".github/workflows/backend.yml"]
paths: ["backend/**", "infra/compose/postgres/initdb/**", ".github/workflows/backend.yml"]

concurrency:
group: backend-${{ github.ref }}
Expand All @@ -17,7 +18,8 @@ permissions:
jobs:
checks:
runs-on: ubuntu-24.04
timeout-minutes: 15
# 4,120 tests on 4 workers take about 7.5 minutes on a 4-CPU machine; this leaves room for a slower runner
timeout-minutes: 30
defaults:
run:
working-directory: backend
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/auth/invite-screen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ describe('InviteScreen', () => {

it('puts the reason the API refuses a password on the password field, keeping the name', async () => {
const rendered = await openInvitation();
await register(rendered, 'lena-2026-password');
expect(await screen.findByText('Do not use your email address in the password.')).toBeInTheDocument();
await register(rendered, 'short');
expect(await screen.findByText(/^Use at least \d+ characters\.$/)).toBeInTheDocument();
expect(screen.getByLabelText('Your name')).toHaveValue('Lena Vogel');
});

Expand Down
10 changes: 6 additions & 4 deletions apps/web/src/features/rankings/ranking-pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,29 @@ export function RankingPagesCard({ trigger, pages, checkedAt, timeZone }: Rankin
const t = useTranslations('rankings.pages');
const rank = useTranslations('rankings.rank');
return (
<PreviewCard trigger={trigger} label={t('label')} className="w-auto max-w-(--size-popover-max) min-w-80 gap-2">
<PreviewCard trigger={trigger} label={t('label')} className="w-(--size-popover-max) max-w-full gap-2">
<p className="type-meta text-foreground-secondary">{t('title', { count: pages.length })}</p>
<table className="w-full border-collapse">
{/* A fixed layout keeps a long path inside the card: the page column takes what the rank leaves and truncates */}
<table className="w-full table-fixed border-collapse">
<thead>
<tr>
<th scope="col" className="pb-1 text-left type-table-header text-foreground-tertiary">
{t('page')}
</th>
<th scope="col" className="pb-1 pl-4 text-right type-table-header text-foreground-tertiary">
<th scope="col" className="w-14 pb-1 pl-4 text-right type-table-header text-foreground-tertiary">
{rank('rank')}
</th>
</tr>
</thead>
<tbody>
{pages.map((page, index) => (
<tr key={page.url.url_id} className="border-t border-border-subtle align-top">
<td className="max-w-72 py-1.5">
<td className="py-1.5">
<a
href={page.url.canonical_url}
target="_blank"
rel="noopener noreferrer"
title={page.url.path}
className="block truncate type-body text-link hover:underline"
>
{page.url.path}
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/features/rankings/rankings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import en from '../../../messages/en.json';

import type { TrackedKeywordState } from '@/lib/api/types';
import type { TrackedKeywordRow, TrackedKeywordState } from '@/lib/api/types';
import { MOCK_ENTITY_IDS } from '@/mocks/entity-fixtures';
import { MOCK_KEYWORDS, MOCK_TRACKED_KEYWORDS } from '@/mocks/keyword-fixtures';
import { KEYWORD_TABLE } from '@/mocks/keyword-table';
Expand Down Expand Up @@ -509,10 +509,11 @@ describe('Rankings', () => {
});

/** The fixture keywords as the mocks state them now: what the API will answer for each active row. */
function fixtureStates(): { text: string; state: TrackedKeywordState }[] {
function fixtureStates(): { text: string; device: TrackedKeywordRow['device']; state: TrackedKeywordState }[] {
const texts = new Map(MOCK_KEYWORDS.map((keyword) => [keyword.keyword_id, keyword.text]));
return MOCK_TRACKED_KEYWORDS.filter((row) => row.is_active).map((row) => ({
text: texts.get(row.keyword_id) ?? '',
device: row.device,
state: rankStateOf(
row.tracked_keyword_id,
row.created_at,
Expand Down Expand Up @@ -967,8 +968,10 @@ describe('Rankings rank filters', () => {
asked.some((query) => query.get('filter[rank_gte]') === '4' && query.get('filter[rank_lte]') === '10'),
).toBe(true),
);
// The table shows one device, desktop unless mobile is chosen
const within4to10 = fixtureStates().filter(
({ state }) => state.current.rank !== null && state.current.rank >= 4 && state.current.rank <= 10,
({ device, state }) =>
device === 'desktop' && state.current.rank !== null && state.current.rank >= 4 && state.current.rank <= 10,
);
expect(within4to10.length).toBeGreaterThan(0);
await waitFor(() => expect(keywordsIn(grid)).toHaveLength(within4to10.length));
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/features/rankings/strip/rank-strip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ describe('RankStrip', () => {
expect(await screen.findByText(String(activeCount))).toBeInTheDocument();
expect(await screen.findByText('55 checked')).toBeInTheDocument();
expect(screen.getByText('Top 3')).toBeInTheDocument();
expect(screen.getByText('6')).toBeInTheDocument();
expect(screen.getByText('7')).toBeInTheDocument();
expect(screen.getByText('Top 10')).toBeInTheDocument();
expect(screen.getByText('12')).toBeInTheDocument();
expect(screen.getByText('Top 20')).toBeInTheDocument();
expect(screen.getByText('28')).toBeInTheDocument();
expect(screen.getByText('23')).toBeInTheDocument();
expect(screen.getAllByText('of 55 checked')).toHaveLength(3);
});

Expand Down Expand Up @@ -85,7 +85,7 @@ describe('RankStrip', () => {
expect(screen.getByText('Top 10')).toBeInTheDocument();
expect(screen.getByText('12')).toBeInTheDocument();
expect(screen.getByText('Top 20')).toBeInTheDocument();
expect(screen.getByText('28')).toBeInTheDocument();
expect(screen.getByText('23')).toBeInTheDocument();
expect(screen.getByText('The summary did not load.')).toBeInTheDocument();
});

Expand Down
7 changes: 3 additions & 4 deletions apps/web/src/features/rankings/strip/rank-strip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export interface RankStripProps {

/**
* Summary metric strip of the filtered keywords (24-page-templates.md section 9.3).
* Four counts of tracked keywords (a keyword on each device is two): Keywords (with checked count), Top 3, Top 10
* and Top 20 (each of checked count). The device chosen narrows all four.
* Four counts of tracked keywords: Keywords (with checked count), Top 3, Top 10 and Top 20 (each of checked
* count). The filters carry the device shown, so each keyword counts once and Keywords equals the table's count.
*/
export function RankStrip({ scope, filters, total, enabled = true }: RankStripProps): ReactElement {
const t = useTranslations('rankings.strip');
Expand Down Expand Up @@ -54,8 +54,7 @@ export function RankStrip({ scope, filters, total, enabled = true }: RankStripPr
};

// The strip ignores rows without a check date for rank counts (24-page-templates.md section 9.3).
// The count of keywords is of tracked keywords, one for each device: both devices chosen, a keyword counts
// twice, as its Top 3, Top 10 and Top 20 do, and one device chosen counts only its own.
// The count of keywords is of the device shown, like the table's: a keyword tracked on both devices is one.
const items: MetricStripItem[] = [
{
id: 'tracked',
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/mocks/handlers/costs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const keyOf = (event: CostEvent, key: GroupKey): string => {
};

/** Today in UTC, as the API's default dates are. */
const today = (): string => new Date().toISOString().slice(0, 10);
const today = (): string => new Date(Date.now()).toISOString().slice(0, 10);

export const costHandlers: HttpHandler[] = [
mock('get', `${W}/costs`, ({ request, cookies, params }) => {
Expand Down
5 changes: 4 additions & 1 deletion backend/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run lint-imports
uv run pytest "$@"
# Four workers, each with its own PostgreSQL, Redis and object store containers (the session fixtures run
# once per worker). The suite waits on the databases far more than on the CPU, so this fits the 4-vCPU CI
# runner and a 4-CPU Colima. Pass -n 0 to run serially.
uv run pytest -n 4 "$@"
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ dev = [
"pytest>=9.1.1",
"pytest-asyncio>=1.4.0",
"pytest-cov>=7.1.0",
"pytest-xdist>=3.8",
"ruff>=0.16.9",
"testcontainers[postgres,redis]>=4.15.0",
"types-aiobotocore[s3]>=3.9.1",
Expand Down
Loading
Loading