Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 90
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand Down
12 changes: 11 additions & 1 deletion components/FormattedMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@ const MdH1 = ({ children, className, ...props }: MdH1Props) => (
</h3>
)

export function FormattedMarkdownBase({ children }: { children: string }) {
type MdParagraphProps = ComponentPropsWithoutRef<'div'>
// Use <div> so block HTML from intl placeholders (e.g. <ul>) is never nested
// inside <p>, which is invalid and breaks hydration after browser HTML fixups.
const MdParagraph = ({ children, className, ...props }: MdParagraphProps) => (
<div className={twMerge('mb-4 last:mb-0', className)} {...props}>
{children}
</div>
)

export const FormattedMarkdownBase = ({ children }: { children: string }) => {
return (
<Markdown
options={{
overrides: {
h1: { component: MdH1 },
p: { component: MdParagraph },
iframe: () => null,
script: () => null,
style: () => null,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"yup": "^1.7.1"
},
"devDependencies": {
"@biomejs/biome": "^2.4.6",
"@biomejs/biome": "^2.4.9",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^22.5.4",
Expand Down
4 changes: 2 additions & 2 deletions pages/circumvention.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ const Page = (props) => {
id="ThematicPage.Circumvention.Text"
values={{
domainsList: domainsList(domains),
appsList: `<ul className="my-4">${CIRCUMVENTION_TESTS.map(
appsList: `\n\n<ul class="my-4">${CIRCUMVENTION_TESTS.map(
(d) =>
`<li><a href='#${d}'>${intl.formatMessage({
`<li><a href="#${d}">${intl.formatMessage({
id: CIRCUMVENTION_TESTS_STRINGS[d],
})}</a></li>`,
).join('')}</ul>`,
Expand Down
6 changes: 3 additions & 3 deletions pages/social-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const SOCIAL_MEDIA_TESTS_STRINGS = {
}

export const domainsList = (domains) =>
`<ul className='grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 my-4'>${domains.map((d) => `<li><a href='#${d}'>${d}</a></li>`).join('')}</ul>`
`\n\n<ul class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 my-4">${domains.map((d) => `<li><a href="#${d}">${d}</a></li>`).join('')}</ul>`

export const getServerSideProps = async () => {
try {
Expand Down Expand Up @@ -104,9 +104,9 @@ const Page = (props) => {
id="ThematicPage.SocialMedia.Text"
values={{
domainsList: domainsList(domains),
appsList: `<ul className="my-4">${SOCIAL_MEDIA_IM_TESTS.map(
appsList: `\n\n<ul class="my-4">${SOCIAL_MEDIA_IM_TESTS.map(
(d) =>
`<li><a href='#${d}'>${intl.formatMessage({
`<li><a href="#${d}">${intl.formatMessage({
id: SOCIAL_MEDIA_TESTS_STRINGS[d],
})}</a></li>`,
).join('')}</ul>`,
Expand Down
26 changes: 18 additions & 8 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
timeout: 60000,

/* Same baseline on Linux (CI) and macOS (local); avoids *-linux vs *-darwin missing files */
snapshotPathTemplate: '{testDir}/{testFilePath}-snapshots/{arg}-{projectName}{ext}',

/* Allow minor font/AA differences between OSes when sharing one baseline */
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.05,
},
},

testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
Expand Down Expand Up @@ -41,15 +51,15 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
Expand Down
2 changes: 1 addition & 1 deletion public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/

const PACKAGE_VERSION = '2.12.14'
const PACKAGE_VERSION = '2.13.5'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
Expand Down
38 changes: 38 additions & 0 deletions tests/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
import type { Page } from '@playwright/test'

/**
* Proxies browser API requests (see route glob in implementation) through the
* real network and adds CORS headers so calls from the Next app to OONI API
* succeed in Playwright (OPTIONS + passthrough for GET/POST/etc.).
*/
export async function routeApiWithCors(page: Page): Promise<void> {
await page.route('**/api/**', async (route) => {
const request = route.request()

if (request.method() === 'OPTIONS') {
await route.fulfill({
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
})
return
}

const response = await route.fetch()
const body = await response.body()
const headers = response.headers()

await route.fulfill({
status: response.status(),
headers: {
...headers,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
},
body,
})
})
}

// Scroll to bottom of the page so that all the lazy loaded content is loaded
export const scrollToBottom = async (page: Page) => {
await page.evaluate(async () => {
Expand Down
21 changes: 10 additions & 11 deletions tests/e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { test, expect } from '@playwright/test'
import { scrollToBottom } from './helpers'

test.skip('Home Page Tests', () => {
test.beforeEach(async ({ page }) => {
test.describe('Home Page Tests', () => {
test('matches the screenshot', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
})

// TODO: Check if stats appear
// TODO: Check if monthly coverage graph loads
// TODO: Check if Highlights cards are displayed
await page.waitForLoadState('networkidle')

test('explore button works', async ({ page }) => {
const exploreLink = page.getByRole('link', { name: 'Explore' }).first()
await scrollToBottom(page)

await exploreLink.click()
await expect(page).toHaveURL(/\/chart\/mat/)
await expect(page).toHaveScreenshot('homepage-desktop.png', {
fullPage: true,
maxDiffPixelRatio: 0.12,
mask: [page.locator('[data-testid="stats-value"]')],
})
})
})
34 changes: 2 additions & 32 deletions tests/e2e/legacy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,9 @@
import { test, expect } from '@playwright/test'
import { routeApiWithCors } from './helpers'

test.describe('Search Page Tests', () => {
test.beforeEach(async ({ page }) => {
await page.route('**/api/**', async (route) => {
const request = route.request()

// Handle OPTIONS preflight requests
if (request.method() === 'OPTIONS') {
await route.fulfill({
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
})
return
}

// For actual requests, fetch and add CORS headers
const response = await route.fetch()
const body = await response.body()
const headers = response.headers()

await route.fulfill({
status: response.status(),
headers: {
...headers,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
},
body: body,
})
})
await routeApiWithCors(page)
})

// test('can access "HTTP Hosts" measurements', async ({ page }) => {
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/mat.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test'
import { scrollToBottom } from './helpers'

test.describe('MAT Tests', () => {
test.describe('MAT redirections', () => {
Expand Down Expand Up @@ -71,6 +72,13 @@ test.describe('MAT Tests', () => {
await expect(page.getByRole('heading', { level: 1 })).toContainText(
'Measurement Aggregation Toolkit',
)

await scrollToBottom(page)

await expect(page).toHaveScreenshot('mat-desktop.png', {
fullPage: true,
maxDiffPixelRatio: 0.12,
})
})

test('Clicking Submit button loads table and charts', async ({ page }) => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions tests/e2e/measurement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ test.describe('Measurement Page Tests', () => {
page.locator('head meta[property="og:description"]'),
).toHaveAttribute('content', og_description)
}

await expect(page).toHaveScreenshot(`${testName}-${result}-desktop.png`, {
fullPage: true,
maxDiffPixelRatio: 0.12,
})
})
}
})
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 11 additions & 32 deletions tests/e2e/search.spec.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,10 @@
import dayjs from '../../services/dayjs'
import { test, expect } from '@playwright/test'
import { routeApiWithCors, scrollToBottom } from './helpers'

test.describe('Search Page Tests', () => {
test.beforeEach(async ({ page }) => {
await page.route('**/api/**', async (route) => {
const request = route.request()

// Handle OPTIONS preflight requests
if (request.method() === 'OPTIONS') {
await route.fulfill({
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
})
return
}

// For actual requests, fetch and add CORS headers
const response = await route.fetch()
const body = await response.body()
const headers = response.headers()

await route.fulfill({
status: response.status(),
headers: {
...headers,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
},
body: body,
})
})
await routeApiWithCors(page)

await page.goto('/search')
await page.waitForLoadState('networkidle')
Expand All @@ -52,6 +22,15 @@ test.describe('Search Page Tests', () => {
const href = await links.nth(i).getAttribute('href')
expect(href).toMatch(/\/m\//)
}

await page.waitForLoadState('networkidle')

await scrollToBottom(page)

await expect(page).toHaveScreenshot('search-desktop.png', {
fullPage: true,
maxDiffPixelRatio: 0.12,
})
})

test('shows relevant search results when filter changes', async ({
Expand Down
66 changes: 66 additions & 0 deletions tests/e2e/thematic-pages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { test, expect } from '@playwright/test'
import { routeApiWithCors } from './helpers'

test.describe('Thematic Pages Tests', () => {
test.beforeEach(async ({ page }) => {
await routeApiWithCors(page)
})

test('social-media - desktop', async ({ page }) => {
await page.goto(
'/social-media?since=2025-03-01&until=2025-03-02&probe_cc=CN%2CIR%2CRU',
)

await page.waitForLoadState('networkidle')

await expect(page).toHaveScreenshot('social-media-desktop.png', {
fullPage: false,
})
})

test('news-media - desktop', async ({ page }) => {
await page.goto(
'/news-media?since=2025-03-01&until=2025-03-02&probe_cc=CN%2CIR%2CRU',
)

await page.waitForLoadState('networkidle')

await expect(page).toHaveScreenshot('news-media-desktop.png', {
fullPage: false,
})
})

test('circumvention - desktop', async ({ page }) => {
await page.goto(
'/circumvention?since=2025-03-01&until=2025-03-02&probe_cc=CN%2CIR%2CRU',
)

await page.waitForLoadState('networkidle')

await expect(page).toHaveScreenshot('circumvention-desktop.png', {
fullPage: false,
})
})

test('domain - desktop', async ({ page }) => {
await page.goto('/domain/twitter.com?since=2025-03-01&until=2025-03-02')

await page.waitForLoadState('networkidle')

await expect(page).toHaveScreenshot('domain-desktop.png', {
fullPage: false,
})
})

test('network - desktop', async ({ page }) => {
await page.goto('/as/AS15598?since=2025-04-07&until=2025-04-08')

await page.waitForLoadState('networkidle')

await expect(page).toHaveScreenshot('network-desktop.png', {
fullPage: false,
})

await page.unrouteAll({ behavior: 'ignoreErrors' })
})
})
Loading
Loading